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 +192 -49 3.3.2 → 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.
@@ -140,19 +191,19 @@
140 191
141 192 // Determine the subscriber state.
142 193 // If a Form is specified, mark the subscriber as inactive, so the form's double optin is honored.
143 194 // If a Tag or Sequence is specified, mark the subscriber as active, as there's no double optin for tags or sequences.
144 - $subscriber_state = $form_id !== false ? 'inactive' : 'active';
195 + $subscriber_state = $form_id ? 'inactive' : 'active';
145 196
146 197 // Create subscriber.
147 198 $result = $api->create_subscriber(
148 199 sanitize_email( $form_data['email'] ),
149 - array_key_exists( 'first_name', $form_data ) ? $form_data['first_name'] : '',
200 + $first_name,
150 201 $subscriber_state,
151 202 $custom_fields
152 203 );
153 204
154 - // Bail if an error occured.
205 + // Bail if an error occurred.
155 206 if ( is_wp_error( $result ) ) {
156 207 // Store entry and return.
157 208 if ( $form_data['store_entries'] ) {
158 209 $entries->upsert(
@@ -158,9 +209,9 @@
158 209 $entries->upsert(
159 210 array(
160 211 'post_id' => $form_data['post_id'],
161 212 'email' => $form_data['email'],
162 - 'first_name' => $form_data['first_name'],
213 + 'first_name' => $first_name,
163 214 'custom_fields' => $custom_fields,
164 215 'form_id' => $form_id,
165 216 'tag_id' => $tag_id,
166 217 'sequence_id' => $sequence_id,
@@ -168,8 +219,10 @@
168 219 'api_error' => $result->get_error_message(),
169 220 )
170 221 );
171 222 }
223 +
224 + $this->error = $result;
172 225 return;
173 226 }
174 227
175 228 // Store entry.
@@ -177,9 +230,9 @@
177 230 $entries->upsert(
178 231 array(
179 232 'post_id' => $form_data['post_id'],
180 233 'email' => $form_data['email'],
181 - 'first_name' => $form_data['first_name'],
234 + 'first_name' => $first_name,
182 235 'custom_fields' => $custom_fields,
183 236 'form_id' => $form_id,
184 237 'tag_id' => $tag_id,
185 238 'sequence_id' => $sequence_id,
@@ -187,11 +240,14 @@
187 240 )
188 241 );
189 242 }
190 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 +
191 247 // Store the subscriber ID in a cookie.
192 248 $subscriber = new ConvertKit_Subscriber();
193 - $subscriber->set( $result['subscriber']['id'] );
249 + $subscriber->set( $subscriber_id );
194 250
195 251 // If a form was specified, add the subscriber to the form.
196 252 if ( $form_id ) {
197 253 // For Legacy Forms, a different endpoint is used.
@@ -198,14 +254,14 @@
198 254 $forms = new ConvertKit_Resource_Forms();
199 255 if ( $forms->is_legacy( $form_id ) ) {
200 256 $result = $api->add_subscriber_to_legacy_form(
201 257 $form_id,
202 - $result['subscriber']['id']
258 + $subscriber_id
203 259 );
204 260 } else {
205 261 $result = $api->add_subscriber_to_form(
206 262 $form_id,
207 - $result['subscriber']['id'],
263 + $subscriber_id,
208 264 get_permalink( absint( $form_data['post_id'] ) )
209 265 );
210 266 }
211 267
@@ -213,9 +269,9 @@
213 269 $entries->upsert(
214 270 array(
215 271 'post_id' => $form_data['post_id'],
216 272 'email' => $form_data['email'],
217 - 'first_name' => $form_data['first_name'],
273 + 'first_name' => $first_name,
218 274 'custom_fields' => $custom_fields,
219 275 'form_id' => $form_id,
220 276 'tag_id' => $tag_id,
221 277 'sequence_id' => $sequence_id,
@@ -227,9 +283,9 @@
227 283 }
228 284
229 285 // If a tag was specified, add the subscriber to the tag.
230 286 if ( $tag_id ) {
231 - $result = $api->tag_subscriber( $tag_id, $result['subscriber']['id'] );
287 + $result = $api->tag_subscriber( $tag_id, $subscriber_id );
232 288
233 289 if ( $form_data['store_entries'] ) {
234 290 $entries->upsert(
235 291 array(
@@ -234,9 +290,9 @@
234 290 $entries->upsert(
235 291 array(
236 292 'post_id' => $form_data['post_id'],
237 293 'email' => $form_data['email'],
238 - 'first_name' => $form_data['first_name'],
294 + 'first_name' => $first_name,
239 295 'custom_fields' => $custom_fields,
240 296 'form_id' => $form_id,
241 297 'tag_id' => $tag_id,
242 298 'sequence_id' => $sequence_id,
@@ -248,9 +304,9 @@
248 304 }
249 305
250 306 // If a sequence was specified, add the subscriber to the sequence.
251 307 if ( $sequence_id ) {
252 - $result = $api->add_subscriber_to_sequence( $sequence_id, $result['subscriber']['id'] );
308 + $result = $api->add_subscriber_to_sequence( $sequence_id, $subscriber_id );
253 309
254 310 if ( $form_data['store_entries'] ) {
255 311 $entries->upsert(
256 312 array(
@@ -255,9 +311,9 @@
255 311 $entries->upsert(
256 312 array(
257 313 'post_id' => $form_data['post_id'],
258 314 'email' => $form_data['email'],
259 - 'first_name' => $form_data['first_name'],
315 + 'first_name' => $first_name,
260 316 'custom_fields' => $custom_fields,
261 317 'form_id' => $form_id,
262 318 'tag_id' => $tag_id,
263 319 'sequence_id' => $sequence_id,
@@ -273,10 +329,10 @@
273 329 if ( array_key_exists( 'redirect', $form_data ) && wp_http_validate_url( sanitize_url( $form_data['redirect'] ) ) ) {
274 330 // Redirect to the URL specified in the form.
275 331 $redirect = sanitize_url( $form_data['redirect'] );
276 332 } else {
277 - // Redirect to the Post the form was displayed on, to show a success message.
278 - $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'] ) );
279 335 }
280 336
281 337 // Redirect.
282 338 wp_redirect( $redirect ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
@@ -400,10 +456,20 @@
400 456 ),
401 457 ),
402 458 ),
403 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 +
404 468 'has_access_token' => $settings->has_access_and_refresh_token(),
405 - '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,
406 472 );
407 473
408 474 }
409 475
@@ -510,19 +576,29 @@
510 576 * @return bool|array
511 577 */
512 578 public function get_fields() {
513 579
514 - // Get Kit Forms.
515 - $forms = new ConvertKit_Resource_Forms( 'block_form_builder' );
516 - $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();
517 587 if ( $forms->exist() ) {
518 588 foreach ( $forms->get() as $form ) {
519 - // Legacy forms don't include a `format` key, so define them as inline.
520 - $forms_options[ $form['id'] ] = sprintf(
589 + $label = sprintf(
521 590 '%s [%s]',
522 591 sanitize_text_field( $form['name'] ),
592 + // Legacy forms don't include a `format` key, so define them as inline.
523 593 ( ! empty( $form['format'] ) ? sanitize_text_field( $form['format'] ) : 'inline' )
524 594 );
595 +
596 + if ( ! empty( $form['format'] ) ) {
597 + $forms_options[ $form['id'] ] = $label;
598 + } else {
599 + $forms_legacy_options[ $form['id'] ] = $label;
600 + }
525 601 }
526 602 }
527 603
528 604 // Get Kit Tags.
@@ -568,12 +644,13 @@
568 644 'value' => 0,
569 645 ),
570 646 ),
571 647 'form_id' => array(
572 - 'label' => __( 'Form', 'convertkit' ),
573 - 'type' => 'select',
574 - 'description' => __( 'The Kit form to add the subscriber to. Useful if you want to send an incentive email.', 'convertkit' ),
575 - '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,
576 653 ),
577 654 'tag_id' => array(
578 655 'label' => __( 'Tag', 'convertkit' ),
579 656 'type' => 'select',
@@ -658,8 +735,11 @@
658 735
659 736 // Get Post ID.
660 737 $post_id = is_a( $post, 'WP_Post' ) ? $post->ID : 0;
661 738
739 + // Increment the render count, used to identify this block on the page.
740 + ++$this->render_count;
741 +
662 742 // Parse attributes, defining fallback defaults if required
663 743 // and moving some attributes (such as Gutenberg's styles), if defined.
664 744 $atts = $this->sanitize_and_declare_atts( $atts );
665 745
@@ -720,26 +800,26 @@
720 800 '<button type="submit"$1>$2</button>',
721 801 $block_content
722 802 );
723 803
724 - // Return the button if reCAPTCHA does not need to be used.
725 - $settings = new ConvertKit_Settings();
726 - 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 ) {
727 808 return $block_content;
728 809 }
729 810
730 - // Enqueue reCAPTCHA JS.
731 - $recaptcha = new ConvertKit_Recaptcha();
732 - $recaptcha->enqueue_scripts();
811 + // Enqueue the spam protection provider's JS.
812 + $provider->enqueue_scripts();
733 813
734 - // Add reCAPTCHA attributes to button.
814 + // Parse the button's DOM.
735 815 $parser = new ConvertKit_HTML_Parser( $block_content );
736 816 $button = $parser->xpath->query( '//button' )->item( 0 );
737 - $button->setAttribute( 'data-sitekey', esc_attr( $settings->recaptcha_site_key() ) ); // @phpstan-ignore-line
738 - $button->setAttribute( 'data-callback', 'convertKitRecaptchaFormSubmit' ); // @phpstan-ignore-line
739 - $button->setAttribute( 'data-action', 'convertkit_form_builder' ); // @phpstan-ignore-line
740 - $button->setAttribute( 'class', trim( $button->getAttribute( 'class' ) . ' g-recaptcha' ) ); // @phpstan-ignore-line
741 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 +
742 822 // Return button HTML.
743 823 return $parser->get_body_html();
744 824
745 825 }
@@ -769,9 +849,9 @@
769 849 }
770 850
771 851 // Create form element.
772 852 $form = $parser->html->createElement( 'form' );
773 - $form->setAttribute( 'action', esc_url( get_permalink( $post_id ) ) );
853 + $form->setAttribute( 'action', esc_url( $this->get_current_url( $post_id ) ) );
774 854 $form->setAttribute( 'method', 'post' );
775 855
776 856 // Move form builder div contents into form.
777 857 while ( $block_container->hasChildNodes() ) {
@@ -777,8 +857,22 @@
777 857 while ( $block_container->hasChildNodes() ) {
778 858 $form->appendChild( $block_container->firstChild ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
779 859 }
780 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 +
781 875 // Add subscribed message if required.
782 876 if ( $this->subscriber_id ) {
783 877 $subscribed_message = $parser->html->createElement( 'div' );
784 878 $subscribed_message->setAttribute( 'class', 'convertkit-form-builder-subscribed-message' );
@@ -785,8 +879,35 @@
785 879 $subscribed_message->appendChild( $parser->html->createTextNode( $atts['text_if_subscribed'] ) );
786 880 $form->insertBefore( $subscribed_message, $form->firstChild ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
787 881 }
788 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 +
789 910 // Add hidden fields.
790 911 $fields = array(
791 912 'convertkit[post_id]' => absint( $post_id ),
792 913 'convertkit[store_entries]' => $atts['store_entries'] ? '1' : '0',
@@ -793,8 +914,9 @@
793 914 'convertkit[redirect]' => esc_url( $atts['redirect'] ),
794 915 'convertkit[form_id]' => absint( $atts['form_id'] ),
795 916 'convertkit[tag_id]' => absint( $atts['tag_id'] ),
796 917 'convertkit[sequence_id]' => absint( $atts['sequence_id'] ),
918 + 'convertkit[block_index]' => absint( $this->render_count ),
797 919 '_wpnonce' => wp_create_nonce( 'convertkit_block_form_builder' ),
798 920 );
799 921 foreach ( $fields as $name => $value ) {
800 922 $hidden = $parser->html->createElement( 'input' );
@@ -808,8 +930,29 @@
808 930 $block_container->appendChild( $form );
809 931
810 932 // Return modified content.
811 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'] ) ) );
812 955
813 956 }
814 957
815 958 }