PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 2.7.4
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v2.7.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 2.2.9 2.3.0 2.3.1 All 196 releases
← All changes | includes/class-convertkit-output.php +487 -20 2.3.22.7.4 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,10 +255,10 @@
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 264 // Get Post ID and ConvertKit Form ID for the Post.
@@ -215,9 +296,9 @@
215 296 // - the form was deleted from the ConvertKit account.
216 297 // Attempt to fallback to the default form for this Post Type.
217 298 if ( is_wp_error( $form ) ) {
218 299 if ( $this->settings->debug_enabled() ) {
219 - $content .= '<!-- ConvertKit append_form_to_content(): ' . $form->get_error_message() . ' Attempting fallback to Default Form. -->';
300 + $content .= '<!-- Kit append_form_to_content(): ' . $form->get_error_message() . ' Attempting fallback to Default Form. -->';
220 301 }
221 302
222 303 // Get Default Form ID for this Post's Type.
223 304 $form_id = $this->settings->get_default_form( get_post_type( $post_id ) );
@@ -224,9 +305,9 @@
224 305
225 306 // If no Default Form is specified, just return the Post Content, unedited.
226 307 if ( ! $form_id ) {
227 308 if ( $this->settings->debug_enabled() ) {
228 - $content .= '<!-- ConvertKit append_form_to_content(): No Default Form exists as a fallback. -->';
309 + $content .= '<!-- Kit append_form_to_content(): No Default Form exists as a fallback. -->';
229 310 }
230 311
231 312 return $content;
232 313 }
@@ -237,9 +318,9 @@
237 318 // If an error occured again, the default form doesn't exist in this ConvertKit account.
238 319 // Just return the Post Content, unedited.
239 320 if ( is_wp_error( $form ) ) {
240 321 if ( $this->settings->debug_enabled() ) {
241 - $content .= '<!-- ConvertKit append_form_to_content(): Default Form: ' . $form->get_error_message() . ' -->';
322 + $content .= '<!-- Kit append_form_to_content(): Default Form: ' . $form->get_error_message() . ' -->';
242 323 }
243 324
244 325 return $content;
245 326 }
@@ -244,23 +325,70 @@
244 325 return $content;
245 326 }
246 327 }
247 328
329 + // If the Form HTML is empty, it's a modal form that has been set to load in the footer of the site.
330 + // We don't need to append anything to the content.
331 + if ( empty( $form ) ) {
332 + if ( $this->settings->debug_enabled() ) {
333 + $content .= '<!-- Kit append_form_to_content(): Form is non-inline, appended to footer. -->';
334 + }
335 +
336 + return $content;
337 + }
338 +
248 339 // If here, we have a ConvertKit Form.
249 - // Append form to Post's Content.
250 - $content = $content .= $form;
340 + // Append form to Post's Content, based on the position setting.
341 + $form_position = $this->settings->get_default_form_position( get_post_type( $post_id ) );
251 342
343 + if ( $this->settings->debug_enabled() ) {
344 + $content .= '<!-- Kit append_form_to_content(): Form Position: ' . esc_html( $form_position ) . ' -->';
345 + }
346 +
347 + switch ( $form_position ) {
348 + case 'before_after_content':
349 + $content = $form . $content . $form;
350 + break;
351 +
352 + case 'before_content':
353 + $content = $form . $content;
354 + break;
355 +
356 + case 'after_element':
357 + $element = $this->settings->get_default_form_position_element( get_post_type( $post_id ) );
358 + $index = $this->settings->get_default_form_position_element_index( get_post_type( $post_id ) );
359 +
360 + // Check if DOMDocument is installed.
361 + // It should be installed as mosts hosts include php-dom and php-xml modules.
362 + // If not, fallback to using preg_match_all(), which is less reliable.
363 + if ( ! class_exists( 'DOMDocument' ) ) {
364 + $content = $this->inject_form_after_element_fallback( $content, $element, $index, $form );
365 + break;
366 + }
367 +
368 + // Use DOMDocument.
369 + $content = $this->inject_form_after_element( $content, $element, $index, $form );
370 + break;
371 +
372 + case 'after_content':
373 + default:
374 + // Default behaviour < 2.5.8 was to append the Form after the content.
375 + $content .= $form;
376 + break;
377 + }
378 +
252 379 /**
253 380 * Filter the Post's Content, which includes a ConvertKit Form, immediately before it is output.
254 381 *
255 382 * @since 1.9.6
256 383 *
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
384 + * @param string $content Post Content
385 + * @param string $form ConvertKit Form HTML
386 + * @param int $post_id Post ID
387 + * @param int $form_id ConvertKit Form ID
388 + * @param string $form_position Form Position setting for the Post's Type.
261 389 */
262 - $content = apply_filters( 'convertkit_frontend_append_form', $content, $form, $post_id, $form_id );
390 + $content = apply_filters( 'convertkit_frontend_append_form', $content, $form, $post_id, $form_id, $form_position );
263 391
264 392 return $content;
265 393
266 394 }
@@ -265,8 +393,237 @@
265 393
266 394 }
267 395
268 396 /**
397 + * Injects the form after the given element and index, using DOMDocument.
398 + *
399 + * @since 2.6.2
400 + *
401 + * @param string $content Page / Post Content.
402 + * @param string $tag HTML tag to insert form after.
403 + * @param int $index Number of $tag elements to find before inserting form.
404 + * @param string $form Form HTML to inject.
405 + * @return string
406 + */
407 + private function inject_form_after_element( $content, $tag, $index, $form ) {
408 +
409 + // If the form is empty, don't inject anything.
410 + if ( empty( $form ) ) {
411 + return $content;
412 + }
413 +
414 + // Define the meta tag.
415 + $meta_tag = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
416 +
417 + // Wrap content in <html>, <head> and <body> tags now, so we can inject the UTF-8 Content-Type meta tag.
418 + $modified_content = '<html><head></head><body>' . $content . '</body></html>';
419 +
420 + // Forcibly tell DOMDocument that this HTML uses the UTF-8 charset.
421 + // <meta charset="utf-8"> isn't enough, as DOMDocument still interprets the HTML as ISO-8859, which breaks character encoding
422 + // Use of mb_convert_encoding() with HTML-ENTITIES is deprecated in PHP 8.2, so we have to use this method.
423 + // If we don't, special characters render incorrectly.
424 + $modified_content = str_replace( '<head>', '<head>' . "\n" . $meta_tag, $modified_content );
425 +
426 + // Load Page / Post content into DOMDocument.
427 + libxml_use_internal_errors( true );
428 + $html = new DOMDocument();
429 + $html->loadHTML( $modified_content, LIBXML_HTML_NODEFDTD );
430 +
431 + // Find the element to append the form to.
432 + // item() is a zero based index.
433 + $element_node = $html->getElementsByTagName( $tag )->item( $index - 1 );
434 +
435 + // If the element could not be found, either the number of elements by tag name is less
436 + // than the requested position the form be inserted in, or no element exists.
437 + // Append the form to the original content and return.
438 + if ( is_null( $element_node ) ) {
439 + return $content . $form;
440 + }
441 +
442 + // Create new element for the Form.
443 + $form_node = new DOMDocument();
444 + $form_node->loadHTML( $form, LIBXML_HTML_NODEFDTD );
445 +
446 + // Append the form to the specific element.
447 + $element_node->parentNode->insertBefore( $html->importNode( $form_node->documentElement, true ), $element_node->nextSibling ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
448 +
449 + // Fetch HTML string.
450 + $modified_content = $html->saveHTML();
451 +
452 + // Remove some HTML tags that DOMDocument adds, returning the output.
453 + // We do this instead of using LIBXML_HTML_NOIMPLIED in loadHTML(), because Legacy Forms are not always contained in
454 + // a single root / outer element, which is required for LIBXML_HTML_NOIMPLIED to correctly work.
455 + $modified_content = str_replace( '<html>', '', $modified_content );
456 + $modified_content = str_replace( '</html>', '', $modified_content );
457 + $modified_content = str_replace( '<head>', '', $modified_content );
458 + $modified_content = str_replace( '</head>', '', $modified_content );
459 + $modified_content = str_replace( '<body>', '', $modified_content );
460 + $modified_content = str_replace( '</body>', '', $modified_content );
461 + $modified_content = str_replace( $meta_tag, '', $modified_content );
462 +
463 + return $modified_content;
464 +
465 + }
466 +
467 + /**
468 + * Injects the form after the given element and index, using preg_match_all().
469 + * This is less reliable than DOMDocument, and is called if DOMDocument is
470 + * not installed on the server.
471 + *
472 + * @since 2.6.2
473 + *
474 + * @param string $content Page / Post Content.
475 + * @param string $tag HTML tag to insert form after.
476 + * @param int $index Number of $tag elements to find before inserting form.
477 + * @param string $form Form HTML to inject.
478 + * @return string
479 + */
480 + private function inject_form_after_element_fallback( $content, $tag, $index, $form ) {
481 +
482 + // If the form is empty, don't inject anything.
483 + if ( empty( $form ) ) {
484 + return $content;
485 + }
486 +
487 + // Calculate tag length.
488 + $tag_length = ( strlen( $tag ) + 3 );
489 +
490 + // Find all closing elements.
491 + preg_match_all( '/<\/' . $tag . '>/', $content, $matches );
492 +
493 + // If no elements exist, just append the form.
494 + if ( count( $matches[0] ) === 0 ) {
495 + $content = $content . $form;
496 + return $content;
497 + }
498 +
499 + // If the number of elements is less than the index, we don't have enough elements to add the form to.
500 + // Just add the form after the content.
501 + if ( count( $matches[0] ) <= $index ) {
502 + $content = $content . $form;
503 + return $content;
504 + }
505 +
506 + // Iterate through the content to find the element at the configured index e.g. find the 4th closing paragraph.
507 + $offset = 0;
508 + foreach ( $matches[0] as $element_index => $element ) {
509 + $position = strpos( $content, $element, $offset );
510 + if ( ( $element_index + 1 ) === $index ) {
511 + return substr( $content, 0, $position + 4 ) . $form . substr( $content, $position + 4 );
512 + }
513 +
514 + // Increment offset.
515 + $offset = $position + 1;
516 + }
517 +
518 + // If here, something went wrong.
519 + // Just add the form after the content.
520 + $content = $content . $form;
521 + return $content;
522 +
523 + }
524 +
525 + /**
526 + * Registers the ConvertKit Form block to before or after the Query Loop block, when viewing a Category archive.
527 + *
528 + * See append_form_block_on_category_archive() configures the block to display the applicable category's Form.
529 + *
530 + * @since 2.4.9.1
531 + *
532 + * @param array $hooked_blocks The list of hooked block types.
533 + * @param string $position The relative position of the hooked blocks.
534 + * @param string $anchor_block The anchor block type.
535 + * @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.
536 + * @return array
537 + */
538 + public function maybe_register_form_block_on_category_archive( $hooked_blocks, $position, $anchor_block, $context ) {
539 +
540 + // Don't append if we're not viewing a category archive.
541 + if ( ! is_category() ) {
542 + return $hooked_blocks;
543 + }
544 +
545 + if ( $context instanceof WP_Block_Template && $context->slug !== 'archive' ) {
546 + return $hooked_blocks;
547 + }
548 +
549 + // Don't append if the anchor block isn't the Query Loop block.
550 + if ( $anchor_block !== 'core/query' ) {
551 + return $hooked_blocks;
552 + }
553 +
554 + // Don't append if the Category's form position setting is not defined.
555 + $form_position = $this->get_term_form_position();
556 + if ( ! $form_position ) {
557 + // Unhook this function as we don't need to check again in this request, as we'll
558 + // never output a form on the Category archive.
559 + remove_filter( 'hooked_block_types', array( $this, 'maybe_register_form_block_on_category_archive' ), 10 );
560 +
561 + return $hooked_blocks;
562 + }
563 +
564 + // Don't append if the position doesn't match.
565 + if ( $form_position !== $position ) {
566 + return $hooked_blocks;
567 + }
568 +
569 + // Hook the ConvertKit Form block.
570 + $hooked_blocks[] = 'convertkit/form';
571 +
572 + // Unhook this function as we don't need to check again in this request, as
573 + // we have now appended the form.
574 + remove_filter( 'hooked_block_types', array( $this, 'maybe_register_form_block_on_category_archive' ), 10 );
575 +
576 + return $hooked_blocks;
577 +
578 + }
579 +
580 + /**
581 + * Configures the ConvertKit Form block that was hooked below the Query Loop block by maybe_register_form_block_on_category_archive,
582 + * defining the Form ID based on the current Category's Form ID.
583 + *
584 + * @since 2.4.9.1
585 + *
586 + * @param array $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block.
587 + * @return null|array
588 + */
589 + public function append_form_block_to_category_archive( $parsed_hooked_block ) {
590 +
591 + // Sanity check that we're still viewing a Category archive.
592 + if ( ! is_category() ) {
593 + // Returning null will unregister the Form block from displaying.
594 + return null;
595 + }
596 +
597 + // Get Category archive being viewed.
598 + $category = get_category( get_query_var( 'cat' ) );
599 +
600 + // Bail if the Category could be found.
601 + if ( is_wp_error( $category ) || is_null( $category ) ) {
602 + // Returning null will unregister the Form block from displaying.
603 + return null;
604 + }
605 +
606 + // Load Term Settings.
607 + $term_settings = new ConvertKit_Term( $category->term_id );
608 +
609 + // Bail if no Form specified for the Category.
610 + if ( ! $term_settings->has_form() ) {
611 + // Returning null will unregister the Form block from displaying.
612 + return null;
613 + }
614 +
615 + // Define the form block attributes to display the given Form ID.
616 + $parsed_hooked_block['attrs'] = array(
617 + 'id' => absint( $term_settings->get_form() ),
618 + );
619 +
620 + // Return the Form block with its attributes.
621 + return $parsed_hooked_block;
622 +
623 + }
624 +
625 + /**
269 626 * Returns the Post, Category or Plugin ConvertKit Form ID for the given Post.
270 627 *
271 628 * If the Post specifies a form to use, returns that Form ID.
272 629 * If the Post uses the 'Default' setting, and an assigned Category has a Form ID, uses the Category's Form ID.
@@ -332,16 +689,53 @@
332 689 // If a Form ID exists, return it now.
333 690 if ( $term_settings->has_form() ) {
334 691 return $term_settings->get_form();
335 692 }
693 +
694 + // If the Term specifies that no Form should be used, return false.
695 + if ( $term_settings->uses_no_form() ) {
696 + return false;
697 + }
336 698 }
337 699
338 - // If here, use the Plugin's Default Form.
700 + // If here, all Terms were set to display the Default Form.
701 + // Therefore use the Plugin's Default Form.
339 702 return $this->settings->get_default_form( get_post_type( $post_id ) );
340 703
341 704 }
342 705
343 706 /**
707 + * Returns the Form Position setting for the currently viewed Category.
708 + *
709 + * @since 2.4.9.1
710 + *
711 + * @return bool|string
712 + */
713 + private function get_term_form_position() {
714 +
715 + // Get Category archive being viewed.
716 + $category = get_category( get_query_var( 'cat' ) );
717 +
718 + // Bail if the Category could be found.
719 + if ( is_wp_error( $category ) || is_null( $category ) ) {
720 + return false;
721 + }
722 +
723 + // Load Term Settings.
724 + $term_settings = new ConvertKit_Term( $category->term_id );
725 +
726 + // Return false if no form position is defined i.e. we don't want to display
727 + // it on the Category archive.
728 + if ( ! $term_settings->has_form_position() ) {
729 + return false;
730 + }
731 +
732 + // Return form position.
733 + return $term_settings->get_form_position();
734 +
735 + }
736 +
737 + /**
344 738 * Enqueue scripts.
345 739 *
346 740 * @since 1.9.6
347 741 */
@@ -362,9 +756,9 @@
362 756 // Register scripts that we might use.
363 757 wp_register_script(
364 758 'convertkit-js',
365 759 CONVERTKIT_PLUGIN_URL . 'resources/frontend/js/convertkit.js',
366 - array( 'jquery' ),
760 + array(),
367 761 CONVERTKIT_PLUGIN_VERSION,
368 762 true
369 763 );
370 764 wp_localize_script(
@@ -374,10 +768,8 @@
374 768 'ajaxurl' => admin_url( 'admin-ajax.php' ),
375 769 'debug' => $settings->debug_enabled(),
376 770 'nonce' => wp_create_nonce( 'convertkit' ),
377 771 'subscriber_id' => $this->subscriber_id,
378 - 'tag' => ( ( is_singular() && $convertkit_post->has_tag() ) ? $convertkit_post->get_tag() : false ),
379 - 'post_id' => $post->ID,
380 772 )
381 773 );
382 774
383 775 // Bail if the no scripts setting is enabled.
@@ -410,8 +802,64 @@
410 802
411 803 }
412 804
413 805 /**
806 + * Outputs a non-inline forms if defined in the Plugin's settings >
807 + * Default Forms (Site Wide) setting.
808 + *
809 + * @since 2.3.3
810 + */
811 + public function output_global_non_inline_form() {
812 +
813 + // Get Settings, if they have not yet been loaded.
814 + if ( ! $this->settings ) {
815 + $this->settings = new ConvertKit_Settings();
816 + }
817 +
818 + // Bail if no non-inline form setting is specified.
819 + if ( ! $this->settings->has_non_inline_form() ) {
820 + return;
821 + }
822 +
823 + // Bail if the Page, Post or Custom Post Type's Form setting is set to 'None'
824 + // and the Plugin is set to honor this setting.
825 + if ( $this->post_settings !== false && $this->post_settings->uses_no_form() && $this->settings->non_inline_form_honor_none_setting() ) {
826 + return;
827 + }
828 +
829 + // Get form.
830 + $convertkit_forms = new ConvertKit_Resource_Forms();
831 +
832 + // Iterate through forms.
833 + foreach ( $this->settings->get_non_inline_form() as $form_id ) {
834 + // Get Form.
835 + $form = $convertkit_forms->get_by_id( (int) $form_id );
836 +
837 + // Bail if the Form doesn't exist (this shouldn't happen, but you never know).
838 + if ( ! $form ) {
839 + continue;
840 + }
841 +
842 + // Add the form to the scripts array so it is included in the output.
843 + add_filter(
844 + 'convertkit_output_scripts_footer',
845 + function ( $scripts ) use ( $form ) {
846 +
847 + $scripts[] = array(
848 + 'async' => true,
849 + 'data-uid' => $form['uid'],
850 + 'src' => $form['embed_js'],
851 + );
852 +
853 + return $scripts;
854 +
855 + }
856 + );
857 + }
858 +
859 + }
860 +
861 + /**
414 862 * Outputs any JS <script> tags registered with the convertkit_output_scripts_footer
415 863 * filter
416 864 *
417 865 * @since 2.1.4
@@ -439,10 +887,19 @@
439 887 $output_scripts = array();
440 888
441 889 // Iterate through scripts, building the <script> tag for each.
442 890 foreach ( $scripts as $script ) {
891 + /**
892 + * Filter the form <script> key/value pairs immediately before the script is output.
893 + *
894 + * @since 2.4.5
895 + *
896 + * @param array $script Form script key/value pairs to output as <script> tag.
897 + */
898 + $script = apply_filters( 'convertkit_output_script_footer', $script );
899 +
900 + // Build output.
443 901 $output = '<script';
444 -
445 902 foreach ( $script as $attribute => $value ) {
446 903 // If the value is true, just output the attribute.
447 904 if ( $value === true ) {
448 905 $output .= ' ' . esc_attr( $attribute );
@@ -448,11 +905,21 @@
448 905 $output .= ' ' . esc_attr( $attribute );
449 906 continue;
450 907 }
451 908
909 + // Sanitize attribute and value.
910 + $attribute = esc_attr( $attribute );
911 + $value = ( $attribute === 'src' ? esc_url( $value ) : esc_attr( $value ) );
912 +
452 913 // Output the attribute and value.
453 - $output .= ' ' . esc_attr( $attribute ) . '="' . esc_attr( $value ) . '"';
914 + $output .= ' ' . $attribute;
915 +
916 + // Output the value, if it's not a blank string.
917 + if ( strlen( $value ) > 0 ) {
918 + $output .= '="' . $value . '"';
919 + }
454 920 }
921 +
455 922 $output .= '></script>';
456 923
457 924 // Add to array.
458 925 $output_scripts[] = $output;