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.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.2.3, at includes/class-convertkit-output.php

1,033 lines 30.6 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( 'rest_api_init', array( $this, 'register_routes' ) );
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 * 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 /**
220 * Runs the `convertkit_output_output_form` action for singular Post Types that don't use the_content()
221 * or apply_filters( 'the_content' ) to output a ConvertKit Form.
222 *
223 * @since 1.9.6
224 */
225 public function output_form() {
226
227 /**
228 * Outputs a ConvertKit Form on singular Post Types that don't use the_content()
229 * or apply_filters( 'the_content' ).
230 *
231 * @since 1.9.6
232 *
233 * @return string Post Content with Form Appended, if applicable
234 */
235 do_action( 'convertkit_output_output_form' );
236
237 }
238
239 /**
240 * Outputs a ConvertKit Landing Page if configured, replacing all output for the singular Post Type.
241 *
242 * @since 1.9.6
243 */
244 public function page_takeover() {
245
246 $queried_object = get_queried_object();
247
248 // Bail if the queried object cannot be inspected.
249 if ( ! isset( $queried_object->post_type ) ) {
250 return;
251 }
252
253 // Get Post ID.
254 $post_id = $queried_object->ID;
255
256 // Bail if the queried object isn't a supported Post Type for Landing Pages.
257 if ( $queried_object->post_type !== 'page' ) {
258 return;
259 }
260
261 // Get ConvertKit Post's Settings, if they have not yet been loaded.
262 if ( ! $this->post_settings ) {
263 $this->post_settings = new ConvertKit_Post( $post_id );
264 }
265
266 // Get Landing Page ID.
267 $landing_page_id = $this->post_settings->get_landing_page();
268
269 /**
270 * Define the ConvertKit Landing Page ID to display for the given Post ID,
271 * overriding the Post settings.
272 *
273 * Return false to not display any ConvertKit Landing Page.
274 *
275 * @since 1.9.6
276 *
277 * @param int $landing_page_id Landing Page ID
278 * @param int $post_id Post ID
279 */
280 $landing_page_id = apply_filters( 'convertkit_output_page_takeover_landing_page_id', $landing_page_id, $post_id );
281
282 // Bail if no Landing Page is configured to be output.
283 if ( empty( $landing_page_id ) ) {
284 return;
285 }
286
287 // Get available ConvertKit Landing Pages, if they have not yet been loaded.
288 if ( ! $this->landing_pages ) {
289 $this->landing_pages = new ConvertKit_Resource_Landing_Pages( 'output_landing_page' );
290 }
291
292 // Get Landing Page.
293 $landing_page = $this->landing_pages->get_html( $this->post_settings->get_landing_page() );
294
295 // Bail if an error occured.
296 if ( is_wp_error( $landing_page ) ) {
297 return;
298 }
299
300 // Replace the favicon with the WordPress site's favicon, if specified.
301 $landing_page = $this->landing_pages->replace_favicon( $landing_page );
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
318 // Output Landing Page.
319 // Output is supplied from ConvertKit's API, which is already sanitized.
320 echo $landing_page; // phpcs:ignore WordPress.Security.EscapeOutput
321 exit;
322
323 }
324
325 /**
326 * Inserts a form to the singular Page, Post or Custom Post Type's Content.
327 *
328 * @param string $content Post Content.
329 * @return string Post Content with Form Appended, if applicable
330 */
331 public function append_form_to_content( $content ) {
332
333 // Bail if not a singular Post Type supported by ConvertKit.
334 if ( ! is_singular( convertkit_get_supported_post_types() ) ) {
335 return $content;
336 }
337
338 // Get Post ID.
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.
347 $form_id = $this->get_post_form_id( $post_id );
348
349 /**
350 * Define the ConvertKit Form ID to display for the given Post ID,
351 * overriding the Post, Category or Plugin settings.
352 *
353 * Return false to not display any ConvertKit Form.
354 *
355 * @since 1.9.6
356 *
357 * @param bool|int $form_id Form ID
358 * @param int $post_id Post ID
359 */
360 $form_id = apply_filters( 'convertkit_output_append_form_to_content_form_id', $form_id, $post_id );
361
362 // Return the Post Content, unedited, if the Form ID is false or zero.
363 if ( ! $form_id ) {
364 return $content;
365 }
366
367 // Get available ConvertKit Forms, if they have not yet been loaded.
368 if ( ! $this->forms ) {
369 $this->forms = new ConvertKit_Resource_Forms( 'output_form' );
370 }
371
372 // Get Form HTML.
373 $form = $this->forms->get_html( $form_id, $post_id );
374
375 // If an error occured, it could be because the specified Form ID for the Post either:
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
377 // - the form was deleted from the ConvertKit account.
378 // Attempt to fallback to the default form for this Post Type.
379 if ( is_wp_error( $form ) ) {
380 if ( $this->settings->debug_enabled() ) {
381 $content .= '<!-- Kit append_form_to_content(): ' . $form->get_error_message() . ' Attempting fallback to Default Form. -->';
382 }
383
384 // Get Default Form ID for this Post's Type.
385 $form_id = $this->settings->get_default_form( get_post_type( $post_id ) );
386
387 // If no Default Form is specified, just return the Post Content, unedited.
388 if ( ! $form_id ) {
389 if ( $this->settings->debug_enabled() ) {
390 $content .= '<!-- Kit append_form_to_content(): No Default Form exists as a fallback. -->';
391 }
392
393 return $content;
394 }
395
396 // Get Form HTML.
397 $form = $this->forms->get_html( $form_id, $post_id );
398
399 // If an error occured again, the default form doesn't exist in this ConvertKit account.
400 // Just return the Post Content, unedited.
401 if ( is_wp_error( $form ) ) {
402 if ( $this->settings->debug_enabled() ) {
403 $content .= '<!-- Kit append_form_to_content(): Default Form: ' . $form->get_error_message() . ' -->';
404 }
405
406 return $content;
407 }
408 }
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
420 // If here, we have a ConvertKit 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 ) );
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
460 /**
461 * Filter the Post's Content, which includes a ConvertKit Form, immediately before it is output.
462 *
463 * @since 1.9.6
464 *
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.
470 */
471 $content = apply_filters( 'convertkit_frontend_append_form', $content, $form, $post_id, $form_id, $form_position );
472
473 return $content;
474
475 }
476
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 /**
689 * Returns the Post, Category or Plugin ConvertKit Form ID for the given Post.
690 *
691 * If the Post specifies a form to use, returns that Form ID.
692 * If the Post uses the 'Default' setting, and an assigned Category has a Form ID, uses the Category's Form ID.
693 * Otherwise falls back to the Plugin's Default Form ID (if any).
694 *
695 * @since 1.9.6
696 *
697 * @param int $post_id Post ID.
698 * @return bool|string|int false|'default'|Form ID
699 */
700 private function get_post_form_id( $post_id ) {
701
702 // Get Settings, if they have not yet been loaded.
703 if ( ! $this->settings ) {
704 $this->settings = new ConvertKit_Settings();
705 }
706
707 // Get ConvertKit Post's Settings, if they have not yet been loaded.
708 if ( ! $this->post_settings ) {
709 $this->post_settings = new ConvertKit_Post( $post_id );
710 }
711
712 // If the Post specifies a Form to use, return its ID now.
713 if ( $this->post_settings->has_form() ) {
714 return $this->post_settings->get_form();
715 }
716
717 // If the Post specifies that no Form should be used, return false.
718 if ( $this->post_settings->uses_no_form() ) {
719 return false;
720 }
721
722 // Sanity check that the Post uses the Default Form setting, which should be the case
723 // because the above conditions were not met.
724 if ( ! $this->post_settings->uses_default_form() ) {
725 return false;
726 }
727
728 // Get Post's Categories.
729 $categories = wp_get_post_categories(
730 $post_id,
731 array(
732 'fields' => 'ids',
733 )
734 );
735
736 // If no Categories exist, use the Default Form.
737 if ( ! is_array( $categories ) || ! count( $categories ) ) {
738 // Get Post Type.
739 return $this->settings->get_default_form( get_post_type( $post_id ) );
740 }
741
742 /**
743 * Iterate through Categories in reverse order.
744 * This honors the behaviour < 1.9.6, which states that if multiple Categories each have a Form.
745 * assigned, the last Category with a Form in the wp_get_post_categories() call will be used.
746 */
747 $categories = array_reverse( $categories );
748 foreach ( $categories as $term_id ) {
749 // Load Term Settings.
750 $term_settings = new ConvertKit_Term( $term_id );
751
752 // If a Form ID exists, return it now.
753 if ( $term_settings->has_form() ) {
754 return $term_settings->get_form();
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 }
761 }
762
763 // If here, all Terms were set to display the Default Form.
764 // Therefore use the Plugin's Default Form.
765 return $this->settings->get_default_form( get_post_type( $post_id ) );
766
767 }
768
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 /**
801 * Enqueue scripts.
802 *
803 * @since 1.9.6
804 */
805 public function enqueue_scripts() {
806
807 // Get ConvertKit Settings and Post's Settings.
808 $settings = new ConvertKit_Settings();
809
810 // Bail if the no scripts setting is enabled.
811 if ( $settings->scripts_disabled() ) {
812 return;
813 }
814
815 // Enqueue frontend JS.
816 convertkit_enqueue_frontend_js();
817
818 // Define variables.
819 wp_localize_script(
820 'convertkit-js',
821 'convertkit',
822 array(
823 'ajaxurl' => rest_url( 'kit/v1/subscriber/store-email-as-id-in-cookie' ),
824 'debug' => $settings->debug_enabled(),
825 'nonce' => wp_create_nonce( 'wp_rest' ),
826 'subscriber_id' => $this->subscriber_id,
827 )
828 );
829
830 }
831
832 /**
833 * Gets the subscriber ID from the request (either the cookie or the URL).
834 *
835 * @since 1.9.6
836 */
837 public function get_subscriber_id_from_request() {
838
839 // Use ConvertKit_Subscriber class to fetch and validate the subscriber ID.
840 $subscriber = new ConvertKit_Subscriber();
841 $subscriber_id = $subscriber->get_subscriber_id();
842
843 // If an error occured, the subscriber ID in the request/cookie is not a valid subscriber.
844 if ( is_wp_error( $subscriber_id ) ) {
845 return;
846 }
847
848 $this->subscriber_id = $subscriber_id;
849
850 }
851
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 /**
913 * Outputs any JS <script> tags registered with the convertkit_output_scripts_footer
914 * filter
915 *
916 * @since 2.1.4
917 */
918 public function output_scripts_footer() {
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
925 // Define array of scripts.
926 $scripts = array();
927
928 /**
929 * Define an array of scripts to output in the footer of the WordPress site.
930 *
931 * @since 2.1.4
932 *
933 * @param array $scripts Scripts.
934 */
935 $scripts = apply_filters( 'convertkit_output_scripts_footer', $scripts );
936
937 // Bail if no scripts exist.
938 if ( ! count( $scripts ) ) {
939 return;
940 }
941
942 // Define array to store <script> outputs.
943 $output_scripts = array();
944
945 // Iterate through scripts, building the <script> tag for each.
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.
962 $output = '<script';
963 foreach ( $script as $attribute => $value ) {
964 // If the value is true, just output the attribute.
965 if ( $value === true ) {
966 $output .= ' ' . esc_attr( $attribute );
967 continue;
968 }
969
970 // Sanitize attribute and value.
971 $attribute = esc_attr( $attribute );
972 $value = ( $attribute === 'src' ? esc_url( $value ) : esc_attr( $value ) );
973
974 // Output the attribute and 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 }
981 }
982
983 $output .= '></script>';
984
985 // Add to array.
986 $output_scripts[] = $output;
987 }
988
989 // Remove duplicate scripts.
990 // This prevents the same non-inline form displaying twice. For example, if a modal form is specified both
991 // in the Page's settings and the Form block, the user would see the same modal form displayed twice
992 // because the script would be output twice.
993 $output_scripts = array_unique( $output_scripts );
994
995 // Output scripts.
996 foreach ( $output_scripts as $output_script ) {
997 echo $output_script . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
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'] );
1029
1030 }
1031
1032 }
1033