| @@ -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 | } |