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

749 lines 21.1 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 if an API Key and Secret is not defined.
116 if ( ! $this->settings->has_api_key_and_secret() ) {
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 * Appends 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 .= '<!-- ConvertKit 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 .= '<!-- ConvertKit 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 .= '<!-- ConvertKit append_form_to_content(): Default Form: ' . $form->get_error_message() . ' -->';
323 }
324
325 return $content;
326 }
327 }
328
329 // If here, we have a ConvertKit Form.
330 // Append form to Post's Content.
331 $content = $content .= $form;
332
333 /**
334 * Filter the Post's Content, which includes a ConvertKit Form, immediately before it is output.
335 *
336 * @since 1.9.6
337 *
338 * @param string $content Post Content
339 * @param string $form ConvertKit Form HTML
340 * @param int $post_id Post ID
341 * @param int $form_id ConvertKit Form ID
342 */
343 $content = apply_filters( 'convertkit_frontend_append_form', $content, $form, $post_id, $form_id );
344
345 return $content;
346
347 }
348
349 /**
350 * Registers the ConvertKit Form block to before or after the Query Loop block, when viewing a Category archive.
351 *
352 * See append_form_block_on_category_archive() configures the block to display the applicable category's Form.
353 *
354 * @since 2.4.9.1
355 *
356 * @param array $hooked_blocks The list of hooked block types.
357 * @param string $position The relative position of the hooked blocks.
358 * @param string $anchor_block The anchor block type.
359 * @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.
360 * @return array
361 */
362 public function maybe_register_form_block_on_category_archive( $hooked_blocks, $position, $anchor_block, $context ) {
363
364 // Don't append if we're not viewing a category archive.
365 if ( ! is_category() ) {
366 return $hooked_blocks;
367 }
368
369 if ( $context instanceof WP_Block_Template && $context->slug !== 'archive' ) {
370 return $hooked_blocks;
371 }
372
373 // Don't append if the anchor block isn't the Query Loop block.
374 if ( $anchor_block !== 'core/query' ) {
375 return $hooked_blocks;
376 }
377
378 // Don't append if the Category's form position setting is not defined.
379 $form_position = $this->get_term_form_position();
380 if ( ! $form_position ) {
381 // Unhook this function as we don't need to check again in this request, as we'll
382 // never output a form on the Category archive.
383 remove_filter( 'hooked_block_types', array( $this, 'maybe_register_form_block_on_category_archive' ), 10 );
384
385 return $hooked_blocks;
386 }
387
388 // Don't append if the position doesn't match.
389 if ( $form_position !== $position ) {
390 return $hooked_blocks;
391 }
392
393 // Hook the ConvertKit Form block.
394 $hooked_blocks[] = 'convertkit/form';
395
396 // Unhook this function as we don't need to check again in this request, as
397 // we have now appended the form.
398 remove_filter( 'hooked_block_types', array( $this, 'maybe_register_form_block_on_category_archive' ), 10 );
399
400 return $hooked_blocks;
401
402 }
403
404 /**
405 * Configures the ConvertKit Form block that was hooked below the Query Loop block by maybe_register_form_block_on_category_archive,
406 * defining the Form ID based on the current Category's Form ID.
407 *
408 * @since 2.4.9.1
409 *
410 * @param array $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block.
411 * @return null|array
412 */
413 public function append_form_block_to_category_archive( $parsed_hooked_block ) {
414
415 // Sanity check that we're still viewing a Category archive.
416 if ( ! is_category() ) {
417 // Returning null will unregister the Form block from displaying.
418 return null;
419 }
420
421 // Get Category archive being viewed.
422 $category = get_category( get_query_var( 'cat' ) );
423
424 // Bail if the Category could be found.
425 if ( is_wp_error( $category ) || is_null( $category ) ) {
426 // Returning null will unregister the Form block from displaying.
427 return null;
428 }
429
430 // Load Term Settings.
431 $term_settings = new ConvertKit_Term( $category->term_id );
432
433 // Bail if no Form specified for the Category.
434 if ( ! $term_settings->has_form() ) {
435 // Returning null will unregister the Form block from displaying.
436 return null;
437 }
438
439 // Define the form block attributes to display the given Form ID.
440 $parsed_hooked_block['attrs'] = array(
441 'id' => absint( $term_settings->get_form() ),
442 );
443
444 // Return the Form block with its attributes.
445 return $parsed_hooked_block;
446
447 }
448
449 /**
450 * Returns the Post, Category or Plugin ConvertKit Form ID for the given Post.
451 *
452 * If the Post specifies a form to use, returns that Form ID.
453 * If the Post uses the 'Default' setting, and an assigned Category has a Form ID, uses the Category's Form ID.
454 * Otherwise falls back to the Plugin's Default Form ID (if any).
455 *
456 * @since 1.9.6
457 *
458 * @param int $post_id Post ID.
459 * @return bool|string|int false|'default'|Form ID
460 */
461 private function get_post_form_id( $post_id ) {
462
463 // Get Settings, if they have not yet been loaded.
464 if ( ! $this->settings ) {
465 $this->settings = new ConvertKit_Settings();
466 }
467
468 // Get ConvertKit Post's Settings, if they have not yet been loaded.
469 if ( ! $this->post_settings ) {
470 $this->post_settings = new ConvertKit_Post( $post_id );
471 }
472
473 // If the Post specifies a Form to use, return its ID now.
474 if ( $this->post_settings->has_form() ) {
475 return $this->post_settings->get_form();
476 }
477
478 // If the Post specifies that no Form should be used, return false.
479 if ( $this->post_settings->uses_no_form() ) {
480 return false;
481 }
482
483 // Sanity check that the Post uses the Default Form setting, which should be the case
484 // because the above conditions were not met.
485 if ( ! $this->post_settings->uses_default_form() ) {
486 return false;
487 }
488
489 // Get Post's Categories.
490 $categories = wp_get_post_categories(
491 $post_id,
492 array(
493 'fields' => 'ids',
494 )
495 );
496
497 // If no Categories exist, use the Default Form.
498 if ( ! is_array( $categories ) || ! count( $categories ) ) {
499 // Get Post Type.
500 return $this->settings->get_default_form( get_post_type( $post_id ) );
501 }
502
503 /**
504 * Iterate through Categories in reverse order.
505 * This honors the behaviour < 1.9.6, which states that if multiple Categories each have a Form.
506 * assigned, the last Category with a Form in the wp_get_post_categories() call will be used.
507 */
508 $categories = array_reverse( $categories );
509 foreach ( $categories as $term_id ) {
510 // Load Term Settings.
511 $term_settings = new ConvertKit_Term( $term_id );
512
513 // If a Form ID exists, return it now.
514 if ( $term_settings->has_form() ) {
515 return $term_settings->get_form();
516 }
517 }
518
519 // If here, use the Plugin's Default Form.
520 return $this->settings->get_default_form( get_post_type( $post_id ) );
521
522 }
523
524 /**
525 * Returns the Form Position setting for the currently viewed Category.
526 *
527 * @since 2.4.9.1
528 *
529 * @return bool|string
530 */
531 private function get_term_form_position() {
532
533 // Get Category archive being viewed.
534 $category = get_category( get_query_var( 'cat' ) );
535
536 // Bail if the Category could be found.
537 if ( is_wp_error( $category ) || is_null( $category ) ) {
538 return false;
539 }
540
541 // Load Term Settings.
542 $term_settings = new ConvertKit_Term( $category->term_id );
543
544 // Return false if no form position is defined i.e. we don't want to display
545 // it on the Category archive.
546 if ( ! $term_settings->has_form_position() ) {
547 return false;
548 }
549
550 // Return form position.
551 return $term_settings->get_form_position();
552
553 }
554
555 /**
556 * Enqueue scripts.
557 *
558 * @since 1.9.6
559 */
560 public function enqueue_scripts() {
561
562 // Get Post.
563 $post = get_post();
564
565 // Bail if no Post could be fetched.
566 if ( ! $post ) {
567 return;
568 }
569
570 // Get ConvertKit Settings and Post's Settings.
571 $settings = new ConvertKit_Settings();
572 $convertkit_post = new ConvertKit_Post( $post->ID );
573
574 // Register scripts that we might use.
575 wp_register_script(
576 'convertkit-js',
577 CONVERTKIT_PLUGIN_URL . 'resources/frontend/js/convertkit.js',
578 array(),
579 CONVERTKIT_PLUGIN_VERSION,
580 true
581 );
582 wp_localize_script(
583 'convertkit-js',
584 'convertkit',
585 array(
586 'ajaxurl' => admin_url( 'admin-ajax.php' ),
587 'debug' => $settings->debug_enabled(),
588 'nonce' => wp_create_nonce( 'convertkit' ),
589 'subscriber_id' => $this->subscriber_id,
590 )
591 );
592
593 // Bail if the no scripts setting is enabled.
594 if ( $settings->scripts_disabled() ) {
595 return;
596 }
597
598 // Enqueue.
599 wp_enqueue_script( 'convertkit-js' );
600
601 }
602
603 /**
604 * Gets the subscriber ID from the request (either the cookie or the URL).
605 *
606 * @since 1.9.6
607 */
608 public function get_subscriber_id_from_request() {
609
610 // Use ConvertKit_Subscriber class to fetch and validate the subscriber ID.
611 $subscriber = new ConvertKit_Subscriber();
612 $subscriber_id = $subscriber->get_subscriber_id();
613
614 // If an error occured, the subscriber ID in the request/cookie is not a valid subscriber.
615 if ( is_wp_error( $subscriber_id ) ) {
616 return;
617 }
618
619 $this->subscriber_id = $subscriber_id;
620
621 }
622
623 /**
624 * Outputs a non-inline form if defined in the Plugin's settings >
625 * Default Non-Inline Form (Global) setting.
626 *
627 * @since 2.3.3
628 */
629 public function output_global_non_inline_form() {
630
631 // Get Settings, if they have not yet been loaded.
632 if ( ! $this->settings ) {
633 $this->settings = new ConvertKit_Settings();
634 }
635
636 // Bail if no non-inline form setting is specified.
637 if ( ! $this->settings->has_non_inline_form() ) {
638 return;
639 }
640
641 // Get form.
642 $convertkit_forms = new ConvertKit_Resource_Forms();
643 $form = $convertkit_forms->get_by_id( (int) $this->settings->get_non_inline_form() );
644
645 // Bail if the Form doesn't exist (this shouldn't happen, but you never know).
646 if ( ! $form ) {
647 return;
648 }
649
650 // Add the form to the scripts array so it is included in the output.
651 add_filter(
652 'convertkit_output_scripts_footer',
653 function ( $scripts ) use ( $form ) {
654
655 $scripts[] = array(
656 'async' => true,
657 'data-uid' => $form['uid'],
658 'src' => $form['embed_js'],
659 );
660
661 return $scripts;
662
663 }
664 );
665
666 }
667
668 /**
669 * Outputs any JS <script> tags registered with the convertkit_output_scripts_footer
670 * filter
671 *
672 * @since 2.1.4
673 */
674 public function output_scripts_footer() {
675
676 // Define array of scripts.
677 $scripts = array();
678
679 /**
680 * Define an array of scripts to output in the footer of the WordPress site.
681 *
682 * @since 2.1.4
683 *
684 * @param array $scripts Scripts.
685 */
686 $scripts = apply_filters( 'convertkit_output_scripts_footer', $scripts );
687
688 // Bail if no scripts exist.
689 if ( ! count( $scripts ) ) {
690 return;
691 }
692
693 // Define array to store <script> outputs.
694 $output_scripts = array();
695
696 // Iterate through scripts, building the <script> tag for each.
697 foreach ( $scripts as $script ) {
698 /**
699 * Filter the form <script> key/value pairs immediately before the script is output.
700 *
701 * @since 2.4.5
702 *
703 * @param array $script Form script key/value pairs to output as <script> tag.
704 */
705 $script = apply_filters( 'convertkit_output_script_footer', $script );
706
707 // Build output.
708 $output = '<script';
709 foreach ( $script as $attribute => $value ) {
710 // If the value is true, just output the attribute.
711 if ( $value === true ) {
712 $output .= ' ' . esc_attr( $attribute );
713 continue;
714 }
715
716 // Sanitize attribute and value.
717 $attribute = esc_attr( $attribute );
718 $value = ( $attribute === 'src' ? esc_url( $value ) : esc_attr( $value ) );
719
720 // Output the attribute and value.
721 $output .= ' ' . $attribute;
722
723 // Output the value, if it's not a blank string.
724 if ( strlen( $value ) > 0 ) {
725 $output .= '="' . $value . '"';
726 }
727 }
728
729 $output .= '></script>';
730
731 // Add to array.
732 $output_scripts[] = $output;
733 }
734
735 // Remove duplicate scripts.
736 // This prevents the same non-inline form displaying twice. For example, if a modal form is specified both
737 // in the Page's settings and the Form block, the user would see the same modal form displayed twice
738 // because the script would be output twice.
739 $output_scripts = array_unique( $output_scripts );
740
741 // Output scripts.
742 foreach ( $output_scripts as $output_script ) {
743 echo $output_script . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
744 }
745
746 }
747
748 }
749