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

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