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 +146 -20 3.4.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,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
@@ -675,8 +735,11 @@
675 735
676 736 // Get Post ID.
677 737 $post_id = is_a( $post, 'WP_Post' ) ? $post->ID : 0;
678 738
739 + // Increment the render count, used to identify this block on the page.
740 + ++$this->render_count;
741 +
679 742 // Parse attributes, defining fallback defaults if required
680 743 // and moving some attributes (such as Gutenberg's styles), if defined.
681 744 $atts = $this->sanitize_and_declare_atts( $atts );
682 745
@@ -786,9 +849,9 @@
786 849 }
787 850
788 851 // Create form element.
789 852 $form = $parser->html->createElement( 'form' );
790 - $form->setAttribute( 'action', esc_url( get_permalink( $post_id ) ) );
853 + $form->setAttribute( 'action', esc_url( $this->get_current_url( $post_id ) ) );
791 854 $form->setAttribute( 'method', 'post' );
792 855
793 856 // Move form builder div contents into form.
794 857 while ( $block_container->hasChildNodes() ) {
@@ -794,8 +857,22 @@
794 857 while ( $block_container->hasChildNodes() ) {
795 858 $form->appendChild( $block_container->firstChild ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
796 859 }
797 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 +
798 875 // Add subscribed message if required.
799 876 if ( $this->subscriber_id ) {
800 877 $subscribed_message = $parser->html->createElement( 'div' );
801 878 $subscribed_message->setAttribute( 'class', 'convertkit-form-builder-subscribed-message' );
@@ -802,8 +879,35 @@
802 879 $subscribed_message->appendChild( $parser->html->createTextNode( $atts['text_if_subscribed'] ) );
803 880 $form->insertBefore( $subscribed_message, $form->firstChild ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
804 881 }
805 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 +
806 910 // Add hidden fields.
807 911 $fields = array(
808 912 'convertkit[post_id]' => absint( $post_id ),
809 913 'convertkit[store_entries]' => $atts['store_entries'] ? '1' : '0',
@@ -810,8 +914,9 @@
810 914 'convertkit[redirect]' => esc_url( $atts['redirect'] ),
811 915 'convertkit[form_id]' => absint( $atts['form_id'] ),
812 916 'convertkit[tag_id]' => absint( $atts['tag_id'] ),
813 917 'convertkit[sequence_id]' => absint( $atts['sequence_id'] ),
918 + 'convertkit[block_index]' => absint( $this->render_count ),
814 919 '_wpnonce' => wp_create_nonce( 'convertkit_block_form_builder' ),
815 920 );
816 921 foreach ( $fields as $name => $value ) {
817 922 $hidden = $parser->html->createElement( 'input' );
@@ -825,8 +930,29 @@
825 930 $block_container->appendChild( $form );
826 931
827 932 // Return modified content.
828 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'] ) ) );
829 955
830 956 }
831 957
832 958 }