PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 2.7.5
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v2.7.5
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/class-convertkit-output.php +495 -21 2.3.0 → 2.7.5 View file →
@@ -66,18 +66,84 @@
66 66 * @since 1.9.6
67 67 */
68 68 public function __construct() {
69 69
70 - add_action( 'init', array( $this, 'get_subscriber_id_from_request' ), 1 );
70 + add_action( 'init', array( $this, 'get_subscriber_id_from_request' ) );
71 + add_action( 'wp', array( $this, 'maybe_tag_subscriber' ) );
71 72 add_action( 'template_redirect', array( $this, 'output_form' ) );
72 73 add_action( 'template_redirect', array( $this, 'page_takeover' ) );
73 74 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
74 75 add_filter( 'the_content', array( $this, 'append_form_to_content' ) );
76 + add_filter( 'hooked_block_types', array( $this, 'maybe_register_form_block_on_category_archive' ), 10, 4 );
77 + add_filter( 'hooked_block_convertkit/form', array( $this, 'append_form_block_to_category_archive' ), 10, 1 );
78 + add_action( 'wp_footer', array( $this, 'output_global_non_inline_form' ), 1 );
75 79 add_action( 'wp_footer', array( $this, 'output_scripts_footer' ) );
76 80
77 81 }
78 82
79 83 /**
84 + * Tags the subscriber, if:
85 + * - a subscriber ID exists in the cookie or URL,
86 + * - the WordPress Page has the "Add a Tag" setting specified
87 + *
88 + * @since 2.4.9.1
89 + */
90 + public function maybe_tag_subscriber() {
91 +
92 + // Bail if no subscriber ID detected.
93 + if ( ! $this->subscriber_id ) {
94 + return;
95 + }
96 +
97 + // Bail if not a singular Post Type supported by ConvertKit.
98 + if ( ! is_singular( convertkit_get_supported_post_types() ) ) {
99 + return;
100 + }
101 +
102 + // Get Post ID.
103 + $post_id = get_the_ID();
104 +
105 + // Bail if a Post ID couldn't be identified.
106 + if ( ! $post_id ) {
107 + return;
108 + }
109 +
110 + // Get Settings, if they have not yet been loaded.
111 + if ( ! $this->settings ) {
112 + $this->settings = new ConvertKit_Settings();
113 + }
114 +
115 + // Bail if the API hasn't been configured.
116 + if ( ! $this->settings->has_access_and_refresh_token() ) {
117 + return;
118 + }
119 +
120 + // Get ConvertKit Post's Settings, if they have not yet been loaded.
121 + if ( ! $this->post_settings ) {
122 + $this->post_settings = new ConvertKit_Post( $post_id );
123 + }
124 +
125 + // Bail if no "Add a Tag" setting specified for this Page.
126 + if ( ! $this->post_settings->has_tag() ) {
127 + return;
128 + }
129 +
130 + // Initialize the API.
131 + $api = new ConvertKit_API_V4(
132 + CONVERTKIT_OAUTH_CLIENT_ID,
133 + CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI,
134 + $this->settings->get_access_token(),
135 + $this->settings->get_refresh_token(),
136 + $this->settings->debug_enabled(),
137 + 'output'
138 + );
139 +
140 + // Tag subscriber.
141 + $api->tag_subscriber( $this->post_settings->get_tag(), $this->subscriber_id );
142 +
143 + }
144 +
145 + /**
80 146 * Runs the `convertkit_output_output_form` action for singular Post Types that don't use the_content()
81 147 * or apply_filters( 'the_content' ) to output a ConvertKit Form.
82 148 *
83 149 * @since 1.9.6
@@ -159,8 +225,23 @@
159 225
160 226 // Replace the favicon with the WordPress site's favicon, if specified.
161 227 $landing_page = $this->landing_pages->replace_favicon( $landing_page );
162 228
229 + /**
230 + * Perform any actions immediately prior to outputting the Landing Page.
231 + *
232 + * Caching and minification Plugins may need to hook here to prevent
233 + * CSS / JS minification and lazy loading images, which can interfere
234 + * with Landing Pages.
235 + *
236 + * @since 2.4.4
237 + *
238 + * @param string $landing_page ConvertKit Landing Page HTML.
239 + * @param int $landing_page_id ConvertKit Landing Page ID.
240 + * @param int $post_id WordPress Page ID.
241 + */
242 + do_action( 'convertkit_output_landing_page_before', $landing_page, $landing_page_id, $post_id );
243 +
163 244 // Output Landing Page.
164 245 // Output is supplied from ConvertKit's API, which is already sanitized.
165 246 echo $landing_page; // phpcs:ignore WordPress.Security.EscapeOutput
166 247 exit;
@@ -167,9 +248,9 @@
167 248
168 249 }
169 250
170 251 /**
171 - * Appends a form to the singular Page, Post or Custom Post Type's Content.
252 + * Inserts a form to the singular Page, Post or Custom Post Type's Content.
172 253 *
173 254 * @param string $content Post Content.
174 255 * @return string Post Content with Form Appended, if applicable
175 256 */
@@ -174,15 +255,22 @@
174 255 * @return string Post Content with Form Appended, if applicable
175 256 */
176 257 public function append_form_to_content( $content ) {
177 258
178 - // Bail if not a singular Post Type.
179 - if ( ! is_singular() ) {
259 + // Bail if not a singular Post Type supported by ConvertKit.
260 + if ( ! is_singular( convertkit_get_supported_post_types() ) ) {
180 261 return $content;
181 262 }
182 263
183 - // Get Post ID and ConvertKit Form ID for the Post.
264 + // Get Post ID.
184 265 $post_id = get_the_ID();
266 +
267 + // Bail if Post Type is not supported by Kit.
268 + if ( ! in_array( get_post_type( $post_id ), convertkit_get_supported_post_types(), true ) ) {
269 + return $content;
270 + }
271 +
272 + // Get Form ID for the Post.
185 273 $form_id = $this->get_post_form_id( $post_id );
186 274
187 275 /**
188 276 * Define the ConvertKit Form ID to display for the given Post ID,
@@ -215,9 +303,9 @@
215 303 // - the form was deleted from the ConvertKit account.
216 304 // Attempt to fallback to the default form for this Post Type.
217 305 if ( is_wp_error( $form ) ) {
218 306 if ( $this->settings->debug_enabled() ) {
219 - $content .= '<!-- ConvertKit append_form_to_content(): ' . $form->get_error_message() . ' Attempting fallback to Default Form. -->';
307 + $content .= '<!-- Kit append_form_to_content(): ' . $form->get_error_message() . ' Attempting fallback to Default Form. -->';
220 308 }
221 309
222 310 // Get Default Form ID for this Post's Type.
223 311 $form_id = $this->settings->get_default_form( get_post_type( $post_id ) );
@@ -224,9 +312,9 @@
224 312
225 313 // If no Default Form is specified, just return the Post Content, unedited.
226 314 if ( ! $form_id ) {
227 315 if ( $this->settings->debug_enabled() ) {
228 - $content .= '<!-- ConvertKit append_form_to_content(): No Default Form exists as a fallback. -->';
316 + $content .= '<!-- Kit append_form_to_content(): No Default Form exists as a fallback. -->';
229 317 }
230 318
231 319 return $content;
232 320 }
@@ -237,9 +325,9 @@
237 325 // If an error occured again, the default form doesn't exist in this ConvertKit account.
238 326 // Just return the Post Content, unedited.
239 327 if ( is_wp_error( $form ) ) {
240 328 if ( $this->settings->debug_enabled() ) {
241 - $content .= '<!-- ConvertKit append_form_to_content(): Default Form: ' . $form->get_error_message() . ' -->';
329 + $content .= '<!-- Kit append_form_to_content(): Default Form: ' . $form->get_error_message() . ' -->';
242 330 }
243 331
244 332 return $content;
245 333 }
@@ -244,23 +332,70 @@
244 332 return $content;
245 333 }
246 334 }
247 335
336 + // If the Form HTML is empty, it's a modal form that has been set to load in the footer of the site.
337 + // We don't need to append anything to the content.
338 + if ( empty( $form ) ) {
339 + if ( $this->settings->debug_enabled() ) {
340 + $content .= '<!-- Kit append_form_to_content(): Form is non-inline, appended to footer. -->';
341 + }
342 +
343 + return $content;
344 + }
345 +
248 346 // If here, we have a ConvertKit Form.
249 - // Append form to Post's Content.
250 - $content = $content .= $form;
347 + // Append form to Post's Content, based on the position setting.
348 + $form_position = $this->settings->get_default_form_position( get_post_type( $post_id ) );
251 349
350 + if ( $this->settings->debug_enabled() ) {
351 + $content .= '<!-- Kit append_form_to_content(): Form Position: ' . esc_html( $form_position ) . ' -->';
352 + }
353 +
354 + switch ( $form_position ) {
355 + case 'before_after_content':
356 + $content = $form . $content . $form;
357 + break;
358 +
359 + case 'before_content':
360 + $content = $form . $content;
361 + break;
362 +
363 + case 'after_element':
364 + $element = $this->settings->get_default_form_position_element( get_post_type( $post_id ) );
365 + $index = $this->settings->get_default_form_position_element_index( get_post_type( $post_id ) );
366 +
367 + // Check if DOMDocument is installed.
368 + // It should be installed as mosts hosts include php-dom and php-xml modules.
369 + // If not, fallback to using preg_match_all(), which is less reliable.
370 + if ( ! class_exists( 'DOMDocument' ) ) {
371 + $content = $this->inject_form_after_element_fallback( $content, $element, $index, $form );
372 + break;
373 + }
374 +
375 + // Use DOMDocument.
376 + $content = $this->inject_form_after_element( $content, $element, $index, $form );
377 + break;
378 +
379 + case 'after_content':
380 + default:
381 + // Default behaviour < 2.5.8 was to append the Form after the content.
382 + $content .= $form;
383 + break;
384 + }
385 +
252 386 /**
253 387 * Filter the Post's Content, which includes a ConvertKit Form, immediately before it is output.
254 388 *
255 389 * @since 1.9.6
256 390 *
257 - * @param string $content Post Content
258 - * @param string $form ConvertKit Form HTML
259 - * @param int $post_id Post ID
260 - * @param int $form_id ConvertKit Form ID
391 + * @param string $content Post Content
392 + * @param string $form ConvertKit Form HTML
393 + * @param int $post_id Post ID
394 + * @param int $form_id ConvertKit Form ID
395 + * @param string $form_position Form Position setting for the Post's Type.
261 396 */
262 - $content = apply_filters( 'convertkit_frontend_append_form', $content, $form, $post_id, $form_id );
397 + $content = apply_filters( 'convertkit_frontend_append_form', $content, $form, $post_id, $form_id, $form_position );
263 398
264 399 return $content;
265 400
266 401 }
@@ -265,8 +400,237 @@
265 400
266 401 }
267 402
268 403 /**
404 + * Injects the form after the given element and index, using DOMDocument.
405 + *
406 + * @since 2.6.2
407 + *
408 + * @param string $content Page / Post Content.
409 + * @param string $tag HTML tag to insert form after.
410 + * @param int $index Number of $tag elements to find before inserting form.
411 + * @param string $form Form HTML to inject.
412 + * @return string
413 + */
414 + private function inject_form_after_element( $content, $tag, $index, $form ) {
415 +
416 + // If the form is empty, don't inject anything.
417 + if ( empty( $form ) ) {
418 + return $content;
419 + }
420 +
421 + // Define the meta tag.
422 + $meta_tag = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
423 +
424 + // Wrap content in <html>, <head> and <body> tags now, so we can inject the UTF-8 Content-Type meta tag.
425 + $modified_content = '<html><head></head><body>' . $content . '</body></html>';
426 +
427 + // Forcibly tell DOMDocument that this HTML uses the UTF-8 charset.
428 + // <meta charset="utf-8"> isn't enough, as DOMDocument still interprets the HTML as ISO-8859, which breaks character encoding
429 + // Use of mb_convert_encoding() with HTML-ENTITIES is deprecated in PHP 8.2, so we have to use this method.
430 + // If we don't, special characters render incorrectly.
431 + $modified_content = str_replace( '<head>', '<head>' . "\n" . $meta_tag, $modified_content );
432 +
433 + // Load Page / Post content into DOMDocument.
434 + libxml_use_internal_errors( true );
435 + $html = new DOMDocument();
436 + $html->loadHTML( $modified_content, LIBXML_HTML_NODEFDTD );
437 +
438 + // Find the element to append the form to.
439 + // item() is a zero based index.
440 + $element_node = $html->getElementsByTagName( $tag )->item( $index - 1 );
441 +
442 + // If the element could not be found, either the number of elements by tag name is less
443 + // than the requested position the form be inserted in, or no element exists.
444 + // Append the form to the original content and return.
445 + if ( is_null( $element_node ) ) {
446 + return $content . $form;
447 + }
448 +
449 + // Create new element for the Form.
450 + $form_node = new DOMDocument();
451 + $form_node->loadHTML( $form, LIBXML_HTML_NODEFDTD );
452 +
453 + // Append the form to the specific element.
454 + $element_node->parentNode->insertBefore( $html->importNode( $form_node->documentElement, true ), $element_node->nextSibling ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
455 +
456 + // Fetch HTML string.
457 + $modified_content = $html->saveHTML();
458 +
459 + // Remove some HTML tags that DOMDocument adds, returning the output.
460 + // We do this instead of using LIBXML_HTML_NOIMPLIED in loadHTML(), because Legacy Forms are not always contained in
461 + // a single root / outer element, which is required for LIBXML_HTML_NOIMPLIED to correctly work.
462 + $modified_content = str_replace( '<html>', '', $modified_content );
463 + $modified_content = str_replace( '</html>', '', $modified_content );
464 + $modified_content = str_replace( '<head>', '', $modified_content );
465 + $modified_content = str_replace( '</head>', '', $modified_content );
466 + $modified_content = str_replace( '<body>', '', $modified_content );
467 + $modified_content = str_replace( '</body>', '', $modified_content );
468 + $modified_content = str_replace( $meta_tag, '', $modified_content );
469 +
470 + return $modified_content;
471 +
472 + }
473 +
474 + /**
475 + * Injects the form after the given element and index, using preg_match_all().
476 + * This is less reliable than DOMDocument, and is called if DOMDocument is
477 + * not installed on the server.
478 + *
479 + * @since 2.6.2
480 + *
481 + * @param string $content Page / Post Content.
482 + * @param string $tag HTML tag to insert form after.
483 + * @param int $index Number of $tag elements to find before inserting form.
484 + * @param string $form Form HTML to inject.
485 + * @return string
486 + */
487 + private function inject_form_after_element_fallback( $content, $tag, $index, $form ) {
488 +
489 + // If the form is empty, don't inject anything.
490 + if ( empty( $form ) ) {
491 + return $content;
492 + }
493 +
494 + // Calculate tag length.
495 + $tag_length = ( strlen( $tag ) + 3 );
496 +
497 + // Find all closing elements.
498 + preg_match_all( '/<\/' . $tag . '>/', $content, $matches );
499 +
500 + // If no elements exist, just append the form.
501 + if ( count( $matches[0] ) === 0 ) {
502 + $content = $content . $form;
503 + return $content;
504 + }
505 +
506 + // If the number of elements is less than the index, we don't have enough elements to add the form to.
507 + // Just add the form after the content.
508 + if ( count( $matches[0] ) <= $index ) {
509 + $content = $content . $form;
510 + return $content;
511 + }
512 +
513 + // Iterate through the content to find the element at the configured index e.g. find the 4th closing paragraph.
514 + $offset = 0;
515 + foreach ( $matches[0] as $element_index => $element ) {
516 + $position = strpos( $content, $element, $offset );
517 + if ( ( $element_index + 1 ) === $index ) {
518 + return substr( $content, 0, $position + 4 ) . $form . substr( $content, $position + 4 );
519 + }
520 +
521 + // Increment offset.
522 + $offset = $position + 1;
523 + }
524 +
525 + // If here, something went wrong.
526 + // Just add the form after the content.
527 + $content = $content . $form;
528 + return $content;
529 +
530 + }
531 +
532 + /**
533 + * Registers the ConvertKit Form block to before or after the Query Loop block, when viewing a Category archive.
534 + *
535 + * See append_form_block_on_category_archive() configures the block to display the applicable category's Form.
536 + *
537 + * @since 2.4.9.1
538 + *
539 + * @param array $hooked_blocks The list of hooked block types.
540 + * @param string $position The relative position of the hooked blocks.
541 + * @param string $anchor_block The anchor block type.
542 + * @param WP_Block_Template|WP_Post|array $context The block template, template part, wp_navigation post type, or pattern that the anchor block belongs to.
543 + * @return array
544 + */
545 + public function maybe_register_form_block_on_category_archive( $hooked_blocks, $position, $anchor_block, $context ) {
546 +
547 + // Don't append if we're not viewing a category archive.
548 + if ( ! is_category() ) {
549 + return $hooked_blocks;
550 + }
551 +
552 + if ( $context instanceof WP_Block_Template && $context->slug !== 'archive' ) {
553 + return $hooked_blocks;
554 + }
555 +
556 + // Don't append if the anchor block isn't the Query Loop block.
557 + if ( $anchor_block !== 'core/query' ) {
558 + return $hooked_blocks;
559 + }
560 +
561 + // Don't append if the Category's form position setting is not defined.
562 + $form_position = $this->get_term_form_position();
563 + if ( ! $form_position ) {
564 + // Unhook this function as we don't need to check again in this request, as we'll
565 + // never output a form on the Category archive.
566 + remove_filter( 'hooked_block_types', array( $this, 'maybe_register_form_block_on_category_archive' ), 10 );
567 +
568 + return $hooked_blocks;
569 + }
570 +
571 + // Don't append if the position doesn't match.
572 + if ( $form_position !== $position ) {
573 + return $hooked_blocks;
574 + }
575 +
576 + // Hook the ConvertKit Form block.
577 + $hooked_blocks[] = 'convertkit/form';
578 +
579 + // Unhook this function as we don't need to check again in this request, as
580 + // we have now appended the form.
581 + remove_filter( 'hooked_block_types', array( $this, 'maybe_register_form_block_on_category_archive' ), 10 );
582 +
583 + return $hooked_blocks;
584 +
585 + }
586 +
587 + /**
588 + * Configures the ConvertKit Form block that was hooked below the Query Loop block by maybe_register_form_block_on_category_archive,
589 + * defining the Form ID based on the current Category's Form ID.
590 + *
591 + * @since 2.4.9.1
592 + *
593 + * @param array $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block.
594 + * @return null|array
595 + */
596 + public function append_form_block_to_category_archive( $parsed_hooked_block ) {
597 +
598 + // Sanity check that we're still viewing a Category archive.
599 + if ( ! is_category() ) {
600 + // Returning null will unregister the Form block from displaying.
601 + return null;
602 + }
603 +
604 + // Get Category archive being viewed.
605 + $category = get_category( get_query_var( 'cat' ) );
606 +
607 + // Bail if the Category could be found.
608 + if ( is_wp_error( $category ) || is_null( $category ) ) {
609 + // Returning null will unregister the Form block from displaying.
610 + return null;
611 + }
612 +
613 + // Load Term Settings.
614 + $term_settings = new ConvertKit_Term( $category->term_id );
615 +
616 + // Bail if no Form specified for the Category.
617 + if ( ! $term_settings->has_form() ) {
618 + // Returning null will unregister the Form block from displaying.
619 + return null;
620 + }
621 +
622 + // Define the form block attributes to display the given Form ID.
623 + $parsed_hooked_block['attrs'] = array(
624 + 'id' => absint( $term_settings->get_form() ),
625 + );
626 +
627 + // Return the Form block with its attributes.
628 + return $parsed_hooked_block;
629 +
630 + }
631 +
632 + /**
269 633 * Returns the Post, Category or Plugin ConvertKit Form ID for the given Post.
270 634 *
271 635 * If the Post specifies a form to use, returns that Form ID.
272 636 * If the Post uses the 'Default' setting, and an assigned Category has a Form ID, uses the Category's Form ID.
@@ -332,16 +696,53 @@
332 696 // If a Form ID exists, return it now.
333 697 if ( $term_settings->has_form() ) {
334 698 return $term_settings->get_form();
335 699 }
700 +
701 + // If the Term specifies that no Form should be used, return false.
702 + if ( $term_settings->uses_no_form() ) {
703 + return false;
704 + }
336 705 }
337 706
338 - // If here, use the Plugin's Default Form.
707 + // If here, all Terms were set to display the Default Form.
708 + // Therefore use the Plugin's Default Form.
339 709 return $this->settings->get_default_form( get_post_type( $post_id ) );
340 710
341 711 }
342 712
343 713 /**
714 + * Returns the Form Position setting for the currently viewed Category.
715 + *
716 + * @since 2.4.9.1
717 + *
718 + * @return bool|string
719 + */
720 + private function get_term_form_position() {
721 +
722 + // Get Category archive being viewed.
723 + $category = get_category( get_query_var( 'cat' ) );
724 +
725 + // Bail if the Category could be found.
726 + if ( is_wp_error( $category ) || is_null( $category ) ) {
727 + return false;
728 + }
729 +
730 + // Load Term Settings.
731 + $term_settings = new ConvertKit_Term( $category->term_id );
732 +
733 + // Return false if no form position is defined i.e. we don't want to display
734 + // it on the Category archive.
735 + if ( ! $term_settings->has_form_position() ) {
736 + return false;
737 + }
738 +
739 + // Return form position.
740 + return $term_settings->get_form_position();
741 +
742 + }
743 +
744 + /**
344 745 * Enqueue scripts.
345 746 *
346 747 * @since 1.9.6
347 748 */
@@ -362,9 +763,9 @@
362 763 // Register scripts that we might use.
363 764 wp_register_script(
364 765 'convertkit-js',
365 766 CONVERTKIT_PLUGIN_URL . 'resources/frontend/js/convertkit.js',
366 - array( 'jquery' ),
767 + array(),
367 768 CONVERTKIT_PLUGIN_VERSION,
368 769 true
369 770 );
370 771 wp_localize_script(
@@ -374,10 +775,8 @@
374 775 'ajaxurl' => admin_url( 'admin-ajax.php' ),
375 776 'debug' => $settings->debug_enabled(),
376 777 'nonce' => wp_create_nonce( 'convertkit' ),
377 778 'subscriber_id' => $this->subscriber_id,
378 - 'tag' => ( ( is_singular() && $convertkit_post->has_tag() ) ? $convertkit_post->get_tag() : false ),
379 - 'post_id' => $post->ID,
380 779 )
381 780 );
382 781
383 782 // Bail if the no scripts setting is enabled.
@@ -410,8 +809,64 @@
410 809
411 810 }
412 811
413 812 /**
813 + * Outputs a non-inline forms if defined in the Plugin's settings >
814 + * Default Forms (Site Wide) setting.
815 + *
816 + * @since 2.3.3
817 + */
818 + public function output_global_non_inline_form() {
819 +
820 + // Get Settings, if they have not yet been loaded.
821 + if ( ! $this->settings ) {
822 + $this->settings = new ConvertKit_Settings();
823 + }
824 +
825 + // Bail if no non-inline form setting is specified.
826 + if ( ! $this->settings->has_non_inline_form() ) {
827 + return;
828 + }
829 +
830 + // Bail if the Page, Post or Custom Post Type's Form setting is set to 'None'
831 + // and the Plugin is set to honor this setting.
832 + if ( $this->post_settings !== false && $this->post_settings->uses_no_form() && $this->settings->non_inline_form_honor_none_setting() ) {
833 + return;
834 + }
835 +
836 + // Get form.
837 + $convertkit_forms = new ConvertKit_Resource_Forms();
838 +
839 + // Iterate through forms.
840 + foreach ( $this->settings->get_non_inline_form() as $form_id ) {
841 + // Get Form.
842 + $form = $convertkit_forms->get_by_id( (int) $form_id );
843 +
844 + // Bail if the Form doesn't exist (this shouldn't happen, but you never know).
845 + if ( ! $form ) {
846 + continue;
847 + }
848 +
849 + // Add the form to the scripts array so it is included in the output.
850 + add_filter(
851 + 'convertkit_output_scripts_footer',
852 + function ( $scripts ) use ( $form ) {
853 +
854 + $scripts[] = array(
855 + 'async' => true,
856 + 'data-uid' => $form['uid'],
857 + 'src' => $form['embed_js'],
858 + );
859 +
860 + return $scripts;
861 +
862 + }
863 + );
864 + }
865 +
866 + }
867 +
868 + /**
414 869 * Outputs any JS <script> tags registered with the convertkit_output_scripts_footer
415 870 * filter
416 871 *
417 872 * @since 2.1.4
@@ -439,10 +894,19 @@
439 894 $output_scripts = array();
440 895
441 896 // Iterate through scripts, building the <script> tag for each.
442 897 foreach ( $scripts as $script ) {
898 + /**
899 + * Filter the form <script> key/value pairs immediately before the script is output.
900 + *
901 + * @since 2.4.5
902 + *
903 + * @param array $script Form script key/value pairs to output as <script> tag.
904 + */
905 + $script = apply_filters( 'convertkit_output_script_footer', $script );
906 +
907 + // Build output.
443 908 $output = '<script';
444 -
445 909 foreach ( $script as $attribute => $value ) {
446 910 // If the value is true, just output the attribute.
447 911 if ( $value === true ) {
448 912 $output .= ' ' . esc_attr( $attribute );
@@ -448,11 +912,21 @@
448 912 $output .= ' ' . esc_attr( $attribute );
449 913 continue;
450 914 }
451 915
916 + // Sanitize attribute and value.
917 + $attribute = esc_attr( $attribute );
918 + $value = ( $attribute === 'src' ? esc_url( $value ) : esc_attr( $value ) );
919 +
452 920 // Output the attribute and value.
453 - $output .= ' ' . esc_attr( $attribute ) . '="' . esc_attr( $value ) . '"';
921 + $output .= ' ' . $attribute;
922 +
923 + // Output the value, if it's not a blank string.
924 + if ( strlen( $value ) > 0 ) {
925 + $output .= '="' . $value . '"';
926 + }
454 927 }
928 +
455 929 $output .= '></script>';
456 930
457 931 // Add to array.
458 932 $output_scripts[] = $output;