PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.1.0
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.1.0
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 +536 -36 2.2.33.1.0 View file →
@@ -66,18 +66,99 @@
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 + // If the subscriber ID is not numeric, it's a cryptographically-signed subscriber ID, set
141 + // when using the Member Content functionality by Kit's API.
142 + // Fetch the underlying subscriber ID for the tag_subscriber() method.
143 + if ( ! is_numeric( $this->subscriber_id ) ) {
144 + $result = $api->profile( $this->subscriber_id );
145 +
146 + // If an error occured, the subscriber ID is invalid.
147 + if ( is_wp_error( $result ) ) {
148 + return;
149 + }
150 +
151 + // Set the subscriber ID.
152 + $this->subscriber_id = $result['id'];
153 + }
154 +
155 + // Tag subscriber.
156 + $api->tag_subscriber( $this->post_settings->get_tag(), $this->subscriber_id );
157 +
158 + }
159 +
160 + /**
80 161 * Runs the `convertkit_output_output_form` action for singular Post Types that don't use the_content()
81 162 * or apply_filters( 'the_content' ) to output a ConvertKit Form.
82 163 *
83 164 * @since 1.9.6
@@ -156,8 +237,26 @@
156 237 if ( is_wp_error( $landing_page ) ) {
157 238 return;
158 239 }
159 240
241 + // Replace the favicon with the WordPress site's favicon, if specified.
242 + $landing_page = $this->landing_pages->replace_favicon( $landing_page );
243 +
244 + /**
245 + * Perform any actions immediately prior to outputting the Landing Page.
246 + *
247 + * Caching and minification Plugins may need to hook here to prevent
248 + * CSS / JS minification and lazy loading images, which can interfere
249 + * with Landing Pages.
250 + *
251 + * @since 2.4.4
252 + *
253 + * @param string $landing_page ConvertKit Landing Page HTML.
254 + * @param int $landing_page_id ConvertKit Landing Page ID.
255 + * @param int $post_id WordPress Page ID.
256 + */
257 + do_action( 'convertkit_output_landing_page_before', $landing_page, $landing_page_id, $post_id );
258 +
160 259 // Output Landing Page.
161 260 // Output is supplied from ConvertKit's API, which is already sanitized.
162 261 echo $landing_page; // phpcs:ignore WordPress.Security.EscapeOutput
163 262 exit;
@@ -164,9 +263,9 @@
164 263
165 264 }
166 265
167 266 /**
168 - * Appends a form to the singular Page, Post or Custom Post Type's Content.
267 + * Inserts a form to the singular Page, Post or Custom Post Type's Content.
169 268 *
170 269 * @param string $content Post Content.
171 270 * @return string Post Content with Form Appended, if applicable
172 271 */
@@ -171,15 +270,22 @@
171 270 * @return string Post Content with Form Appended, if applicable
172 271 */
173 272 public function append_form_to_content( $content ) {
174 273
175 - // Bail if not a singular Post Type.
176 - if ( ! is_singular() ) {
274 + // Bail if not a singular Post Type supported by ConvertKit.
275 + if ( ! is_singular( convertkit_get_supported_post_types() ) ) {
177 276 return $content;
178 277 }
179 278
180 - // Get Post ID and ConvertKit Form ID for the Post.
279 + // Get Post ID.
181 280 $post_id = get_the_ID();
281 +
282 + // Bail if Post Type is not supported by Kit.
283 + if ( ! in_array( get_post_type( $post_id ), convertkit_get_supported_post_types(), true ) ) {
284 + return $content;
285 + }
286 +
287 + // Get Form ID for the Post.
182 288 $form_id = $this->get_post_form_id( $post_id );
183 289
184 290 /**
185 291 * Define the ConvertKit Form ID to display for the given Post ID,
@@ -204,9 +310,9 @@
204 310 $this->forms = new ConvertKit_Resource_Forms( 'output_form' );
205 311 }
206 312
207 313 // Get Form HTML.
208 - $form = $this->forms->get_html( $form_id );
314 + $form = $this->forms->get_html( $form_id, $post_id );
209 315
210 316 // If an error occured, it could be because the specified Form ID for the Post either:
211 317 // - belongs to another ConvertKit account (i.e. API credentials were changed in the Plugin, but this Post's specified Form was not changed), or
212 318 // - the form was deleted from the ConvertKit account.
@@ -212,9 +318,9 @@
212 318 // - the form was deleted from the ConvertKit account.
213 319 // Attempt to fallback to the default form for this Post Type.
214 320 if ( is_wp_error( $form ) ) {
215 321 if ( $this->settings->debug_enabled() ) {
216 - $content .= '<!-- ConvertKit append_form_to_content(): ' . $form->get_error_message() . ' Attempting fallback to Default Form. -->';
322 + $content .= '<!-- Kit append_form_to_content(): ' . $form->get_error_message() . ' Attempting fallback to Default Form. -->';
217 323 }
218 324
219 325 // Get Default Form ID for this Post's Type.
220 326 $form_id = $this->settings->get_default_form( get_post_type( $post_id ) );
@@ -221,9 +327,9 @@
221 327
222 328 // If no Default Form is specified, just return the Post Content, unedited.
223 329 if ( ! $form_id ) {
224 330 if ( $this->settings->debug_enabled() ) {
225 - $content .= '<!-- ConvertKit append_form_to_content(): No Default Form exists as a fallback. -->';
331 + $content .= '<!-- Kit append_form_to_content(): No Default Form exists as a fallback. -->';
226 332 }
227 333
228 334 return $content;
229 335 }
@@ -228,15 +334,15 @@
228 334 return $content;
229 335 }
230 336
231 337 // Get Form HTML.
232 - $form = $this->forms->get_html( $form_id );
338 + $form = $this->forms->get_html( $form_id, $post_id );
233 339
234 340 // If an error occured again, the default form doesn't exist in this ConvertKit account.
235 341 // Just return the Post Content, unedited.
236 342 if ( is_wp_error( $form ) ) {
237 343 if ( $this->settings->debug_enabled() ) {
238 - $content .= '<!-- ConvertKit append_form_to_content(): Default Form: ' . $form->get_error_message() . ' -->';
344 + $content .= '<!-- Kit append_form_to_content(): Default Form: ' . $form->get_error_message() . ' -->';
239 345 }
240 346
241 347 return $content;
242 348 }
@@ -241,23 +347,70 @@
241 347 return $content;
242 348 }
243 349 }
244 350
351 + // If the Form HTML is empty, it's a modal form that has been set to load in the footer of the site.
352 + // We don't need to append anything to the content.
353 + if ( empty( $form ) ) {
354 + if ( $this->settings->debug_enabled() ) {
355 + $content .= '<!-- Kit append_form_to_content(): Form is non-inline, appended to footer. -->';
356 + }
357 +
358 + return $content;
359 + }
360 +
245 361 // If here, we have a ConvertKit Form.
246 - // Append form to Post's Content.
247 - $content = $content .= $form;
362 + // Append form to Post's Content, based on the position setting.
363 + $form_position = $this->settings->get_default_form_position( get_post_type( $post_id ) );
248 364
365 + if ( $this->settings->debug_enabled() ) {
366 + $content .= '<!-- Kit append_form_to_content(): Form Position: ' . esc_html( $form_position ) . ' -->';
367 + }
368 +
369 + switch ( $form_position ) {
370 + case 'before_after_content':
371 + $content = $form . $content . $form;
372 + break;
373 +
374 + case 'before_content':
375 + $content = $form . $content;
376 + break;
377 +
378 + case 'after_element':
379 + $element = $this->settings->get_default_form_position_element( get_post_type( $post_id ) );
380 + $index = $this->settings->get_default_form_position_element_index( get_post_type( $post_id ) );
381 +
382 + // Check if DOMDocument is installed.
383 + // It should be installed as mosts hosts include php-dom and php-xml modules.
384 + // If not, fallback to using preg_match_all(), which is less reliable.
385 + if ( ! class_exists( 'DOMDocument' ) ) {
386 + $content = $this->inject_form_after_element_fallback( $content, $element, $index, $form );
387 + break;
388 + }
389 +
390 + // Use DOMDocument.
391 + $content = $this->inject_form_after_element( $content, $element, $index, $form );
392 + break;
393 +
394 + case 'after_content':
395 + default:
396 + // Default behaviour < 2.5.8 was to append the Form after the content.
397 + $content .= $form;
398 + break;
399 + }
400 +
249 401 /**
250 402 * Filter the Post's Content, which includes a ConvertKit Form, immediately before it is output.
251 403 *
252 404 * @since 1.9.6
253 405 *
254 - * @param string $content Post Content
255 - * @param string $form ConvertKit Form HTML
256 - * @param int $post_id Post ID
257 - * @param int $form_id ConvertKit Form ID
406 + * @param string $content Post Content
407 + * @param string $form ConvertKit Form HTML
408 + * @param int $post_id Post ID
409 + * @param int $form_id ConvertKit Form ID
410 + * @param string $form_position Form Position setting for the Post's Type.
258 411 */
259 - $content = apply_filters( 'convertkit_frontend_append_form', $content, $form, $post_id, $form_id );
412 + $content = apply_filters( 'convertkit_frontend_append_form', $content, $form, $post_id, $form_id, $form_position );
260 413
261 414 return $content;
262 415
263 416 }
@@ -262,8 +415,210 @@
262 415
263 416 }
264 417
265 418 /**
419 + * Injects the form after the given element and index, using DOMDocument.
420 + *
421 + * @since 2.6.2
422 + *
423 + * @param string $content Page / Post Content.
424 + * @param string $tag HTML tag to insert form after.
425 + * @param int $index Number of $tag elements to find before inserting form.
426 + * @param string $form Form HTML to inject.
427 + * @return string
428 + */
429 + private function inject_form_after_element( $content, $tag, $index, $form ) {
430 +
431 + // If the form is empty, don't inject anything.
432 + if ( empty( $form ) ) {
433 + return $content;
434 + }
435 +
436 + // Load the content into the parser.
437 + $parser = new ConvertKit_HTML_Parser( $content, LIBXML_HTML_NODEFDTD );
438 +
439 + // Find the element to append the form to.
440 + // item() is a zero based index.
441 + $element_node = $parser->html->getElementsByTagName( $tag )->item( $index - 1 );
442 +
443 + // If the element could not be found, either the number of elements by tag name is less
444 + // than the requested position the form be inserted in, or no element exists.
445 + // Append the form to the original content and return.
446 + if ( is_null( $element_node ) ) {
447 + return $content . $form;
448 + }
449 +
450 + // Create new element for the Form.
451 + $form_node = new DOMDocument();
452 + $form_node->loadHTML( $form, LIBXML_HTML_NODEFDTD );
453 +
454 + // Append the form to the specific element.
455 + $element_node->parentNode->insertBefore( $parser->html->importNode( $form_node->documentElement, true ), $element_node->nextSibling ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
456 +
457 + // Fetch HTML string.
458 + return $parser->get_body_html();
459 +
460 + }
461 +
462 + /**
463 + * Injects the form after the given element and index, using preg_match_all().
464 + * This is less reliable than DOMDocument, and is called if DOMDocument is
465 + * not installed on the server.
466 + *
467 + * @since 2.6.2
468 + *
469 + * @param string $content Page / Post Content.
470 + * @param string $tag HTML tag to insert form after.
471 + * @param int $index Number of $tag elements to find before inserting form.
472 + * @param string $form Form HTML to inject.
473 + * @return string
474 + */
475 + private function inject_form_after_element_fallback( $content, $tag, $index, $form ) {
476 +
477 + // If the form is empty, don't inject anything.
478 + if ( empty( $form ) ) {
479 + return $content;
480 + }
481 +
482 + // Calculate tag length.
483 + $tag_length = ( strlen( $tag ) + 3 );
484 +
485 + // Find all closing elements.
486 + preg_match_all( '/<\/' . $tag . '>/', $content, $matches );
487 +
488 + // If no elements exist, just append the form.
489 + if ( count( $matches[0] ) === 0 ) {
490 + $content = $content . $form;
491 + return $content;
492 + }
493 +
494 + // If the number of elements is less than the index, we don't have enough elements to add the form to.
495 + // Just add the form after the content.
496 + if ( count( $matches[0] ) <= $index ) {
497 + $content = $content . $form;
498 + return $content;
499 + }
500 +
501 + // Iterate through the content to find the element at the configured index e.g. find the 4th closing paragraph.
502 + $offset = 0;
503 + foreach ( $matches[0] as $element_index => $element ) {
504 + $position = strpos( $content, $element, $offset );
505 + if ( ( $element_index + 1 ) === $index ) {
506 + return substr( $content, 0, $position + 4 ) . $form . substr( $content, $position + 4 );
507 + }
508 +
509 + // Increment offset.
510 + $offset = $position + 1;
511 + }
512 +
513 + // If here, something went wrong.
514 + // Just add the form after the content.
515 + $content = $content . $form;
516 + return $content;
517 +
518 + }
519 +
520 + /**
521 + * Registers the ConvertKit Form block to before or after the Query Loop block, when viewing a Category archive.
522 + *
523 + * See append_form_block_on_category_archive() configures the block to display the applicable category's Form.
524 + *
525 + * @since 2.4.9.1
526 + *
527 + * @param array $hooked_blocks The list of hooked block types.
528 + * @param string $position The relative position of the hooked blocks.
529 + * @param string $anchor_block The anchor block type.
530 + * @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.
531 + * @return array
532 + */
533 + public function maybe_register_form_block_on_category_archive( $hooked_blocks, $position, $anchor_block, $context ) {
534 +
535 + // Don't append if we're not viewing a category archive.
536 + if ( ! is_category() ) {
537 + return $hooked_blocks;
538 + }
539 +
540 + if ( $context instanceof WP_Block_Template && $context->slug !== 'archive' ) {
541 + return $hooked_blocks;
542 + }
543 +
544 + // Don't append if the anchor block isn't the Query Loop block.
545 + if ( $anchor_block !== 'core/query' ) {
546 + return $hooked_blocks;
547 + }
548 +
549 + // Don't append if the Category's form position setting is not defined.
550 + $form_position = $this->get_term_form_position();
551 + if ( ! $form_position ) {
552 + // Unhook this function as we don't need to check again in this request, as we'll
553 + // never output a form on the Category archive.
554 + remove_filter( 'hooked_block_types', array( $this, 'maybe_register_form_block_on_category_archive' ), 10 );
555 +
556 + return $hooked_blocks;
557 + }
558 +
559 + // Don't append if the position doesn't match.
560 + if ( $form_position !== $position ) {
561 + return $hooked_blocks;
562 + }
563 +
564 + // Hook the ConvertKit Form block.
565 + $hooked_blocks[] = 'convertkit/form';
566 +
567 + // Unhook this function as we don't need to check again in this request, as
568 + // we have now appended the form.
569 + remove_filter( 'hooked_block_types', array( $this, 'maybe_register_form_block_on_category_archive' ), 10 );
570 +
571 + return $hooked_blocks;
572 +
573 + }
574 +
575 + /**
576 + * Configures the ConvertKit Form block that was hooked below the Query Loop block by maybe_register_form_block_on_category_archive,
577 + * defining the Form ID based on the current Category's Form ID.
578 + *
579 + * @since 2.4.9.1
580 + *
581 + * @param array $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block.
582 + * @return null|array
583 + */
584 + public function append_form_block_to_category_archive( $parsed_hooked_block ) {
585 +
586 + // Sanity check that we're still viewing a Category archive.
587 + if ( ! is_category() ) {
588 + // Returning null will unregister the Form block from displaying.
589 + return null;
590 + }
591 +
592 + // Get Category archive being viewed.
593 + $category = get_category( get_query_var( 'cat' ) );
594 +
595 + // Bail if the Category could be found.
596 + if ( is_wp_error( $category ) || is_null( $category ) ) {
597 + // Returning null will unregister the Form block from displaying.
598 + return null;
599 + }
600 +
601 + // Load Term Settings.
602 + $term_settings = new ConvertKit_Term( $category->term_id );
603 +
604 + // Bail if no Form specified for the Category.
605 + if ( ! $term_settings->has_form() ) {
606 + // Returning null will unregister the Form block from displaying.
607 + return null;
608 + }
609 +
610 + // Define the form block attributes to display the given Form ID.
611 + $parsed_hooked_block['attrs'] = array(
612 + 'id' => absint( $term_settings->get_form() ),
613 + );
614 +
615 + // Return the Form block with its attributes.
616 + return $parsed_hooked_block;
617 +
618 + }
619 +
620 + /**
266 621 * Returns the Post, Category or Plugin ConvertKit Form ID for the given Post.
267 622 *
268 623 * If the Post specifies a form to use, returns that Form ID.
269 624 * If the Post uses the 'Default' setting, and an assigned Category has a Form ID, uses the Category's Form ID.
@@ -329,16 +684,53 @@
329 684 // If a Form ID exists, return it now.
330 685 if ( $term_settings->has_form() ) {
331 686 return $term_settings->get_form();
332 687 }
688 +
689 + // If the Term specifies that no Form should be used, return false.
690 + if ( $term_settings->uses_no_form() ) {
691 + return false;
692 + }
333 693 }
334 694
335 - // If here, use the Plugin's Default Form.
695 + // If here, all Terms were set to display the Default Form.
696 + // Therefore use the Plugin's Default Form.
336 697 return $this->settings->get_default_form( get_post_type( $post_id ) );
337 698
338 699 }
339 700
340 701 /**
702 + * Returns the Form Position setting for the currently viewed Category.
703 + *
704 + * @since 2.4.9.1
705 + *
706 + * @return bool|string
707 + */
708 + private function get_term_form_position() {
709 +
710 + // Get Category archive being viewed.
711 + $category = get_category( get_query_var( 'cat' ) );
712 +
713 + // Bail if the Category could be found.
714 + if ( is_wp_error( $category ) || is_null( $category ) ) {
715 + return false;
716 + }
717 +
718 + // Load Term Settings.
719 + $term_settings = new ConvertKit_Term( $category->term_id );
720 +
721 + // Return false if no form position is defined i.e. we don't want to display
722 + // it on the Category archive.
723 + if ( ! $term_settings->has_form_position() ) {
724 + return false;
725 + }
726 +
727 + // Return form position.
728 + return $term_settings->get_form_position();
729 +
730 + }
731 +
732 + /**
341 733 * Enqueue scripts.
342 734 *
343 735 * @since 1.9.6
344 736 */
@@ -343,25 +735,21 @@
343 735 * @since 1.9.6
344 736 */
345 737 public function enqueue_scripts() {
346 738
347 - // Get Post.
348 - $post = get_post();
739 + // Get ConvertKit Settings and Post's Settings.
740 + $settings = new ConvertKit_Settings();
349 741
350 - // Bail if no Post could be fetched.
351 - if ( ! $post ) {
742 + // Bail if the no scripts setting is enabled.
743 + if ( $settings->scripts_disabled() ) {
352 744 return;
353 745 }
354 746
355 - // Get ConvertKit Settings and Post's Settings.
356 - $settings = new ConvertKit_Settings();
357 - $convertkit_post = new ConvertKit_Post( $post->ID );
358 -
359 747 // Register scripts that we might use.
360 748 wp_register_script(
361 749 'convertkit-js',
362 750 CONVERTKIT_PLUGIN_URL . 'resources/frontend/js/convertkit.js',
363 - array( 'jquery' ),
751 + array(),
364 752 CONVERTKIT_PLUGIN_VERSION,
365 753 true
366 754 );
367 755 wp_localize_script(
@@ -371,18 +759,11 @@
371 759 'ajaxurl' => admin_url( 'admin-ajax.php' ),
372 760 'debug' => $settings->debug_enabled(),
373 761 'nonce' => wp_create_nonce( 'convertkit' ),
374 762 'subscriber_id' => $this->subscriber_id,
375 - 'tag' => ( ( is_singular() && $convertkit_post->has_tag() ) ? $convertkit_post->get_tag() : false ),
376 - 'post_id' => $post->ID,
377 763 )
378 764 );
379 765
380 - // Bail if the no scripts setting is enabled.
381 - if ( $settings->scripts_disabled() ) {
382 - return;
383 - }
384 -
385 766 // Enqueue.
386 767 wp_enqueue_script( 'convertkit-js' );
387 768
388 769 }
@@ -407,8 +788,68 @@
407 788
408 789 }
409 790
410 791 /**
792 + * Outputs a non-inline forms if defined in the Plugin's settings >
793 + * Default Forms (Site Wide) setting.
794 + *
795 + * @since 2.3.3
796 + */
797 + public function output_global_non_inline_form() {
798 +
799 + // Get Settings, if they have not yet been loaded.
800 + if ( ! $this->settings ) {
801 + $this->settings = new ConvertKit_Settings();
802 + }
803 +
804 + // Bail if no non-inline form setting is specified.
805 + if ( ! $this->settings->has_non_inline_form() ) {
806 + return;
807 + }
808 +
809 + // Bail if the Page, Post or Custom Post Type's Form setting is set to 'None'
810 + // and the Plugin is set to honor this setting.
811 + if ( $this->post_settings !== false && $this->post_settings->uses_no_form() && $this->settings->non_inline_form_honor_none_setting() ) {
812 + return;
813 + }
814 +
815 + // Determine if the Non-inline Form Limit per Session setting is enabled.
816 + $limit_per_session = $this->settings->non_inline_form_limit_per_session();
817 +
818 + // Get form.
819 + $convertkit_forms = new ConvertKit_Resource_Forms();
820 +
821 + // Iterate through forms.
822 + foreach ( $this->settings->get_non_inline_form() as $form_id ) {
823 + // Get Form.
824 + $form = $convertkit_forms->get_by_id( (int) $form_id );
825 +
826 + // Bail if the Form doesn't exist (this shouldn't happen, but you never know).
827 + if ( ! $form ) {
828 + continue;
829 + }
830 +
831 + // Add the form to the scripts array so it is included in the output.
832 + add_filter(
833 + 'convertkit_output_scripts_footer',
834 + function ( $scripts ) use ( $form, $limit_per_session ) {
835 +
836 + $scripts[] = array(
837 + 'async' => true,
838 + 'data-uid' => $form['uid'],
839 + 'src' => $form['embed_js'],
840 + 'data-kit-limit-per-session' => $limit_per_session ? '1' : '0',
841 + );
842 +
843 + return $scripts;
844 +
845 + }
846 + );
847 + }
848 +
849 + }
850 +
851 + /**
411 852 * Outputs any JS <script> tags registered with the convertkit_output_scripts_footer
412 853 * filter
413 854 *
414 855 * @since 2.1.4
@@ -414,8 +855,13 @@
414 855 * @since 2.1.4
415 856 */
416 857 public function output_scripts_footer() {
417 858
859 + // Don't output scripts if the request is for a search page or 404.
860 + if ( is_search() || is_404() ) {
861 + return;
862 + }
863 +
418 864 // Define array of scripts.
419 865 $scripts = array();
420 866
421 867 /**
@@ -436,10 +882,24 @@
436 882 $output_scripts = array();
437 883
438 884 // Iterate through scripts, building the <script> tag for each.
439 885 foreach ( $scripts as $script ) {
886 + /**
887 + * Filter the form <script> key/value pairs immediately before the script is output.
888 + *
889 + * @since 2.4.5
890 + *
891 + * @param array $script Form script key/value pairs to output as <script> tag.
892 + */
893 + $script = apply_filters( 'convertkit_output_script_footer', $script );
894 +
895 + // Skip script if it is limited by the Non-inline Form Limit per Session setting.
896 + if ( $this->is_script_output_limited_by_session( $script ) ) {
897 + continue;
898 + }
899 +
900 + // Build output.
440 901 $output = '<script';
441 -
442 902 foreach ( $script as $attribute => $value ) {
443 903 // If the value is true, just output the attribute.
444 904 if ( $value === true ) {
445 905 $output .= ' ' . esc_attr( $attribute );
@@ -445,11 +905,21 @@
445 905 $output .= ' ' . esc_attr( $attribute );
446 906 continue;
447 907 }
448 908
909 + // Sanitize attribute and value.
910 + $attribute = esc_attr( $attribute );
911 + $value = ( $attribute === 'src' ? esc_url( $value ) : esc_attr( $value ) );
912 +
449 913 // Output the attribute and value.
450 - $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 + }
451 920 }
921 +
452 922 $output .= '></script>';
453 923
454 924 // Add to array.
455 925 $output_scripts[] = $output;
@@ -464,8 +934,38 @@
464 934 // Output scripts.
465 935 foreach ( $output_scripts as $output_script ) {
466 936 echo $output_script . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
467 937 }
938 +
939 + }
940 +
941 + /**
942 + * Checks if a script is limited by the Non-inline Form Limit per Session setting.
943 + *
944 + * @since 3.0.0
945 + *
946 + * @param array $script Script.
947 + * @return bool
948 + */
949 + private function is_script_output_limited_by_session( $script ) {
950 +
951 + // Get Settings, if they have not yet been loaded.
952 + if ( ! $this->settings ) {
953 + $this->settings = new ConvertKit_Settings();
954 + }
955 +
956 + // Display script if the "Display Limit" setting isn't enabled.
957 + if ( ! $this->settings->non_inline_form_limit_per_session() ) {
958 + return false;
959 + }
960 +
961 + // Display script if the "Display Limit" setting should not be applied to this script.
962 + if ( ! isset( $script['data-kit-limit-per-session'] ) ) {
963 + return false;
964 + }
965 +
966 + // Display script if this is the first time the visitor has seen any non-inline form.
967 + return isset( $_COOKIE['ck_non_inline_form_displayed'] );
468 968
469 969 }
470 970
471 971 }