PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / trunk
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages vtrunk
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 2.3.2 2.3.3 All 194 releases
convertkit / includes / class-convertkit-output.php

class-convertkit-output.php in Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages trunk, at includes/class-convertkit-output.php

974 lines 29.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ConvertKit Output class.
4 *
5 * @package ConvertKit
6 * @author ConvertKit
7 */
8
9 /**
10 * Outputs Forms and Landing Pages on the frontend web site, based on
11 * the Post and Plugin's configuration.
12 *
13 * @since 1.9.6
14 */
15 class ConvertKit_Output {
16
17 /**
18 * Holds the ConvertKit Subscriber ID.
19 *
20 * @since 2.0.6
21 *
22 * @var int|string
23 */
24 private $subscriber_id = 0;
25
26 /**
27 * Holds the ConvertKit Plugin Settings class
28 *
29 * @since 1.9.6
30 *
31 * @var bool|ConvertKit_Settings
32 */
33 private $settings = false;
34
35 /**
36 * Holds the ConvertKit Post Settings class
37 *
38 * @since 1.9.6
39 *
40 * @var bool|ConvertKit_Post
41 */
42 private $post_settings = false;
43
44 /**
45 * Holds the available ConvertKit Forms
46 *
47 * @since 1.9.6
48 *
49 * @var bool|ConvertKit_Resource_Forms
50 */
51 private $forms = false;
52
53 /**
54 * Holds the available ConvertKit Landing Pages
55 *
56 * @since 1.9.6
57 *
58 * @var bool|ConvertKit_Resource_Landing_Pages
59 */
60 private $landing_pages = false;
61
62 /**
63 * Constructor. Registers actions and filters to output ConvertKit Forms and Landing Pages
64 * on the frontend web site.
65 *
66 * @since 1.9.6
67 */
68 public function __construct() {
69
70 add_action( 'wp', array( $this, 'maybe_tag_subscriber' ) );
71 add_action( 'template_redirect', array( $this, 'output_form' ) );
72 add_action( 'template_redirect', array( $this, 'page_takeover' ) );
73 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
74 add_filter( 'the_content', array( $this, 'append_form_to_content' ) );
75 add_filter( 'hooked_block_types', array( $this, 'maybe_register_form_block_on_category_archive' ), 10, 4 );
76 add_filter( 'hooked_block_convertkit/form', array( $this, 'append_form_block_to_category_archive' ), 10, 1 );
77 add_action( 'wp_footer', array( $this, 'output_global_non_inline_form' ), 1 );
78 add_action( 'wp_footer', array( $this, 'output_scripts_footer' ) );
79
80 }
81
82 /**
83 * Tags the subscriber, if:
84 * - a subscriber ID exists in the cookie or URL,
85 * - the WordPress Page has the "Add a Tag" setting specified
86 *
87 * @since 2.4.9.1
88 */
89 public function maybe_tag_subscriber() {
90
91 // Bail if not a singular Post Type supported by ConvertKit.
92 if ( ! is_singular( convertkit_get_supported_post_types() ) ) {
93 return;
94 }
95
96 // Get Post ID.
97 $post_id = get_the_ID();
98
99 // Bail if a Post ID couldn't be identified.
100 if ( ! $post_id ) {
101 return;
102 }
103
104 // Get Settings, if they have not yet been loaded.
105 if ( ! $this->settings ) {
106 $this->settings = new ConvertKit_Settings();
107 }
108
109 // Bail if the API hasn't been configured.
110 if ( ! $this->settings->has_access_and_refresh_token() ) {
111 return;
112 }
113
114 // Get ConvertKit Post's Settings, if they have not yet been loaded.
115 if ( ! $this->post_settings ) {
116 $this->post_settings = new ConvertKit_Post( $post_id );
117 }
118
119 // Bail if no "Add a Tag" setting specified for this Page.
120 if ( ! $this->post_settings->has_tag() ) {
121 return;
122 }
123
124 // Get subscriber ID from URL or cookie.
125 $this->get_subscriber_id_from_request();
126
127 // Bail if no subscriber ID detected.
128 if ( ! $this->subscriber_id ) {
129 return;
130 }
131
132 // Initialize the API.
133 $api = new ConvertKit_API_V4(
134 CONVERTKIT_OAUTH_CLIENT_ID,
135 CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI,
136 $this->settings->get_access_token(),
137 $this->settings->get_refresh_token(),
138 $this->settings->debug_enabled(),
139 'output'
140 );
141
142 // If the subscriber ID is not numeric, it's a cryptographically-signed subscriber ID, set
143 // when using the Member Content functionality by Kit's API.
144 // Fetch the underlying subscriber ID for the tag_subscriber() method.
145 if ( ! is_numeric( $this->subscriber_id ) ) {
146 $result = $api->profile( $this->subscriber_id );
147
148 // If an error occurred, the subscriber ID is invalid.
149 if ( is_wp_error( $result ) ) {
150 return;
151 }
152
153 // Set the subscriber ID.
154 $this->subscriber_id = $result['id'];
155 }
156
157 // Tag subscriber.
158 $api->tag_subscriber( $this->post_settings->get_tag(), $this->subscriber_id );
159
160 }
161
162 /**
163 * Runs the `convertkit_output_output_form` action for singular Post Types that don't use the_content()
164 * or apply_filters( 'the_content' ) to output a ConvertKit Form.
165 *
166 * @since 1.9.6
167 */
168 public function output_form() {
169
170 /**
171 * Outputs a ConvertKit Form on singular Post Types that don't use the_content()
172 * or apply_filters( 'the_content' ).
173 *
174 * @since 1.9.6
175 *
176 * @return string Post Content with Form Appended, if applicable
177 */
178 do_action( 'convertkit_output_output_form' );
179
180 }
181
182 /**
183 * Outputs a ConvertKit Landing Page if configured, replacing all output for the singular Post Type.
184 *
185 * @since 1.9.6
186 */
187 public function page_takeover() {
188
189 $queried_object = get_queried_object();
190
191 // Bail if the queried object cannot be inspected.
192 if ( ! isset( $queried_object->post_type ) ) {
193 return;
194 }
195
196 // Get Post ID.
197 $post_id = $queried_object->ID;
198
199 // Bail if the queried object isn't a supported Post Type for Landing Pages.
200 if ( $queried_object->post_type !== 'page' ) {
201 return;
202 }
203
204 // Get ConvertKit Post's Settings, if they have not yet been loaded.
205 if ( ! $this->post_settings ) {
206 $this->post_settings = new ConvertKit_Post( $post_id );
207 }
208
209 // Get Landing Page ID.
210 $landing_page_id = $this->post_settings->get_landing_page();
211
212 /**
213 * Define the ConvertKit Landing Page ID to display for the given Post ID,
214 * overriding the Post settings.
215 *
216 * Return false to not display any ConvertKit Landing Page.
217 *
218 * @since 1.9.6
219 *
220 * @param int $landing_page_id Landing Page ID
221 * @param int $post_id Post ID
222 */
223 $landing_page_id = apply_filters( 'convertkit_output_page_takeover_landing_page_id', $landing_page_id, $post_id );
224
225 // Bail if no Landing Page is configured to be output.
226 if ( empty( $landing_page_id ) ) {
227 return;
228 }
229
230 // Get available ConvertKit Landing Pages, if they have not yet been loaded.
231 if ( ! $this->landing_pages ) {
232 $this->landing_pages = new ConvertKit_Resource_Landing_Pages( 'output_landing_page' );
233 }
234
235 // Get Landing Page.
236 $landing_page = $this->landing_pages->get_html( $this->post_settings->get_landing_page() );
237
238 // Bail if an error occurred.
239 if ( is_wp_error( $landing_page ) ) {
240 return;
241 }
242
243 // Replace the favicon with the WordPress site's favicon, if specified.
244 $landing_page = $this->landing_pages->replace_favicon( $landing_page );
245
246 /**
247 * Perform any actions immediately prior to outputting the Landing Page.
248 *
249 * Caching and minification Plugins may need to hook here to prevent
250 * CSS / JS minification and lazy loading images, which can interfere
251 * with Landing Pages.
252 *
253 * @since 2.4.4
254 *
255 * @param string $landing_page ConvertKit Landing Page HTML.
256 * @param int $landing_page_id ConvertKit Landing Page ID.
257 * @param int $post_id WordPress Page ID.
258 */
259 do_action( 'convertkit_output_landing_page_before', $landing_page, $landing_page_id, $post_id );
260
261 // Output Landing Page.
262 // Output is supplied from ConvertKit's API, which is already sanitized.
263 echo $landing_page; // phpcs:ignore WordPress.Security.EscapeOutput
264 exit;
265
266 }
267
268 /**
269 * Inserts a form to the singular Page, Post or Custom Post Type's Content.
270 *
271 * @param string $content Post Content.
272 * @return string Post Content with Form Appended, if applicable
273 */
274 public function append_form_to_content( $content ) {
275
276 // Bail if not a singular Post Type supported by ConvertKit.
277 if ( ! is_singular( convertkit_get_supported_post_types() ) ) {
278 return $content;
279 }
280
281 // Get Post ID.
282 $post_id = get_the_ID();
283
284 // Bail if Post Type is not supported by Kit.
285 if ( ! in_array( get_post_type( $post_id ), convertkit_get_supported_post_types(), true ) ) {
286 return $content;
287 }
288
289 // Get Form ID for the Post.
290 $form_id = $this->get_post_form_id( $post_id );
291
292 /**
293 * Define the ConvertKit Form ID to display for the given Post ID,
294 * overriding the Post, Category or Plugin settings.
295 *
296 * Return false to not display any ConvertKit Form.
297 *
298 * @since 1.9.6
299 *
300 * @param bool|int $form_id Form ID
301 * @param int $post_id Post ID
302 */
303 $form_id = apply_filters( 'convertkit_output_append_form_to_content_form_id', $form_id, $post_id );
304
305 // Return the Post Content, unedited, if the Form ID is false or zero.
306 if ( ! $form_id ) {
307 return $content;
308 }
309
310 // Get available ConvertKit Forms, if they have not yet been loaded.
311 if ( ! $this->forms ) {
312 $this->forms = new ConvertKit_Resource_Forms( 'output_form' );
313 }
314
315 // Get Form HTML.
316 $form = $this->forms->get_html( $form_id, $post_id );
317
318 // If an error occurred, it could be because the specified Form ID for the Post either:
319 // - belongs to another ConvertKit account (i.e. API credentials were changed in the Plugin, but this Post's specified Form was not changed), or
320 // - the form was deleted from the ConvertKit account.
321 // Attempt to fallback to the default form for this Post Type.
322 if ( is_wp_error( $form ) ) {
323 if ( $this->settings->debug_enabled() ) {
324 $content .= '<!-- Kit append_form_to_content(): ' . $form->get_error_message() . ' Attempting fallback to Default Form. -->';
325 }
326
327 // Get Default Form ID for this Post's Type.
328 $form_id = $this->settings->get_default_form( get_post_type( $post_id ) );
329
330 // If no Default Form is specified, just return the Post Content, unedited.
331 if ( ! $form_id ) {
332 if ( $this->settings->debug_enabled() ) {
333 $content .= '<!-- Kit append_form_to_content(): No Default Form exists as a fallback. -->';
334 }
335
336 return $content;
337 }
338
339 // Get Form HTML.
340 $form = $this->forms->get_html( $form_id, $post_id );
341
342 // If an error occurred again, the default form doesn't exist in this ConvertKit account.
343 // Just return the Post Content, unedited.
344 if ( is_wp_error( $form ) ) {
345 if ( $this->settings->debug_enabled() ) {
346 $content .= '<!-- Kit append_form_to_content(): Default Form: ' . $form->get_error_message() . ' -->';
347 }
348
349 return $content;
350 }
351 }
352
353 // If the Form HTML is empty, it's a modal form that has been set to load in the footer of the site.
354 // We don't need to append anything to the content.
355 if ( empty( $form ) ) {
356 if ( $this->settings->debug_enabled() ) {
357 $content .= '<!-- Kit append_form_to_content(): Form is non-inline, appended to footer. -->';
358 }
359
360 return $content;
361 }
362
363 // If here, we have a ConvertKit Form.
364 // Append form to Post's Content, based on the position setting.
365 $form_position = $this->settings->get_default_form_position( get_post_type( $post_id ) );
366
367 if ( $this->settings->debug_enabled() ) {
368 $content .= '<!-- Kit append_form_to_content(): Form Position: ' . esc_html( $form_position ) . ' -->';
369 }
370
371 switch ( $form_position ) {
372 case 'before_after_content':
373 $content = $form . $content . $form;
374 break;
375
376 case 'before_content':
377 $content = $form . $content;
378 break;
379
380 case 'after_element':
381 $element = $this->settings->get_default_form_position_element( get_post_type( $post_id ) );
382 $index = $this->settings->get_default_form_position_element_index( get_post_type( $post_id ) );
383
384 // Check if DOMDocument is installed.
385 // It should be installed as mosts hosts include php-dom and php-xml modules.
386 // If not, fallback to using preg_match_all(), which is less reliable.
387 if ( ! class_exists( 'DOMDocument' ) ) {
388 $content = $this->inject_form_after_element_fallback( $content, $element, $index, $form );
389 break;
390 }
391
392 // Use DOMDocument.
393 $content = $this->inject_form_after_element( $content, $element, $index, $form );
394 break;
395
396 case 'after_content':
397 default:
398 // Default behaviour < 2.5.8 was to append the Form after the content.
399 $content .= $form;
400 break;
401 }
402
403 /**
404 * Filter the Post's Content, which includes a ConvertKit Form, immediately before it is output.
405 *
406 * @since 1.9.6
407 *
408 * @param string $content Post Content
409 * @param string $form ConvertKit Form HTML
410 * @param int $post_id Post ID
411 * @param int $form_id ConvertKit Form ID
412 * @param string $form_position Form Position setting for the Post's Type.
413 */
414 $content = apply_filters( 'convertkit_frontend_append_form', $content, $form, $post_id, $form_id, $form_position );
415
416 return $content;
417
418 }
419
420 /**
421 * Injects the form after the given element and index, using DOMDocument.
422 *
423 * @since 2.6.2
424 *
425 * @param string $content Page / Post Content.
426 * @param string $tag HTML tag to insert form after.
427 * @param int $index Number of $tag elements to find before inserting form.
428 * @param string $form Form HTML to inject.
429 * @return string
430 */
431 private function inject_form_after_element( $content, $tag, $index, $form ) {
432
433 // If the form is empty, don't inject anything.
434 if ( empty( $form ) ) {
435 return $content;
436 }
437
438 // Load the content into the parser.
439 $parser = new ConvertKit_HTML_Parser( $content, LIBXML_HTML_NODEFDTD );
440
441 // Find the element to append the form to.
442 // item() is a zero based index.
443 $element_node = $parser->html->getElementsByTagName( $tag )->item( $index - 1 );
444
445 // If the element could not be found, either the number of elements by tag name is less
446 // than the requested position the form be inserted in, or no element exists.
447 // Append the form to the original content and return.
448 if ( is_null( $element_node ) ) {
449 return $content . $form;
450 }
451
452 // Load the form into the parser.
453 $form_parser = new ConvertKit_HTML_Parser( $form, LIBXML_HTML_NODEFDTD );
454 $form_body = $form_parser->html->getElementsByTagName( 'body' )->item( 0 );
455
456 // Collect nodes first to avoid live NodeList mutation issues.
457 $nodes_to_insert = array();
458 foreach ( $form_body->childNodes as $child ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
459 $nodes_to_insert[] = $parser->html->importNode( $child, true );
460 }
461
462 // Inject the form node(s) after the element node e.g. after the paragraph, heading etc.
463 $next_sibling = $element_node->nextSibling; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
464 foreach ( $nodes_to_insert as $node ) {
465 $element_node->parentNode->insertBefore( $node, $element_node->nextSibling ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
466 }
467
468 // Return modified HTML string.
469 return $parser->get_body_html();
470
471 }
472
473 /**
474 * Injects the form after the given element and index, using preg_match_all().
475 * This is less reliable than DOMDocument, and is called if DOMDocument is
476 * not installed on the server.
477 *
478 * @since 2.6.2
479 *
480 * @param string $content Page / Post Content.
481 * @param string $tag HTML tag to insert form after.
482 * @param int $index Number of $tag elements to find before inserting form.
483 * @param string $form Form HTML to inject.
484 * @return string
485 */
486 private function inject_form_after_element_fallback( $content, $tag, $index, $form ) {
487
488 // If the form is empty, don't inject anything.
489 if ( empty( $form ) ) {
490 return $content;
491 }
492
493 // Calculate tag length.
494 $tag_length = ( strlen( $tag ) + 3 );
495
496 // Find all closing elements.
497 preg_match_all( '/<\/' . $tag . '>/', $content, $matches );
498
499 // If no elements exist, just append the form.
500 if ( count( $matches[0] ) === 0 ) {
501 $content = $content . $form;
502 return $content;
503 }
504
505 // If the number of elements is less than the index, we don't have enough elements to add the form to.
506 // Just add the form after the content.
507 if ( count( $matches[0] ) <= $index ) {
508 $content = $content . $form;
509 return $content;
510 }
511
512 // Iterate through the content to find the element at the configured index e.g. find the 4th closing paragraph.
513 $offset = 0;
514 foreach ( $matches[0] as $element_index => $element ) {
515 $position = strpos( $content, $element, $offset );
516 if ( ( $element_index + 1 ) === $index ) {
517 return substr( $content, 0, $position + 4 ) . $form . substr( $content, $position + 4 );
518 }
519
520 // Increment offset.
521 $offset = $position + 1;
522 }
523
524 // If here, something went wrong.
525 // Just add the form after the content.
526 $content = $content . $form;
527 return $content;
528
529 }
530
531 /**
532 * Registers the ConvertKit Form block to before or after the Query Loop block, when viewing a Category archive.
533 *
534 * See append_form_block_on_category_archive() configures the block to display the applicable category's Form.
535 *
536 * @since 2.4.9.1
537 *
538 * @param array $hooked_blocks The list of hooked block types.
539 * @param string $position The relative position of the hooked blocks.
540 * @param string $anchor_block The anchor block type.
541 * @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.
542 * @return array
543 */
544 public function maybe_register_form_block_on_category_archive( $hooked_blocks, $position, $anchor_block, $context ) {
545
546 // Don't append if we're not viewing a category archive.
547 if ( ! is_category() ) {
548 return $hooked_blocks;
549 }
550
551 if ( $context instanceof WP_Block_Template && $context->slug !== 'archive' ) {
552 return $hooked_blocks;
553 }
554
555 // Don't append if the anchor block isn't the Query Loop block.
556 if ( $anchor_block !== 'core/query' ) {
557 return $hooked_blocks;
558 }
559
560 // Don't append if the Category's form position setting is not defined.
561 $form_position = $this->get_term_form_position();
562 if ( ! $form_position ) {
563 // Unhook this function as we don't need to check again in this request, as we'll
564 // never output a form on the Category archive.
565 remove_filter( 'hooked_block_types', array( $this, 'maybe_register_form_block_on_category_archive' ), 10 );
566
567 return $hooked_blocks;
568 }
569
570 // Don't append if the position doesn't match.
571 if ( $form_position !== $position ) {
572 return $hooked_blocks;
573 }
574
575 // Hook the ConvertKit Form block.
576 $hooked_blocks[] = 'convertkit/form';
577
578 // Unhook this function as we don't need to check again in this request, as
579 // we have now appended the form.
580 remove_filter( 'hooked_block_types', array( $this, 'maybe_register_form_block_on_category_archive' ), 10 );
581
582 return $hooked_blocks;
583
584 }
585
586 /**
587 * Configures the ConvertKit Form block that was hooked below the Query Loop block by maybe_register_form_block_on_category_archive,
588 * defining the Form ID based on the current Category's Form ID.
589 *
590 * @since 2.4.9.1
591 *
592 * @param array $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block.
593 * @return null|array
594 */
595 public function append_form_block_to_category_archive( $parsed_hooked_block ) {
596
597 // Sanity check that we're still viewing a Category archive.
598 if ( ! is_category() ) {
599 // Returning null will unregister the Form block from displaying.
600 return null;
601 }
602
603 // Get Category archive being viewed.
604 $category = get_category( get_query_var( 'cat' ) );
605
606 // Bail if the Category could be found.
607 if ( is_wp_error( $category ) || is_null( $category ) ) {
608 // Returning null will unregister the Form block from displaying.
609 return null;
610 }
611
612 // Load Term Settings.
613 $term_settings = new ConvertKit_Term( $category->term_id );
614
615 // Bail if no Form specified for the Category.
616 if ( ! $term_settings->has_form() ) {
617 // Returning null will unregister the Form block from displaying.
618 return null;
619 }
620
621 // Define the form block attributes to display the given Form ID.
622 $parsed_hooked_block['attrs'] = array(
623 'id' => absint( $term_settings->get_form() ),
624 );
625
626 // Return the Form block with its attributes.
627 return $parsed_hooked_block;
628
629 }
630
631 /**
632 * Returns the Post, Category or Plugin ConvertKit Form ID for the given Post.
633 *
634 * If the Post specifies a form to use, returns that Form ID.
635 * If the Post uses the 'Default' setting, and an assigned Category has a Form ID, uses the Category's Form ID.
636 * Otherwise falls back to the Plugin's Default Form ID (if any).
637 *
638 * @since 1.9.6
639 *
640 * @param int $post_id Post ID.
641 * @return bool|string|int false|'default'|Form ID
642 */
643 private function get_post_form_id( $post_id ) {
644
645 // Get Settings, if they have not yet been loaded.
646 if ( ! $this->settings ) {
647 $this->settings = new ConvertKit_Settings();
648 }
649
650 // Get ConvertKit Post's Settings, if they have not yet been loaded.
651 if ( ! $this->post_settings ) {
652 $this->post_settings = new ConvertKit_Post( $post_id );
653 }
654
655 // If the Post specifies a Form to use, return its ID now.
656 if ( $this->post_settings->has_form() ) {
657 return $this->post_settings->get_form();
658 }
659
660 // If the Post specifies that no Form should be used, return false.
661 if ( $this->post_settings->uses_no_form() ) {
662 return false;
663 }
664
665 // Sanity check that the Post uses the Default Form setting, which should be the case
666 // because the above conditions were not met.
667 if ( ! $this->post_settings->uses_default_form() ) {
668 return false;
669 }
670
671 // Get Post's Categories.
672 $categories = wp_get_post_categories(
673 $post_id,
674 array(
675 'fields' => 'ids',
676 )
677 );
678
679 // If no Categories exist, use the Default Form.
680 if ( ! is_array( $categories ) || ! count( $categories ) ) {
681 // Get Post Type.
682 return $this->settings->get_default_form( get_post_type( $post_id ) );
683 }
684
685 /**
686 * Iterate through Categories in reverse order.
687 * This honors the behaviour < 1.9.6, which states that if multiple Categories each have a Form.
688 * assigned, the last Category with a Form in the wp_get_post_categories() call will be used.
689 */
690 $categories = array_reverse( $categories );
691 foreach ( $categories as $term_id ) {
692 // Load Term Settings.
693 $term_settings = new ConvertKit_Term( $term_id );
694
695 // If a Form ID exists, return it now.
696 if ( $term_settings->has_form() ) {
697 return $term_settings->get_form();
698 }
699
700 // If the Term specifies that no Form should be used, return false.
701 if ( $term_settings->uses_no_form() ) {
702 return false;
703 }
704 }
705
706 // If here, all Terms were set to display the Default Form.
707 // Therefore use the Plugin's Default Form.
708 return $this->settings->get_default_form( get_post_type( $post_id ) );
709
710 }
711
712 /**
713 * Returns the Form Position setting for the currently viewed Category.
714 *
715 * @since 2.4.9.1
716 *
717 * @return bool|string
718 */
719 private function get_term_form_position() {
720
721 // Get Category archive being viewed.
722 $category = get_category( get_query_var( 'cat' ) );
723
724 // Bail if the Category could be found.
725 if ( is_wp_error( $category ) || is_null( $category ) ) {
726 return false;
727 }
728
729 // Load Term Settings.
730 $term_settings = new ConvertKit_Term( $category->term_id );
731
732 // Return false if no form position is defined i.e. we don't want to display
733 // it on the Category archive.
734 if ( ! $term_settings->has_form_position() ) {
735 return false;
736 }
737
738 // Return form position.
739 return $term_settings->get_form_position();
740
741 }
742
743 /**
744 * Enqueue scripts.
745 *
746 * @since 1.9.6
747 */
748 public function enqueue_scripts() {
749
750 // Get ConvertKit Settings and Post's Settings.
751 $settings = new ConvertKit_Settings();
752
753 // Bail if the no scripts setting is enabled.
754 if ( $settings->scripts_disabled() ) {
755 return;
756 }
757
758 // Enqueue frontend JS.
759 convertkit_enqueue_frontend_js();
760
761 // Define variables.
762 wp_localize_script(
763 'convertkit-js',
764 'convertkit',
765 array(
766 'debug' => $settings->debug_enabled(),
767 'subscriber_id' => $this->subscriber_id,
768 )
769 );
770
771 }
772
773 /**
774 * Gets the subscriber ID from the request (either the cookie or the URL).
775 *
776 * @since 1.9.6
777 */
778 public function get_subscriber_id_from_request() {
779
780 // Use ConvertKit_Subscriber class to fetch and validate the subscriber ID.
781 $subscriber = new ConvertKit_Subscriber();
782 $subscriber_id = $subscriber->get_subscriber_id();
783
784 // If an error occurred, the subscriber ID in the request/cookie is not a valid subscriber.
785 if ( is_wp_error( $subscriber_id ) ) {
786 return;
787 }
788
789 $this->subscriber_id = $subscriber_id;
790
791 }
792
793 /**
794 * Outputs a non-inline forms if defined in the Plugin's settings >
795 * Default Forms (Site Wide) setting.
796 *
797 * @since 2.3.3
798 */
799 public function output_global_non_inline_form() {
800
801 // Get Settings, if they have not yet been loaded.
802 if ( ! $this->settings ) {
803 $this->settings = new ConvertKit_Settings();
804 }
805
806 // Bail if no non-inline form setting is specified.
807 if ( ! $this->settings->has_non_inline_form() ) {
808 return;
809 }
810
811 // Bail if the Page, Post or Custom Post Type's Form setting is set to 'None'
812 // and the Plugin is set to honor this setting.
813 if ( $this->post_settings !== false && $this->post_settings->uses_no_form() && $this->settings->non_inline_form_honor_none_setting() ) {
814 return;
815 }
816
817 // Determine if the Non-inline Form Limit per Session setting is enabled.
818 $limit_per_session = $this->settings->non_inline_form_limit_per_session();
819
820 // Get form.
821 $convertkit_forms = new ConvertKit_Resource_Forms();
822
823 // Iterate through forms.
824 foreach ( $this->settings->get_non_inline_form() as $form_id ) {
825 // Get Form.
826 $form = $convertkit_forms->get_by_id( (int) $form_id );
827
828 // Bail if the Form doesn't exist (this shouldn't happen, but you never know).
829 if ( ! $form ) {
830 continue;
831 }
832
833 // Add the form to the scripts array so it is included in the output.
834 add_filter(
835 'convertkit_output_scripts_footer',
836 function ( $scripts ) use ( $form, $limit_per_session ) {
837
838 $scripts[] = array(
839 'async' => true,
840 'data-uid' => $form['uid'],
841 'src' => $form['embed_js'],
842 'data-kit-limit-per-session' => $limit_per_session ? '1' : '0',
843 );
844
845 return $scripts;
846
847 }
848 );
849 }
850
851 }
852
853 /**
854 * Outputs any JS <script> tags registered with the convertkit_output_scripts_footer
855 * filter
856 *
857 * @since 2.1.4
858 */
859 public function output_scripts_footer() {
860
861 // Don't output scripts if the request is for a search page or 404.
862 if ( is_search() || is_404() ) {
863 return;
864 }
865
866 // Define array of scripts.
867 $scripts = array();
868
869 /**
870 * Define an array of scripts to output in the footer of the WordPress site.
871 *
872 * @since 2.1.4
873 *
874 * @param array $scripts Scripts.
875 */
876 $scripts = apply_filters( 'convertkit_output_scripts_footer', $scripts );
877
878 // Bail if no scripts exist.
879 if ( ! count( $scripts ) ) {
880 return;
881 }
882
883 // Define array to store <script> outputs.
884 $output_scripts = array();
885
886 // Iterate through scripts, building the <script> tag for each.
887 foreach ( $scripts as $script ) {
888 /**
889 * Filter the form <script> key/value pairs immediately before the script is output.
890 *
891 * @since 2.4.5
892 *
893 * @param array $script Form script key/value pairs to output as <script> tag.
894 */
895 $script = apply_filters( 'convertkit_output_script_footer', $script );
896
897 // Skip script if it is limited by the Non-inline Form Limit per Session setting.
898 if ( $this->is_script_output_limited_by_session( $script ) ) {
899 continue;
900 }
901
902 // Build output.
903 $output = '<script';
904 foreach ( $script as $attribute => $value ) {
905 // If the value is true, just output the attribute.
906 if ( $value === true ) {
907 $output .= ' ' . esc_attr( $attribute );
908 continue;
909 }
910
911 // Sanitize attribute and value.
912 $attribute = esc_attr( $attribute );
913 $value = ( $attribute === 'src' ? esc_url( $value ) : esc_attr( $value ) );
914
915 // Output the attribute and value.
916 $output .= ' ' . $attribute;
917
918 // Output the value, if it's not a blank string.
919 if ( strlen( $value ) > 0 ) {
920 $output .= '="' . $value . '"';
921 }
922 }
923
924 $output .= '></script>';
925
926 // Add to array.
927 $output_scripts[] = $output;
928 }
929
930 // Remove duplicate scripts.
931 // This prevents the same non-inline form displaying twice. For example, if a modal form is specified both
932 // in the Page's settings and the Form block, the user would see the same modal form displayed twice
933 // because the script would be output twice.
934 $output_scripts = array_unique( $output_scripts );
935
936 // Output scripts.
937 foreach ( $output_scripts as $output_script ) {
938 echo $output_script . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
939 }
940
941 }
942
943 /**
944 * Checks if a script is limited by the Non-inline Form Limit per Session setting.
945 *
946 * @since 3.0.0
947 *
948 * @param array $script Script.
949 * @return bool
950 */
951 private function is_script_output_limited_by_session( $script ) {
952
953 // Get Settings, if they have not yet been loaded.
954 if ( ! $this->settings ) {
955 $this->settings = new ConvertKit_Settings();
956 }
957
958 // Display script if the "Display Limit" setting isn't enabled.
959 if ( ! $this->settings->non_inline_form_limit_per_session() ) {
960 return false;
961 }
962
963 // Display script if the "Display Limit" setting should not be applied to this script.
964 if ( ! isset( $script['data-kit-limit-per-session'] ) ) {
965 return false;
966 }
967
968 // Display script if this is the first time the visitor has seen any non-inline form.
969 return isset( $_COOKIE['ck_non_inline_form_displayed'] );
970
971 }
972
973 }
974