PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.0.2
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.0.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 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 3.0.2, at includes/class-convertkit-output.php

969 lines 28.5 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( 'init', array( $this, 'get_subscriber_id_from_request' ) );
71 add_action( 'wp', array( $this, 'maybe_tag_subscriber' ) );
72 add_action( 'template_redirect', array( $this, 'output_form' ) );
73 add_action( 'template_redirect', array( $this, 'page_takeover' ) );
74 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
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 );
79 add_action( 'wp_footer', array( $this, 'output_scripts_footer' ) );
80
81 }
82
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 /**
161 * Runs the `convertkit_output_output_form` action for singular Post Types that don't use the_content()
162 * or apply_filters( 'the_content' ) to output a ConvertKit Form.
163 *
164 * @since 1.9.6
165 */
166 public function output_form() {
167
168 /**
169 * Outputs a ConvertKit Form on singular Post Types that don't use the_content()
170 * or apply_filters( 'the_content' ).
171 *
172 * @since 1.9.6
173 *
174 * @return string Post Content with Form Appended, if applicable
175 */
176 do_action( 'convertkit_output_output_form' );
177
178 }
179
180 /**
181 * Outputs a ConvertKit Landing Page if configured, replacing all output for the singular Post Type.
182 *
183 * @since 1.9.6
184 */
185 public function page_takeover() {
186
187 $queried_object = get_queried_object();
188
189 // Bail if the queried object cannot be inspected.
190 if ( ! isset( $queried_object->post_type ) ) {
191 return;
192 }
193
194 // Get Post ID.
195 $post_id = $queried_object->ID;
196
197 // Bail if the queried object isn't a supported Post Type for Landing Pages.
198 if ( $queried_object->post_type !== 'page' ) {
199 return;
200 }
201
202 // Get ConvertKit Post's Settings, if they have not yet been loaded.
203 if ( ! $this->post_settings ) {
204 $this->post_settings = new ConvertKit_Post( $post_id );
205 }
206
207 // Get Landing Page ID.
208 $landing_page_id = $this->post_settings->get_landing_page();
209
210 /**
211 * Define the ConvertKit Landing Page ID to display for the given Post ID,
212 * overriding the Post settings.
213 *
214 * Return false to not display any ConvertKit Landing Page.
215 *
216 * @since 1.9.6
217 *
218 * @param int $landing_page_id Landing Page ID
219 * @param int $post_id Post ID
220 */
221 $landing_page_id = apply_filters( 'convertkit_output_page_takeover_landing_page_id', $landing_page_id, $post_id );
222
223 // Bail if no Landing Page is configured to be output.
224 if ( empty( $landing_page_id ) ) {
225 return;
226 }
227
228 // Get available ConvertKit Landing Pages, if they have not yet been loaded.
229 if ( ! $this->landing_pages ) {
230 $this->landing_pages = new ConvertKit_Resource_Landing_Pages( 'output_landing_page' );
231 }
232
233 // Get Landing Page.
234 $landing_page = $this->landing_pages->get_html( $this->post_settings->get_landing_page() );
235
236 // Bail if an error occured.
237 if ( is_wp_error( $landing_page ) ) {
238 return;
239 }
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
259 // Output Landing Page.
260 // Output is supplied from ConvertKit's API, which is already sanitized.
261 echo $landing_page; // phpcs:ignore WordPress.Security.EscapeOutput
262 exit;
263
264 }
265
266 /**
267 * Inserts a form to the singular Page, Post or Custom Post Type's Content.
268 *
269 * @param string $content Post Content.
270 * @return string Post Content with Form Appended, if applicable
271 */
272 public function append_form_to_content( $content ) {
273
274 // Bail if not a singular Post Type supported by ConvertKit.
275 if ( ! is_singular( convertkit_get_supported_post_types() ) ) {
276 return $content;
277 }
278
279 // Get Post ID.
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.
288 $form_id = $this->get_post_form_id( $post_id );
289
290 /**
291 * Define the ConvertKit Form ID to display for the given Post ID,
292 * overriding the Post, Category or Plugin settings.
293 *
294 * Return false to not display any ConvertKit Form.
295 *
296 * @since 1.9.6
297 *
298 * @param bool|int $form_id Form ID
299 * @param int $post_id Post ID
300 */
301 $form_id = apply_filters( 'convertkit_output_append_form_to_content_form_id', $form_id, $post_id );
302
303 // Return the Post Content, unedited, if the Form ID is false or zero.
304 if ( ! $form_id ) {
305 return $content;
306 }
307
308 // Get available ConvertKit Forms, if they have not yet been loaded.
309 if ( ! $this->forms ) {
310 $this->forms = new ConvertKit_Resource_Forms( 'output_form' );
311 }
312
313 // Get Form HTML.
314 $form = $this->forms->get_html( $form_id, $post_id );
315
316 // If an error occured, it could be because the specified Form ID for the Post either:
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
318 // - the form was deleted from the ConvertKit account.
319 // Attempt to fallback to the default form for this Post Type.
320 if ( is_wp_error( $form ) ) {
321 if ( $this->settings->debug_enabled() ) {
322 $content .= '<!-- Kit append_form_to_content(): ' . $form->get_error_message() . ' Attempting fallback to Default Form. -->';
323 }
324
325 // Get Default Form ID for this Post's Type.
326 $form_id = $this->settings->get_default_form( get_post_type( $post_id ) );
327
328 // If no Default Form is specified, just return the Post Content, unedited.
329 if ( ! $form_id ) {
330 if ( $this->settings->debug_enabled() ) {
331 $content .= '<!-- Kit append_form_to_content(): No Default Form exists as a fallback. -->';
332 }
333
334 return $content;
335 }
336
337 // Get Form HTML.
338 $form = $this->forms->get_html( $form_id, $post_id );
339
340 // If an error occured again, the default form doesn't exist in this ConvertKit account.
341 // Just return the Post Content, unedited.
342 if ( is_wp_error( $form ) ) {
343 if ( $this->settings->debug_enabled() ) {
344 $content .= '<!-- Kit append_form_to_content(): Default Form: ' . $form->get_error_message() . ' -->';
345 }
346
347 return $content;
348 }
349 }
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
361 // If here, we have a ConvertKit 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 ) );
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
401 /**
402 * Filter the Post's Content, which includes a ConvertKit Form, immediately before it is output.
403 *
404 * @since 1.9.6
405 *
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.
411 */
412 $content = apply_filters( 'convertkit_frontend_append_form', $content, $form, $post_id, $form_id, $form_position );
413
414 return $content;
415
416 }
417
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 /**
621 * Returns the Post, Category or Plugin ConvertKit Form ID for the given Post.
622 *
623 * If the Post specifies a form to use, returns that Form ID.
624 * If the Post uses the 'Default' setting, and an assigned Category has a Form ID, uses the Category's Form ID.
625 * Otherwise falls back to the Plugin's Default Form ID (if any).
626 *
627 * @since 1.9.6
628 *
629 * @param int $post_id Post ID.
630 * @return bool|string|int false|'default'|Form ID
631 */
632 private function get_post_form_id( $post_id ) {
633
634 // Get Settings, if they have not yet been loaded.
635 if ( ! $this->settings ) {
636 $this->settings = new ConvertKit_Settings();
637 }
638
639 // Get ConvertKit Post's Settings, if they have not yet been loaded.
640 if ( ! $this->post_settings ) {
641 $this->post_settings = new ConvertKit_Post( $post_id );
642 }
643
644 // If the Post specifies a Form to use, return its ID now.
645 if ( $this->post_settings->has_form() ) {
646 return $this->post_settings->get_form();
647 }
648
649 // If the Post specifies that no Form should be used, return false.
650 if ( $this->post_settings->uses_no_form() ) {
651 return false;
652 }
653
654 // Sanity check that the Post uses the Default Form setting, which should be the case
655 // because the above conditions were not met.
656 if ( ! $this->post_settings->uses_default_form() ) {
657 return false;
658 }
659
660 // Get Post's Categories.
661 $categories = wp_get_post_categories(
662 $post_id,
663 array(
664 'fields' => 'ids',
665 )
666 );
667
668 // If no Categories exist, use the Default Form.
669 if ( ! is_array( $categories ) || ! count( $categories ) ) {
670 // Get Post Type.
671 return $this->settings->get_default_form( get_post_type( $post_id ) );
672 }
673
674 /**
675 * Iterate through Categories in reverse order.
676 * This honors the behaviour < 1.9.6, which states that if multiple Categories each have a Form.
677 * assigned, the last Category with a Form in the wp_get_post_categories() call will be used.
678 */
679 $categories = array_reverse( $categories );
680 foreach ( $categories as $term_id ) {
681 // Load Term Settings.
682 $term_settings = new ConvertKit_Term( $term_id );
683
684 // If a Form ID exists, return it now.
685 if ( $term_settings->has_form() ) {
686 return $term_settings->get_form();
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 }
693 }
694
695 // If here, all Terms were set to display the Default Form.
696 // Therefore use the Plugin's Default Form.
697 return $this->settings->get_default_form( get_post_type( $post_id ) );
698
699 }
700
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 /**
733 * Enqueue scripts.
734 *
735 * @since 1.9.6
736 */
737 public function enqueue_scripts() {
738
739 // Get ConvertKit Settings and Post's Settings.
740 $settings = new ConvertKit_Settings();
741
742 // Bail if the no scripts setting is enabled.
743 if ( $settings->scripts_disabled() ) {
744 return;
745 }
746
747 // Register scripts that we might use.
748 wp_register_script(
749 'convertkit-js',
750 CONVERTKIT_PLUGIN_URL . 'resources/frontend/js/convertkit.js',
751 array(),
752 CONVERTKIT_PLUGIN_VERSION,
753 true
754 );
755 wp_localize_script(
756 'convertkit-js',
757 'convertkit',
758 array(
759 'ajaxurl' => admin_url( 'admin-ajax.php' ),
760 'debug' => $settings->debug_enabled(),
761 'nonce' => wp_create_nonce( 'convertkit' ),
762 'subscriber_id' => $this->subscriber_id,
763 )
764 );
765
766 // Enqueue.
767 wp_enqueue_script( 'convertkit-js' );
768
769 }
770
771 /**
772 * Gets the subscriber ID from the request (either the cookie or the URL).
773 *
774 * @since 1.9.6
775 */
776 public function get_subscriber_id_from_request() {
777
778 // Use ConvertKit_Subscriber class to fetch and validate the subscriber ID.
779 $subscriber = new ConvertKit_Subscriber();
780 $subscriber_id = $subscriber->get_subscriber_id();
781
782 // If an error occured, the subscriber ID in the request/cookie is not a valid subscriber.
783 if ( is_wp_error( $subscriber_id ) ) {
784 return;
785 }
786
787 $this->subscriber_id = $subscriber_id;
788
789 }
790
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 // Get form.
816 $convertkit_forms = new ConvertKit_Resource_Forms();
817
818 // Iterate through forms.
819 foreach ( $this->settings->get_non_inline_form() as $form_id ) {
820 // Get Form.
821 $form = $convertkit_forms->get_by_id( (int) $form_id );
822
823 // Bail if the Form doesn't exist (this shouldn't happen, but you never know).
824 if ( ! $form ) {
825 continue;
826 }
827
828 // Add the form to the scripts array so it is included in the output.
829 add_filter(
830 'convertkit_output_scripts_footer',
831 function ( $scripts ) use ( $form ) {
832
833 $scripts[] = array(
834 'async' => true,
835 'data-uid' => $form['uid'],
836 'src' => $form['embed_js'],
837 'data-kit-limit-per-session' => true,
838 );
839
840 return $scripts;
841
842 }
843 );
844 }
845
846 }
847
848 /**
849 * Outputs any JS <script> tags registered with the convertkit_output_scripts_footer
850 * filter
851 *
852 * @since 2.1.4
853 */
854 public function output_scripts_footer() {
855
856 // Don't output scripts if the request is for a search page or 404.
857 if ( is_search() || is_404() ) {
858 return;
859 }
860
861 // Define array of scripts.
862 $scripts = array();
863
864 /**
865 * Define an array of scripts to output in the footer of the WordPress site.
866 *
867 * @since 2.1.4
868 *
869 * @param array $scripts Scripts.
870 */
871 $scripts = apply_filters( 'convertkit_output_scripts_footer', $scripts );
872
873 // Bail if no scripts exist.
874 if ( ! count( $scripts ) ) {
875 return;
876 }
877
878 // Define array to store <script> outputs.
879 $output_scripts = array();
880
881 // Iterate through scripts, building the <script> tag for each.
882 foreach ( $scripts as $script ) {
883 /**
884 * Filter the form <script> key/value pairs immediately before the script is output.
885 *
886 * @since 2.4.5
887 *
888 * @param array $script Form script key/value pairs to output as <script> tag.
889 */
890 $script = apply_filters( 'convertkit_output_script_footer', $script );
891
892 // Skip script if it is limited by the Non-inline Form Limit per Session setting.
893 if ( $this->is_script_output_limited_by_session( $script ) ) {
894 continue;
895 }
896
897 // Build output.
898 $output = '<script';
899 foreach ( $script as $attribute => $value ) {
900 // If the value is true, just output the attribute.
901 if ( $value === true ) {
902 $output .= ' ' . esc_attr( $attribute );
903 continue;
904 }
905
906 // Sanitize attribute and value.
907 $attribute = esc_attr( $attribute );
908 $value = ( $attribute === 'src' ? esc_url( $value ) : esc_attr( $value ) );
909
910 // Output the attribute and value.
911 $output .= ' ' . $attribute;
912
913 // Output the value, if it's not a blank string.
914 if ( strlen( $value ) > 0 ) {
915 $output .= '="' . $value . '"';
916 }
917 }
918
919 $output .= '></script>';
920
921 // Add to array.
922 $output_scripts[] = $output;
923 }
924
925 // Remove duplicate scripts.
926 // This prevents the same non-inline form displaying twice. For example, if a modal form is specified both
927 // in the Page's settings and the Form block, the user would see the same modal form displayed twice
928 // because the script would be output twice.
929 $output_scripts = array_unique( $output_scripts );
930
931 // Output scripts.
932 foreach ( $output_scripts as $output_script ) {
933 echo $output_script . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
934 }
935
936 }
937
938 /**
939 * Checks if a script is limited by the Non-inline Form Limit per Session setting.
940 *
941 * @since 3.0.0
942 *
943 * @param array $script Script.
944 * @return bool
945 */
946 private function is_script_output_limited_by_session( $script ) {
947
948 // Get Settings, if they have not yet been loaded.
949 if ( ! $this->settings ) {
950 $this->settings = new ConvertKit_Settings();
951 }
952
953 // Display script if the "Display Limit" setting isn't enabled.
954 if ( ! $this->settings->non_inline_form_limit_per_session() ) {
955 return false;
956 }
957
958 // Display script if the "Display Limit" setting should not be applied to this script.
959 if ( ! isset( $script['data-kit-limit-per-session'] ) ) {
960 return false;
961 }
962
963 // Display script if this is the first time the visitor has seen any non-inline form.
964 return isset( $_COOKIE['ck_non_inline_form_displayed'] );
965
966 }
967
968 }
969