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 +166 -29 3.3.7 → 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,13 +106,20 @@
76 106 if ( ! array_key_exists( 'post_id', $_REQUEST['convertkit'] ) ) {
77 107 return;
78 108 }
79 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 +
80 115 // Check spam protection.
81 116 $spam_protection = new ConvertKit_Spam_Protection();
82 117
83 118 // Bail if spam protection failed.
84 - if ( is_wp_error( $spam_protection->verify( 'convertkit_form_builder' ) ) ) {
119 + $spam_protection_result = $spam_protection->verify( 'convertkit_form_builder' );
120 + if ( is_wp_error( $spam_protection_result ) ) {
121 + $this->error = $spam_protection_result;
85 122 return;
86 123 }
87 124
88 125 // Sanitize form data.
@@ -87,8 +124,18 @@
87 124
88 125 // Sanitize form data.
89 126 $form_data = map_deep( wp_unslash( $_REQUEST['convertkit'] ), 'sanitize_text_field' );
90 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 +
91 138 // Build custom fields, if any were specified.
92 139 $custom_fields = array();
93 140 if ( array_key_exists( 'custom_fields', $form_data ) ) {
94 141 $custom_fields = $form_data['custom_fields'];
@@ -93,12 +140,15 @@
93 140 if ( array_key_exists( 'custom_fields', $form_data ) ) {
94 141 $custom_fields = $form_data['custom_fields'];
95 142 }
96 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 +
97 147 // Get Form, Tag and Sequence IDs, if any were specified.
98 - $form_id = array_key_exists( 'form_id', $form_data ) ? $form_data['form_id'] : false;
99 - $tag_id = array_key_exists( 'tag_id', $form_data ) ? $form_data['tag_id'] : false;
100 - $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;
101 151
102 152 // Initialize classes that will be used.
103 153 $settings = new ConvertKit_Settings();
104 154 $entries = new ConvertKit_Form_Entries();
@@ -110,9 +160,9 @@
110 160 $entries->upsert(
111 161 array(
112 162 'post_id' => $form_data['post_id'],
113 163 'email' => $form_data['email'],
114 - 'first_name' => $form_data['first_name'],
164 + 'first_name' => $first_name,
115 165 'custom_fields' => $custom_fields,
116 166 'form_id' => $form_id,
117 167 'tag_id' => $tag_id,
118 168 'sequence_id' => $sequence_id,
@@ -120,8 +170,13 @@
120 170 'api_error' => __( 'Plugin Access Token not configured', 'convertkit' ),
121 171 )
122 172 );
123 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 + );
124 179 return;
125 180 }
126 181
127 182 // Initialize the API.
@@ -136,14 +191,14 @@
136 191
137 192 // Determine the subscriber state.
138 193 // If a Form is specified, mark the subscriber as inactive, so the form's double optin is honored.
139 194 // If a Tag or Sequence is specified, mark the subscriber as active, as there's no double optin for tags or sequences.
140 - $subscriber_state = $form_id !== false ? 'inactive' : 'active';
195 + $subscriber_state = $form_id ? 'inactive' : 'active';
141 196
142 197 // Create subscriber.
143 198 $result = $api->create_subscriber(
144 199 sanitize_email( $form_data['email'] ),
145 - array_key_exists( 'first_name', $form_data ) ? $form_data['first_name'] : '',
200 + $first_name,
146 201 $subscriber_state,
147 202 $custom_fields
148 203 );
149 204
@@ -154,9 +209,9 @@
154 209 $entries->upsert(
155 210 array(
156 211 'post_id' => $form_data['post_id'],
157 212 'email' => $form_data['email'],
158 - 'first_name' => $form_data['first_name'],
213 + 'first_name' => $first_name,
159 214 'custom_fields' => $custom_fields,
160 215 'form_id' => $form_id,
161 216 'tag_id' => $tag_id,
162 217 'sequence_id' => $sequence_id,
@@ -164,8 +219,10 @@
164 219 'api_error' => $result->get_error_message(),
165 220 )
166 221 );
167 222 }
223 +
224 + $this->error = $result;
168 225 return;
169 226 }
170 227
171 228 // Store entry.
@@ -173,9 +230,9 @@
173 230 $entries->upsert(
174 231 array(
175 232 'post_id' => $form_data['post_id'],
176 233 'email' => $form_data['email'],
177 - 'first_name' => $form_data['first_name'],
234 + 'first_name' => $first_name,
178 235 'custom_fields' => $custom_fields,
179 236 'form_id' => $form_id,
180 237 'tag_id' => $tag_id,
181 238 'sequence_id' => $sequence_id,
@@ -183,11 +240,14 @@
183 240 )
184 241 );
185 242 }
186 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 +
187 247 // Store the subscriber ID in a cookie.
188 248 $subscriber = new ConvertKit_Subscriber();
189 - $subscriber->set( $result['subscriber']['id'] );
249 + $subscriber->set( $subscriber_id );
190 250
191 251 // If a form was specified, add the subscriber to the form.
192 252 if ( $form_id ) {
193 253 // For Legacy Forms, a different endpoint is used.
@@ -194,14 +254,14 @@
194 254 $forms = new ConvertKit_Resource_Forms();
195 255 if ( $forms->is_legacy( $form_id ) ) {
196 256 $result = $api->add_subscriber_to_legacy_form(
197 257 $form_id,
198 - $result['subscriber']['id']
258 + $subscriber_id
199 259 );
200 260 } else {
201 261 $result = $api->add_subscriber_to_form(
202 262 $form_id,
203 - $result['subscriber']['id'],
263 + $subscriber_id,
204 264 get_permalink( absint( $form_data['post_id'] ) )
205 265 );
206 266 }
207 267
@@ -209,9 +269,9 @@
209 269 $entries->upsert(
210 270 array(
211 271 'post_id' => $form_data['post_id'],
212 272 'email' => $form_data['email'],
213 - 'first_name' => $form_data['first_name'],
273 + 'first_name' => $first_name,
214 274 'custom_fields' => $custom_fields,
215 275 'form_id' => $form_id,
216 276 'tag_id' => $tag_id,
217 277 'sequence_id' => $sequence_id,
@@ -223,9 +283,9 @@
223 283 }
224 284
225 285 // If a tag was specified, add the subscriber to the tag.
226 286 if ( $tag_id ) {
227 - $result = $api->tag_subscriber( $tag_id, $result['subscriber']['id'] );
287 + $result = $api->tag_subscriber( $tag_id, $subscriber_id );
228 288
229 289 if ( $form_data['store_entries'] ) {
230 290 $entries->upsert(
231 291 array(
@@ -230,9 +290,9 @@
230 290 $entries->upsert(
231 291 array(
232 292 'post_id' => $form_data['post_id'],
233 293 'email' => $form_data['email'],
234 - 'first_name' => $form_data['first_name'],
294 + 'first_name' => $first_name,
235 295 'custom_fields' => $custom_fields,
236 296 'form_id' => $form_id,
237 297 'tag_id' => $tag_id,
238 298 'sequence_id' => $sequence_id,
@@ -244,9 +304,9 @@
244 304 }
245 305
246 306 // If a sequence was specified, add the subscriber to the sequence.
247 307 if ( $sequence_id ) {
248 - $result = $api->add_subscriber_to_sequence( $sequence_id, $result['subscriber']['id'] );
308 + $result = $api->add_subscriber_to_sequence( $sequence_id, $subscriber_id );
249 309
250 310 if ( $form_data['store_entries'] ) {
251 311 $entries->upsert(
252 312 array(
@@ -251,9 +311,9 @@
251 311 $entries->upsert(
252 312 array(
253 313 'post_id' => $form_data['post_id'],
254 314 'email' => $form_data['email'],
255 - 'first_name' => $form_data['first_name'],
315 + 'first_name' => $first_name,
256 316 'custom_fields' => $custom_fields,
257 317 'form_id' => $form_id,
258 318 'tag_id' => $tag_id,
259 319 'sequence_id' => $sequence_id,
@@ -269,10 +329,10 @@
269 329 if ( array_key_exists( 'redirect', $form_data ) && wp_http_validate_url( sanitize_url( $form_data['redirect'] ) ) ) {
270 330 // Redirect to the URL specified in the form.
271 331 $redirect = sanitize_url( $form_data['redirect'] );
272 332 } else {
273 - // Redirect to the Post the form was displayed on, to show a success message.
274 - $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'] ) );
275 335 }
276 336
277 337 // Redirect.
278 338 wp_redirect( $redirect ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
@@ -516,19 +576,29 @@
516 576 * @return bool|array
517 577 */
518 578 public function get_fields() {
519 579
520 - // Get Kit Forms.
521 - $forms = new ConvertKit_Resource_Forms( 'block_form_builder' );
522 - $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();
523 587 if ( $forms->exist() ) {
524 588 foreach ( $forms->get() as $form ) {
525 - // Legacy forms don't include a `format` key, so define them as inline.
526 - $forms_options[ $form['id'] ] = sprintf(
589 + $label = sprintf(
527 590 '%s [%s]',
528 591 sanitize_text_field( $form['name'] ),
592 + // Legacy forms don't include a `format` key, so define them as inline.
529 593 ( ! empty( $form['format'] ) ? sanitize_text_field( $form['format'] ) : 'inline' )
530 594 );
595 +
596 + if ( ! empty( $form['format'] ) ) {
597 + $forms_options[ $form['id'] ] = $label;
598 + } else {
599 + $forms_legacy_options[ $form['id'] ] = $label;
600 + }
531 601 }
532 602 }
533 603
534 604 // Get Kit Tags.
@@ -574,12 +644,13 @@
574 644 'value' => 0,
575 645 ),
576 646 ),
577 647 'form_id' => array(
578 - 'label' => __( 'Form', 'convertkit' ),
579 - 'type' => 'select',
580 - 'description' => __( 'The Kit form to add the subscriber to. Useful if you want to send an incentive email.', 'convertkit' ),
581 - '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,
582 653 ),
583 654 'tag_id' => array(
584 655 'label' => __( 'Tag', 'convertkit' ),
585 656 'type' => 'select',
@@ -664,8 +735,11 @@
664 735
665 736 // Get Post ID.
666 737 $post_id = is_a( $post, 'WP_Post' ) ? $post->ID : 0;
667 738
739 + // Increment the render count, used to identify this block on the page.
740 + ++$this->render_count;
741 +
668 742 // Parse attributes, defining fallback defaults if required
669 743 // and moving some attributes (such as Gutenberg's styles), if defined.
670 744 $atts = $this->sanitize_and_declare_atts( $atts );
671 745
@@ -775,9 +849,9 @@
775 849 }
776 850
777 851 // Create form element.
778 852 $form = $parser->html->createElement( 'form' );
779 - $form->setAttribute( 'action', esc_url( get_permalink( $post_id ) ) );
853 + $form->setAttribute( 'action', esc_url( $this->get_current_url( $post_id ) ) );
780 854 $form->setAttribute( 'method', 'post' );
781 855
782 856 // Move form builder div contents into form.
783 857 while ( $block_container->hasChildNodes() ) {
@@ -783,8 +857,22 @@
783 857 while ( $block_container->hasChildNodes() ) {
784 858 $form->appendChild( $block_container->firstChild ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
785 859 }
786 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 +
787 875 // Add subscribed message if required.
788 876 if ( $this->subscriber_id ) {
789 877 $subscribed_message = $parser->html->createElement( 'div' );
790 878 $subscribed_message->setAttribute( 'class', 'convertkit-form-builder-subscribed-message' );
@@ -791,8 +879,35 @@
791 879 $subscribed_message->appendChild( $parser->html->createTextNode( $atts['text_if_subscribed'] ) );
792 880 $form->insertBefore( $subscribed_message, $form->firstChild ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
793 881 }
794 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 +
795 910 // Add hidden fields.
796 911 $fields = array(
797 912 'convertkit[post_id]' => absint( $post_id ),
798 913 'convertkit[store_entries]' => $atts['store_entries'] ? '1' : '0',
@@ -799,8 +914,9 @@
799 914 'convertkit[redirect]' => esc_url( $atts['redirect'] ),
800 915 'convertkit[form_id]' => absint( $atts['form_id'] ),
801 916 'convertkit[tag_id]' => absint( $atts['tag_id'] ),
802 917 'convertkit[sequence_id]' => absint( $atts['sequence_id'] ),
918 + 'convertkit[block_index]' => absint( $this->render_count ),
803 919 '_wpnonce' => wp_create_nonce( 'convertkit_block_form_builder' ),
804 920 );
805 921 foreach ( $fields as $name => $value ) {
806 922 $hidden = $parser->html->createElement( 'input' );
@@ -814,8 +930,29 @@
814 930 $block_container->appendChild( $form );
815 931
816 932 // Return modified content.
817 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'] ) ) );
818 955
819 956 }
820 957
821 958 }