PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 2.8.6
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v2.8.6
3.4.3 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 All 196 releases
← All changes | includes/class-convertkit-output.php +85 -138 3.3.22.8.6 View file →
@@ -66,9 +66,9 @@
66 66 * @since 1.9.6
67 67 */
68 68 public function __construct() {
69 69
70 - add_action( 'rest_api_init', array( $this, 'register_routes' ) );
70 + add_action( 'init', array( $this, 'get_subscriber_id_from_request' ) );
71 71 add_action( 'wp', array( $this, 'maybe_tag_subscriber' ) );
72 72 add_action( 'template_redirect', array( $this, 'output_form' ) );
73 73 add_action( 'template_redirect', array( $this, 'page_takeover' ) );
74 74 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
@@ -80,64 +80,8 @@
80 80
81 81 }
82 82
83 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 84 * Tags the subscriber, if:
141 85 * - a subscriber ID exists in the cookie or URL,
142 86 * - the WordPress Page has the "Add a Tag" setting specified
143 87 *
@@ -144,8 +88,13 @@
144 88 * @since 2.4.9.1
145 89 */
146 90 public function maybe_tag_subscriber() {
147 91
92 + // Bail if no subscriber ID detected.
93 + if ( ! $this->subscriber_id ) {
94 + return;
95 + }
96 +
148 97 // Bail if not a singular Post Type supported by ConvertKit.
149 98 if ( ! is_singular( convertkit_get_supported_post_types() ) ) {
150 99 return;
151 100 }
@@ -177,16 +126,8 @@
177 126 if ( ! $this->post_settings->has_tag() ) {
178 127 return;
179 128 }
180 129
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 130 // Initialize the API.
190 131 $api = new ConvertKit_API_V4(
191 132 CONVERTKIT_OAUTH_CLIENT_ID,
192 133 CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI,
@@ -369,9 +310,9 @@
369 310 $this->forms = new ConvertKit_Resource_Forms( 'output_form' );
370 311 }
371 312
372 313 // Get Form HTML.
373 - $form = $this->forms->get_html( $form_id, $post_id );
314 + $form = $this->forms->get_html( $form_id );
374 315
375 316 // If an error occured, it could be because the specified Form ID for the Post either:
376 317 // - belongs to another ConvertKit account (i.e. API credentials were changed in the Plugin, but this Post's specified Form was not changed), or
377 318 // - the form was deleted from the ConvertKit account.
@@ -393,9 +334,9 @@
393 334 return $content;
394 335 }
395 336
396 337 // Get Form HTML.
397 - $form = $this->forms->get_html( $form_id, $post_id );
338 + $form = $this->forms->get_html( $form_id );
398 339
399 340 // If an error occured again, the default form doesn't exist in this ConvertKit account.
400 341 // Just return the Post Content, unedited.
401 342 if ( is_wp_error( $form ) ) {
@@ -491,14 +432,28 @@
491 432 if ( empty( $form ) ) {
492 433 return $content;
493 434 }
494 435
495 - // Load the content into the parser.
496 - $parser = new ConvertKit_HTML_Parser( $content, LIBXML_HTML_NODEFDTD );
436 + // Define the meta tag.
437 + $meta_tag = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
497 438
439 + // Wrap content in <html>, <head> and <body> tags now, so we can inject the UTF-8 Content-Type meta tag.
440 + $modified_content = '<html><head></head><body>' . $content . '</body></html>';
441 +
442 + // Forcibly tell DOMDocument that this HTML uses the UTF-8 charset.
443 + // <meta charset="utf-8"> isn't enough, as DOMDocument still interprets the HTML as ISO-8859, which breaks character encoding
444 + // Use of mb_convert_encoding() with HTML-ENTITIES is deprecated in PHP 8.2, so we have to use this method.
445 + // If we don't, special characters render incorrectly.
446 + $modified_content = str_replace( '<head>', '<head>' . "\n" . $meta_tag, $modified_content );
447 +
448 + // Load Page / Post content into DOMDocument.
449 + libxml_use_internal_errors( true );
450 + $html = new DOMDocument();
451 + $html->loadHTML( $modified_content, LIBXML_HTML_NODEFDTD );
452 +
498 453 // Find the element to append the form to.
499 454 // item() is a zero based index.
500 - $element_node = $parser->html->getElementsByTagName( $tag )->item( $index - 1 );
455 + $element_node = $html->getElementsByTagName( $tag )->item( $index - 1 );
501 456
502 457 // If the element could not be found, either the number of elements by tag name is less
503 458 // than the requested position the form be inserted in, or no element exists.
504 459 // Append the form to the original content and return.
@@ -505,27 +460,31 @@
505 460 if ( is_null( $element_node ) ) {
506 461 return $content . $form;
507 462 }
508 463
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 );
464 + // Create new element for the Form.
465 + $form_node = new DOMDocument();
466 + $form_node->loadHTML( $form, LIBXML_HTML_NODEFDTD );
512 467
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 - }
468 + // Append the form to the specific element.
469 + $element_node->parentNode->insertBefore( $html->importNode( $form_node->documentElement, true ), $element_node->nextSibling ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
518 470
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 - }
471 + // Fetch HTML string.
472 + $modified_content = $html->saveHTML();
524 473
525 - // Return modified HTML string.
526 - return $parser->get_body_html();
474 + // Remove some HTML tags that DOMDocument adds, returning the output.
475 + // We do this instead of using LIBXML_HTML_NOIMPLIED in loadHTML(), because Legacy Forms are not always contained in
476 + // a single root / outer element, which is required for LIBXML_HTML_NOIMPLIED to correctly work.
477 + $modified_content = str_replace( '<html>', '', $modified_content );
478 + $modified_content = str_replace( '</html>', '', $modified_content );
479 + $modified_content = str_replace( '<head>', '', $modified_content );
480 + $modified_content = str_replace( '</head>', '', $modified_content );
481 + $modified_content = str_replace( '<body>', '', $modified_content );
482 + $modified_content = str_replace( '</body>', '', $modified_content );
483 + $modified_content = str_replace( $meta_tag, '', $modified_content );
527 484
485 + return $modified_content;
486 +
528 487 }
529 488
530 489 /**
531 490 * Injects the form after the given element and index, using preg_match_all().
@@ -803,31 +762,47 @@
803 762 * @since 1.9.6
804 763 */
805 764 public function enqueue_scripts() {
806 765
807 - // Get ConvertKit Settings and Post's Settings.
808 - $settings = new ConvertKit_Settings();
766 + // Get Post.
767 + $post = get_post();
809 768
810 - // Bail if the no scripts setting is enabled.
811 - if ( $settings->scripts_disabled() ) {
769 + // Bail if no Post could be fetched.
770 + if ( ! $post ) {
812 771 return;
813 772 }
814 773
815 - // Enqueue frontend JS.
816 - convertkit_enqueue_frontend_js();
774 + // Get ConvertKit Settings and Post's Settings.
775 + $settings = new ConvertKit_Settings();
776 + $convertkit_post = new ConvertKit_Post( $post->ID );
817 777
818 - // Define variables.
778 + // Register scripts that we might use.
779 + wp_register_script(
780 + 'convertkit-js',
781 + CONVERTKIT_PLUGIN_URL . 'resources/frontend/js/convertkit.js',
782 + array(),
783 + CONVERTKIT_PLUGIN_VERSION,
784 + true
785 + );
819 786 wp_localize_script(
820 787 'convertkit-js',
821 788 'convertkit',
822 789 array(
823 - 'ajaxurl' => rest_url( 'kit/v1/subscriber/store-email-as-id-in-cookie' ),
790 + 'ajaxurl' => admin_url( 'admin-ajax.php' ),
824 791 'debug' => $settings->debug_enabled(),
825 - 'nonce' => wp_create_nonce( 'wp_rest' ),
792 + 'nonce' => wp_create_nonce( 'convertkit' ),
826 793 'subscriber_id' => $this->subscriber_id,
827 794 )
828 795 );
829 796
797 + // Bail if the no scripts setting is enabled.
798 + if ( $settings->scripts_disabled() ) {
799 + return;
800 + }
801 +
802 + // Enqueue.
803 + wp_enqueue_script( 'convertkit-js' );
804 +
830 805 }
831 806
832 807 /**
833 808 * Gets the subscriber ID from the request (either the cookie or the URL).
@@ -872,11 +847,8 @@
872 847 if ( $this->post_settings !== false && $this->post_settings->uses_no_form() && $this->settings->non_inline_form_honor_none_setting() ) {
873 848 return;
874 849 }
875 850
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 851 // Get form.
880 852 $convertkit_forms = new ConvertKit_Resource_Forms();
881 853
882 854 // Iterate through forms.
@@ -891,15 +863,14 @@
891 863
892 864 // Add the form to the scripts array so it is included in the output.
893 865 add_filter(
894 866 'convertkit_output_scripts_footer',
895 - function ( $scripts ) use ( $form, $limit_per_session ) {
867 + function ( $scripts ) use ( $form ) {
896 868
897 869 $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',
870 + 'async' => true,
871 + 'data-uid' => $form['uid'],
872 + 'src' => $form['embed_js'],
902 873 );
903 874
904 875 return $scripts;
905 876
@@ -952,13 +923,8 @@
952 923 * @param array $script Form script key/value pairs to output as <script> tag.
953 924 */
954 925 $script = apply_filters( 'convertkit_output_script_footer', $script );
955 926
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 927 // Build output.
962 928 $output = '<script';
963 929 foreach ( $script as $attribute => $value ) {
964 930 // If the value is true, just output the attribute.
@@ -993,40 +959,21 @@
993 959 $output_scripts = array_unique( $output_scripts );
994 960
995 961 // Output scripts.
996 962 foreach ( $output_scripts as $output_script ) {
997 - echo $output_script . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
963 + echo wp_kses(
964 + $output_script,
965 + array(
966 + 'script' => array(
967 + 'src' => true,
968 + 'type' => true,
969 + 'async' => true,
970 + 'data-*' => true,
971 + ),
972 + )
973 + );
974 + echo "\n";
998 975 }
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 976
1030 977 }
1031 978
1032 979 }