PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.1.9
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.1.9
3.4.2 3.4.1 3.4.0 3.3.9 3.3.8 3.3.7 3.3.6 3.3.5 3.3.4 3.3.3 3.3.2 3.3.1 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.2.8 2.2.9 2.3.0 2.3.1 2.3.2 All 195 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.1.9, at includes/class-convertkit-output.php

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