PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.2.3
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.2.3
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 +605 -47 2.3.23.2.3 View file →
@@ -66,18 +66,158 @@
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( 'rest_api_init', array( $this, 'register_routes' ) );
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 + * Register REST API routes.
85 + *
86 + * @since 3.1.7
87 + */
88 + public function register_routes() {
89 +
90 + // Register route to store the Kit subscriber's email's ID in a cookie.
91 + register_rest_route(
92 + 'kit/v1',
93 + '/subscriber/store-email-as-id-in-cookie',
94 + array(
95 + 'methods' => WP_REST_Server::CREATABLE,
96 + 'args' => array(
97 + // Email: Validate email is included in the request, a valid email address
98 + // and sanitize the email address.
99 + 'email' => array(
100 + 'required' => true,
101 + 'validate_callback' => function ( $param ) {
102 +
103 + return is_string( $param ) && is_email( $param );
104 +
105 + },
106 + 'sanitize_callback' => 'sanitize_email',
107 + ),
108 + ),
109 + 'callback' => function ( $request ) {
110 +
111 + // Get email address.
112 + $email = $request->get_param( 'email' );
113 +
114 + // Get subscriber ID.
115 + $subscriber = new ConvertKit_Subscriber();
116 + $subscriber_id = $subscriber->validate_and_store_subscriber_email( $email );
117 +
118 + // Bail if an error occured i.e. API hasn't been configured.
119 + if ( is_wp_error( $subscriber_id ) ) {
120 + return rest_ensure_response( $subscriber_id );
121 + }
122 +
123 + // Return the subscriber ID.
124 + return rest_ensure_response(
125 + array(
126 + 'id' => $subscriber_id,
127 + )
128 + );
129 +
130 + },
131 +
132 + // No authentication required, as this is on the frontend site.
133 + 'permission_callback' => '__return_true',
134 + )
135 + );
136 +
137 + }
138 +
139 + /**
140 + * Tags the subscriber, if:
141 + * - a subscriber ID exists in the cookie or URL,
142 + * - the WordPress Page has the "Add a Tag" setting specified
143 + *
144 + * @since 2.4.9.1
145 + */
146 + public function maybe_tag_subscriber() {
147 +
148 + // Bail if not a singular Post Type supported by ConvertKit.
149 + if ( ! is_singular( convertkit_get_supported_post_types() ) ) {
150 + return;
151 + }
152 +
153 + // Get Post ID.
154 + $post_id = get_the_ID();
155 +
156 + // Bail if a Post ID couldn't be identified.
157 + if ( ! $post_id ) {
158 + return;
159 + }
160 +
161 + // Get Settings, if they have not yet been loaded.
162 + if ( ! $this->settings ) {
163 + $this->settings = new ConvertKit_Settings();
164 + }
165 +
166 + // Bail if the API hasn't been configured.
167 + if ( ! $this->settings->has_access_and_refresh_token() ) {
168 + return;
169 + }
170 +
171 + // Get ConvertKit Post's Settings, if they have not yet been loaded.
172 + if ( ! $this->post_settings ) {
173 + $this->post_settings = new ConvertKit_Post( $post_id );
174 + }
175 +
176 + // Bail if no "Add a Tag" setting specified for this Page.
177 + if ( ! $this->post_settings->has_tag() ) {
178 + return;
179 + }
180 +
181 + // Get subscriber ID from URL or cookie.
182 + $this->get_subscriber_id_from_request();
183 +
184 + // Bail if no subscriber ID detected.
185 + if ( ! $this->subscriber_id ) {
186 + return;
187 + }
188 +
189 + // Initialize the API.
190 + $api = new ConvertKit_API_V4(
191 + CONVERTKIT_OAUTH_CLIENT_ID,
192 + CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI,
193 + $this->settings->get_access_token(),
194 + $this->settings->get_refresh_token(),
195 + $this->settings->debug_enabled(),
196 + 'output'
197 + );
198 +
199 + // If the subscriber ID is not numeric, it's a cryptographically-signed subscriber ID, set
200 + // when using the Member Content functionality by Kit's API.
201 + // Fetch the underlying subscriber ID for the tag_subscriber() method.
202 + if ( ! is_numeric( $this->subscriber_id ) ) {
203 + $result = $api->profile( $this->subscriber_id );
204 +
205 + // If an error occured, the subscriber ID is invalid.
206 + if ( is_wp_error( $result ) ) {
207 + return;
208 + }
209 +
210 + // Set the subscriber ID.
211 + $this->subscriber_id = $result['id'];
212 + }
213 +
214 + // Tag subscriber.
215 + $api->tag_subscriber( $this->post_settings->get_tag(), $this->subscriber_id );
216 +
217 + }
218 +
219 + /**
80 220 * Runs the `convertkit_output_output_form` action for singular Post Types that don't use the_content()
81 221 * or apply_filters( 'the_content' ) to output a ConvertKit Form.
82 222 *
83 223 * @since 1.9.6
@@ -159,8 +299,23 @@
159 299
160 300 // Replace the favicon with the WordPress site's favicon, if specified.
161 301 $landing_page = $this->landing_pages->replace_favicon( $landing_page );
162 302
303 + /**
304 + * Perform any actions immediately prior to outputting the Landing Page.
305 + *
306 + * Caching and minification Plugins may need to hook here to prevent
307 + * CSS / JS minification and lazy loading images, which can interfere
308 + * with Landing Pages.
309 + *
310 + * @since 2.4.4
311 + *
312 + * @param string $landing_page ConvertKit Landing Page HTML.
313 + * @param int $landing_page_id ConvertKit Landing Page ID.
314 + * @param int $post_id WordPress Page ID.
315 + */
316 + do_action( 'convertkit_output_landing_page_before', $landing_page, $landing_page_id, $post_id );
317 +
163 318 // Output Landing Page.
164 319 // Output is supplied from ConvertKit's API, which is already sanitized.
165 320 echo $landing_page; // phpcs:ignore WordPress.Security.EscapeOutput
166 321 exit;
@@ -167,9 +322,9 @@
167 322
168 323 }
169 324
170 325 /**
171 - * Appends a form to the singular Page, Post or Custom Post Type's Content.
326 + * Inserts a form to the singular Page, Post or Custom Post Type's Content.
172 327 *
173 328 * @param string $content Post Content.
174 329 * @return string Post Content with Form Appended, if applicable
175 330 */
@@ -174,15 +329,22 @@
174 329 * @return string Post Content with Form Appended, if applicable
175 330 */
176 331 public function append_form_to_content( $content ) {
177 332
178 - // Bail if not a singular Post Type.
179 - if ( ! is_singular() ) {
333 + // Bail if not a singular Post Type supported by ConvertKit.
334 + if ( ! is_singular( convertkit_get_supported_post_types() ) ) {
180 335 return $content;
181 336 }
182 337
183 - // Get Post ID and ConvertKit Form ID for the Post.
338 + // Get Post ID.
184 339 $post_id = get_the_ID();
340 +
341 + // Bail if Post Type is not supported by Kit.
342 + if ( ! in_array( get_post_type( $post_id ), convertkit_get_supported_post_types(), true ) ) {
343 + return $content;
344 + }
345 +
346 + // Get Form ID for the Post.
185 347 $form_id = $this->get_post_form_id( $post_id );
186 348
187 349 /**
188 350 * Define the ConvertKit Form ID to display for the given Post ID,
@@ -207,9 +369,9 @@
207 369 $this->forms = new ConvertKit_Resource_Forms( 'output_form' );
208 370 }
209 371
210 372 // Get Form HTML.
211 - $form = $this->forms->get_html( $form_id );
373 + $form = $this->forms->get_html( $form_id, $post_id );
212 374
213 375 // If an error occured, it could be because the specified Form ID for the Post either:
214 376 // - belongs to another ConvertKit account (i.e. API credentials were changed in the Plugin, but this Post's specified Form was not changed), or
215 377 // - the form was deleted from the ConvertKit account.
@@ -215,9 +377,9 @@
215 377 // - the form was deleted from the ConvertKit account.
216 378 // Attempt to fallback to the default form for this Post Type.
217 379 if ( is_wp_error( $form ) ) {
218 380 if ( $this->settings->debug_enabled() ) {
219 - $content .= '<!-- ConvertKit append_form_to_content(): ' . $form->get_error_message() . ' Attempting fallback to Default Form. -->';
381 + $content .= '<!-- Kit append_form_to_content(): ' . $form->get_error_message() . ' Attempting fallback to Default Form. -->';
220 382 }
221 383
222 384 // Get Default Form ID for this Post's Type.
223 385 $form_id = $this->settings->get_default_form( get_post_type( $post_id ) );
@@ -224,9 +386,9 @@
224 386
225 387 // If no Default Form is specified, just return the Post Content, unedited.
226 388 if ( ! $form_id ) {
227 389 if ( $this->settings->debug_enabled() ) {
228 - $content .= '<!-- ConvertKit append_form_to_content(): No Default Form exists as a fallback. -->';
390 + $content .= '<!-- Kit append_form_to_content(): No Default Form exists as a fallback. -->';
229 391 }
230 392
231 393 return $content;
232 394 }
@@ -231,15 +393,15 @@
231 393 return $content;
232 394 }
233 395
234 396 // Get Form HTML.
235 - $form = $this->forms->get_html( $form_id );
397 + $form = $this->forms->get_html( $form_id, $post_id );
236 398
237 399 // If an error occured again, the default form doesn't exist in this ConvertKit account.
238 400 // Just return the Post Content, unedited.
239 401 if ( is_wp_error( $form ) ) {
240 402 if ( $this->settings->debug_enabled() ) {
241 - $content .= '<!-- ConvertKit append_form_to_content(): Default Form: ' . $form->get_error_message() . ' -->';
403 + $content .= '<!-- Kit append_form_to_content(): Default Form: ' . $form->get_error_message() . ' -->';
242 404 }
243 405
244 406 return $content;
245 407 }
@@ -244,23 +406,70 @@
244 406 return $content;
245 407 }
246 408 }
247 409
410 + // If the Form HTML is empty, it's a modal form that has been set to load in the footer of the site.
411 + // We don't need to append anything to the content.
412 + if ( empty( $form ) ) {
413 + if ( $this->settings->debug_enabled() ) {
414 + $content .= '<!-- Kit append_form_to_content(): Form is non-inline, appended to footer. -->';
415 + }
416 +
417 + return $content;
418 + }
419 +
248 420 // If here, we have a ConvertKit Form.
249 - // Append form to Post's Content.
250 - $content = $content .= $form;
421 + // Append form to Post's Content, based on the position setting.
422 + $form_position = $this->settings->get_default_form_position( get_post_type( $post_id ) );
251 423
424 + if ( $this->settings->debug_enabled() ) {
425 + $content .= '<!-- Kit append_form_to_content(): Form Position: ' . esc_html( $form_position ) . ' -->';
426 + }
427 +
428 + switch ( $form_position ) {
429 + case 'before_after_content':
430 + $content = $form . $content . $form;
431 + break;
432 +
433 + case 'before_content':
434 + $content = $form . $content;
435 + break;
436 +
437 + case 'after_element':
438 + $element = $this->settings->get_default_form_position_element( get_post_type( $post_id ) );
439 + $index = $this->settings->get_default_form_position_element_index( get_post_type( $post_id ) );
440 +
441 + // Check if DOMDocument is installed.
442 + // It should be installed as mosts hosts include php-dom and php-xml modules.
443 + // If not, fallback to using preg_match_all(), which is less reliable.
444 + if ( ! class_exists( 'DOMDocument' ) ) {
445 + $content = $this->inject_form_after_element_fallback( $content, $element, $index, $form );
446 + break;
447 + }
448 +
449 + // Use DOMDocument.
450 + $content = $this->inject_form_after_element( $content, $element, $index, $form );
451 + break;
452 +
453 + case 'after_content':
454 + default:
455 + // Default behaviour < 2.5.8 was to append the Form after the content.
456 + $content .= $form;
457 + break;
458 + }
459 +
252 460 /**
253 461 * Filter the Post's Content, which includes a ConvertKit Form, immediately before it is output.
254 462 *
255 463 * @since 1.9.6
256 464 *
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
465 + * @param string $content Post Content
466 + * @param string $form ConvertKit Form HTML
467 + * @param int $post_id Post ID
468 + * @param int $form_id ConvertKit Form ID
469 + * @param string $form_position Form Position setting for the Post's Type.
261 470 */
262 - $content = apply_filters( 'convertkit_frontend_append_form', $content, $form, $post_id, $form_id );
471 + $content = apply_filters( 'convertkit_frontend_append_form', $content, $form, $post_id, $form_id, $form_position );
263 472
264 473 return $content;
265 474
266 475 }
@@ -265,8 +474,219 @@
265 474
266 475 }
267 476
268 477 /**
478 + * Injects the form after the given element and index, using DOMDocument.
479 + *
480 + * @since 2.6.2
481 + *
482 + * @param string $content Page / Post Content.
483 + * @param string $tag HTML tag to insert form after.
484 + * @param int $index Number of $tag elements to find before inserting form.
485 + * @param string $form Form HTML to inject.
486 + * @return string
487 + */
488 + private function inject_form_after_element( $content, $tag, $index, $form ) {
489 +
490 + // If the form is empty, don't inject anything.
491 + if ( empty( $form ) ) {
492 + return $content;
493 + }
494 +
495 + // Load the content into the parser.
496 + $parser = new ConvertKit_HTML_Parser( $content, LIBXML_HTML_NODEFDTD );
497 +
498 + // Find the element to append the form to.
499 + // item() is a zero based index.
500 + $element_node = $parser->html->getElementsByTagName( $tag )->item( $index - 1 );
501 +
502 + // If the element could not be found, either the number of elements by tag name is less
503 + // than the requested position the form be inserted in, or no element exists.
504 + // Append the form to the original content and return.
505 + if ( is_null( $element_node ) ) {
506 + return $content . $form;
507 + }
508 +
509 + // Load the form into the parser.
510 + $form_parser = new ConvertKit_HTML_Parser( $form, LIBXML_HTML_NODEFDTD );
511 + $form_body = $form_parser->html->getElementsByTagName( 'body' )->item( 0 );
512 +
513 + // Collect nodes first to avoid live NodeList mutation issues.
514 + $nodes_to_insert = array();
515 + foreach ( $form_body->childNodes as $child ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
516 + $nodes_to_insert[] = $parser->html->importNode( $child, true );
517 + }
518 +
519 + // Inject the form node(s) after the element node e.g. after the paragraph, heading etc.
520 + $next_sibling = $element_node->nextSibling; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
521 + foreach ( $nodes_to_insert as $node ) {
522 + $element_node->parentNode->insertBefore( $node, $element_node->nextSibling ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
523 + }
524 +
525 + // Return modified HTML string.
526 + return $parser->get_body_html();
527 +
528 + }
529 +
530 + /**
531 + * Injects the form after the given element and index, using preg_match_all().
532 + * This is less reliable than DOMDocument, and is called if DOMDocument is
533 + * not installed on the server.
534 + *
535 + * @since 2.6.2
536 + *
537 + * @param string $content Page / Post Content.
538 + * @param string $tag HTML tag to insert form after.
539 + * @param int $index Number of $tag elements to find before inserting form.
540 + * @param string $form Form HTML to inject.
541 + * @return string
542 + */
543 + private function inject_form_after_element_fallback( $content, $tag, $index, $form ) {
544 +
545 + // If the form is empty, don't inject anything.
546 + if ( empty( $form ) ) {
547 + return $content;
548 + }
549 +
550 + // Calculate tag length.
551 + $tag_length = ( strlen( $tag ) + 3 );
552 +
553 + // Find all closing elements.
554 + preg_match_all( '/<\/' . $tag . '>/', $content, $matches );
555 +
556 + // If no elements exist, just append the form.
557 + if ( count( $matches[0] ) === 0 ) {
558 + $content = $content . $form;
559 + return $content;
560 + }
561 +
562 + // If the number of elements is less than the index, we don't have enough elements to add the form to.
563 + // Just add the form after the content.
564 + if ( count( $matches[0] ) <= $index ) {
565 + $content = $content . $form;
566 + return $content;
567 + }
568 +
569 + // Iterate through the content to find the element at the configured index e.g. find the 4th closing paragraph.
570 + $offset = 0;
571 + foreach ( $matches[0] as $element_index => $element ) {
572 + $position = strpos( $content, $element, $offset );
573 + if ( ( $element_index + 1 ) === $index ) {
574 + return substr( $content, 0, $position + 4 ) . $form . substr( $content, $position + 4 );
575 + }
576 +
577 + // Increment offset.
578 + $offset = $position + 1;
579 + }
580 +
581 + // If here, something went wrong.
582 + // Just add the form after the content.
583 + $content = $content . $form;
584 + return $content;
585 +
586 + }
587 +
588 + /**
589 + * Registers the ConvertKit Form block to before or after the Query Loop block, when viewing a Category archive.
590 + *
591 + * See append_form_block_on_category_archive() configures the block to display the applicable category's Form.
592 + *
593 + * @since 2.4.9.1
594 + *
595 + * @param array $hooked_blocks The list of hooked block types.
596 + * @param string $position The relative position of the hooked blocks.
597 + * @param string $anchor_block The anchor block type.
598 + * @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.
599 + * @return array
600 + */
601 + public function maybe_register_form_block_on_category_archive( $hooked_blocks, $position, $anchor_block, $context ) {
602 +
603 + // Don't append if we're not viewing a category archive.
604 + if ( ! is_category() ) {
605 + return $hooked_blocks;
606 + }
607 +
608 + if ( $context instanceof WP_Block_Template && $context->slug !== 'archive' ) {
609 + return $hooked_blocks;
610 + }
611 +
612 + // Don't append if the anchor block isn't the Query Loop block.
613 + if ( $anchor_block !== 'core/query' ) {
614 + return $hooked_blocks;
615 + }
616 +
617 + // Don't append if the Category's form position setting is not defined.
618 + $form_position = $this->get_term_form_position();
619 + if ( ! $form_position ) {
620 + // Unhook this function as we don't need to check again in this request, as we'll
621 + // never output a form on the Category archive.
622 + remove_filter( 'hooked_block_types', array( $this, 'maybe_register_form_block_on_category_archive' ), 10 );
623 +
624 + return $hooked_blocks;
625 + }
626 +
627 + // Don't append if the position doesn't match.
628 + if ( $form_position !== $position ) {
629 + return $hooked_blocks;
630 + }
631 +
632 + // Hook the ConvertKit Form block.
633 + $hooked_blocks[] = 'convertkit/form';
634 +
635 + // Unhook this function as we don't need to check again in this request, as
636 + // we have now appended the form.
637 + remove_filter( 'hooked_block_types', array( $this, 'maybe_register_form_block_on_category_archive' ), 10 );
638 +
639 + return $hooked_blocks;
640 +
641 + }
642 +
643 + /**
644 + * Configures the ConvertKit Form block that was hooked below the Query Loop block by maybe_register_form_block_on_category_archive,
645 + * defining the Form ID based on the current Category's Form ID.
646 + *
647 + * @since 2.4.9.1
648 + *
649 + * @param array $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block.
650 + * @return null|array
651 + */
652 + public function append_form_block_to_category_archive( $parsed_hooked_block ) {
653 +
654 + // Sanity check that we're still viewing a Category archive.
655 + if ( ! is_category() ) {
656 + // Returning null will unregister the Form block from displaying.
657 + return null;
658 + }
659 +
660 + // Get Category archive being viewed.
661 + $category = get_category( get_query_var( 'cat' ) );
662 +
663 + // Bail if the Category could be found.
664 + if ( is_wp_error( $category ) || is_null( $category ) ) {
665 + // Returning null will unregister the Form block from displaying.
666 + return null;
667 + }
668 +
669 + // Load Term Settings.
670 + $term_settings = new ConvertKit_Term( $category->term_id );
671 +
672 + // Bail if no Form specified for the Category.
673 + if ( ! $term_settings->has_form() ) {
674 + // Returning null will unregister the Form block from displaying.
675 + return null;
676 + }
677 +
678 + // Define the form block attributes to display the given Form ID.
679 + $parsed_hooked_block['attrs'] = array(
680 + 'id' => absint( $term_settings->get_form() ),
681 + );
682 +
683 + // Return the Form block with its attributes.
684 + return $parsed_hooked_block;
685 +
686 + }
687 +
688 + /**
269 689 * Returns the Post, Category or Plugin ConvertKit Form ID for the given Post.
270 690 *
271 691 * If the Post specifies a form to use, returns that Form ID.
272 692 * If the Post uses the 'Default' setting, and an assigned Category has a Form ID, uses the Category's Form ID.
@@ -332,16 +752,53 @@
332 752 // If a Form ID exists, return it now.
333 753 if ( $term_settings->has_form() ) {
334 754 return $term_settings->get_form();
335 755 }
756 +
757 + // If the Term specifies that no Form should be used, return false.
758 + if ( $term_settings->uses_no_form() ) {
759 + return false;
760 + }
336 761 }
337 762
338 - // If here, use the Plugin's Default Form.
763 + // If here, all Terms were set to display the Default Form.
764 + // Therefore use the Plugin's Default Form.
339 765 return $this->settings->get_default_form( get_post_type( $post_id ) );
340 766
341 767 }
342 768
343 769 /**
770 + * Returns the Form Position setting for the currently viewed Category.
771 + *
772 + * @since 2.4.9.1
773 + *
774 + * @return bool|string
775 + */
776 + private function get_term_form_position() {
777 +
778 + // Get Category archive being viewed.
779 + $category = get_category( get_query_var( 'cat' ) );
780 +
781 + // Bail if the Category could be found.
782 + if ( is_wp_error( $category ) || is_null( $category ) ) {
783 + return false;
784 + }
785 +
786 + // Load Term Settings.
787 + $term_settings = new ConvertKit_Term( $category->term_id );
788 +
789 + // Return false if no form position is defined i.e. we don't want to display
790 + // it on the Category archive.
791 + if ( ! $term_settings->has_form_position() ) {
792 + return false;
793 + }
794 +
795 + // Return form position.
796 + return $term_settings->get_form_position();
797 +
798 + }
799 +
800 + /**
344 801 * Enqueue scripts.
345 802 *
346 803 * @since 1.9.6
347 804 */
@@ -346,49 +803,31 @@
346 803 * @since 1.9.6
347 804 */
348 805 public function enqueue_scripts() {
349 806
350 - // Get Post.
351 - $post = get_post();
807 + // Get ConvertKit Settings and Post's Settings.
808 + $settings = new ConvertKit_Settings();
352 809
353 - // Bail if no Post could be fetched.
354 - if ( ! $post ) {
810 + // Bail if the no scripts setting is enabled.
811 + if ( $settings->scripts_disabled() ) {
355 812 return;
356 813 }
357 814
358 - // Get ConvertKit Settings and Post's Settings.
359 - $settings = new ConvertKit_Settings();
360 - $convertkit_post = new ConvertKit_Post( $post->ID );
815 + // Enqueue frontend JS.
816 + convertkit_enqueue_frontend_js();
361 817
362 - // Register scripts that we might use.
363 - wp_register_script(
364 - 'convertkit-js',
365 - CONVERTKIT_PLUGIN_URL . 'resources/frontend/js/convertkit.js',
366 - array( 'jquery' ),
367 - CONVERTKIT_PLUGIN_VERSION,
368 - true
369 - );
818 + // Define variables.
370 819 wp_localize_script(
371 820 'convertkit-js',
372 821 'convertkit',
373 822 array(
374 - 'ajaxurl' => admin_url( 'admin-ajax.php' ),
823 + 'ajaxurl' => rest_url( 'kit/v1/subscriber/store-email-as-id-in-cookie' ),
375 824 'debug' => $settings->debug_enabled(),
376 - 'nonce' => wp_create_nonce( 'convertkit' ),
825 + 'nonce' => wp_create_nonce( 'wp_rest' ),
377 826 'subscriber_id' => $this->subscriber_id,
378 - 'tag' => ( ( is_singular() && $convertkit_post->has_tag() ) ? $convertkit_post->get_tag() : false ),
379 - 'post_id' => $post->ID,
380 827 )
381 828 );
382 829
383 - // Bail if the no scripts setting is enabled.
384 - if ( $settings->scripts_disabled() ) {
385 - return;
386 - }
387 -
388 - // Enqueue.
389 - wp_enqueue_script( 'convertkit-js' );
390 -
391 830 }
392 831
393 832 /**
394 833 * Gets the subscriber ID from the request (either the cookie or the URL).
@@ -410,8 +849,68 @@
410 849
411 850 }
412 851
413 852 /**
853 + * Outputs a non-inline forms if defined in the Plugin's settings >
854 + * Default Forms (Site Wide) setting.
855 + *
856 + * @since 2.3.3
857 + */
858 + public function output_global_non_inline_form() {
859 +
860 + // Get Settings, if they have not yet been loaded.
861 + if ( ! $this->settings ) {
862 + $this->settings = new ConvertKit_Settings();
863 + }
864 +
865 + // Bail if no non-inline form setting is specified.
866 + if ( ! $this->settings->has_non_inline_form() ) {
867 + return;
868 + }
869 +
870 + // Bail if the Page, Post or Custom Post Type's Form setting is set to 'None'
871 + // and the Plugin is set to honor this setting.
872 + if ( $this->post_settings !== false && $this->post_settings->uses_no_form() && $this->settings->non_inline_form_honor_none_setting() ) {
873 + return;
874 + }
875 +
876 + // Determine if the Non-inline Form Limit per Session setting is enabled.
877 + $limit_per_session = $this->settings->non_inline_form_limit_per_session();
878 +
879 + // Get form.
880 + $convertkit_forms = new ConvertKit_Resource_Forms();
881 +
882 + // Iterate through forms.
883 + foreach ( $this->settings->get_non_inline_form() as $form_id ) {
884 + // Get Form.
885 + $form = $convertkit_forms->get_by_id( (int) $form_id );
886 +
887 + // Bail if the Form doesn't exist (this shouldn't happen, but you never know).
888 + if ( ! $form ) {
889 + continue;
890 + }
891 +
892 + // Add the form to the scripts array so it is included in the output.
893 + add_filter(
894 + 'convertkit_output_scripts_footer',
895 + function ( $scripts ) use ( $form, $limit_per_session ) {
896 +
897 + $scripts[] = array(
898 + 'async' => true,
899 + 'data-uid' => $form['uid'],
900 + 'src' => $form['embed_js'],
901 + 'data-kit-limit-per-session' => $limit_per_session ? '1' : '0',
902 + );
903 +
904 + return $scripts;
905 +
906 + }
907 + );
908 + }
909 +
910 + }
911 +
912 + /**
414 913 * Outputs any JS <script> tags registered with the convertkit_output_scripts_footer
415 914 * filter
416 915 *
417 916 * @since 2.1.4
@@ -417,8 +916,13 @@
417 916 * @since 2.1.4
418 917 */
419 918 public function output_scripts_footer() {
420 919
920 + // Don't output scripts if the request is for a search page or 404.
921 + if ( is_search() || is_404() ) {
922 + return;
923 + }
924 +
421 925 // Define array of scripts.
422 926 $scripts = array();
423 927
424 928 /**
@@ -439,10 +943,24 @@
439 943 $output_scripts = array();
440 944
441 945 // Iterate through scripts, building the <script> tag for each.
442 946 foreach ( $scripts as $script ) {
947 + /**
948 + * Filter the form <script> key/value pairs immediately before the script is output.
949 + *
950 + * @since 2.4.5
951 + *
952 + * @param array $script Form script key/value pairs to output as <script> tag.
953 + */
954 + $script = apply_filters( 'convertkit_output_script_footer', $script );
955 +
956 + // Skip script if it is limited by the Non-inline Form Limit per Session setting.
957 + if ( $this->is_script_output_limited_by_session( $script ) ) {
958 + continue;
959 + }
960 +
961 + // Build output.
443 962 $output = '<script';
444 -
445 963 foreach ( $script as $attribute => $value ) {
446 964 // If the value is true, just output the attribute.
447 965 if ( $value === true ) {
448 966 $output .= ' ' . esc_attr( $attribute );
@@ -448,11 +966,21 @@
448 966 $output .= ' ' . esc_attr( $attribute );
449 967 continue;
450 968 }
451 969
970 + // Sanitize attribute and value.
971 + $attribute = esc_attr( $attribute );
972 + $value = ( $attribute === 'src' ? esc_url( $value ) : esc_attr( $value ) );
973 +
452 974 // Output the attribute and value.
453 - $output .= ' ' . esc_attr( $attribute ) . '="' . esc_attr( $value ) . '"';
975 + $output .= ' ' . $attribute;
976 +
977 + // Output the value, if it's not a blank string.
978 + if ( strlen( $value ) > 0 ) {
979 + $output .= '="' . $value . '"';
980 + }
454 981 }
982 +
455 983 $output .= '></script>';
456 984
457 985 // Add to array.
458 986 $output_scripts[] = $output;
@@ -467,8 +995,38 @@
467 995 // Output scripts.
468 996 foreach ( $output_scripts as $output_script ) {
469 997 echo $output_script . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
470 998 }
999 +
1000 + }
1001 +
1002 + /**
1003 + * Checks if a script is limited by the Non-inline Form Limit per Session setting.
1004 + *
1005 + * @since 3.0.0
1006 + *
1007 + * @param array $script Script.
1008 + * @return bool
1009 + */
1010 + private function is_script_output_limited_by_session( $script ) {
1011 +
1012 + // Get Settings, if they have not yet been loaded.
1013 + if ( ! $this->settings ) {
1014 + $this->settings = new ConvertKit_Settings();
1015 + }
1016 +
1017 + // Display script if the "Display Limit" setting isn't enabled.
1018 + if ( ! $this->settings->non_inline_form_limit_per_session() ) {
1019 + return false;
1020 + }
1021 +
1022 + // Display script if the "Display Limit" setting should not be applied to this script.
1023 + if ( ! isset( $script['data-kit-limit-per-session'] ) ) {
1024 + return false;
1025 + }
1026 +
1027 + // Display script if this is the first time the visitor has seen any non-inline form.
1028 + return isset( $_COOKIE['ck_non_inline_form_displayed'] );
471 1029
472 1030 }
473 1031
474 1032 }