Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 542
0.00% covered (danger)
0.00%
0 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Comments
0.00% covered (danger)
0.00%
0 / 538
0.00% covered (danger)
0.00%
0 / 21
16002
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 set_default_color_theme_based_on_theme_settings
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 setup_globals
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 setup_actions
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 setup_filters
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 manage_post_cookie
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
30
 get_avatar
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 comment_reply_link
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
6
 get_blog_token
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 comment_form_before
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 comment_form_after
0.00% covered (danger)
0.00%
0 / 113
0.00% covered (danger)
0.00%
0 / 1
756
 watch_comment_parent
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
30
 pre_comment_on_post
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
272
 retry_submit_comment_form_locally
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
6
 add_comment_meta
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
600
 should_show_subscription_modal
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
42
 get_subscription_modal_data_to_parent
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 subscription_modal_status_track_event
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 capture_comment_duplicate_trigger
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
12
 capture_comment_post_redirect_to_reload_parent_frame
0.00% covered (danger)
0.00%
0 / 89
0.00% covered (danger)
0.00%
0 / 1
30
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Module: Comments
4 *
5 * @package automattic/jetpack
6 */
7
8require __DIR__ . '/base.php';
9use Automattic\Jetpack\Comments\Comments;
10use Automattic\Jetpack\Connection\Tokens;
11use Automattic\Jetpack\Status\Host;
12
13if ( ! defined( 'ABSPATH' ) ) {
14    exit( 0 );
15}
16
17/**
18 * Main Comments class
19 *
20 * @package automattic/jetpack
21 * @since   1.4
22 */
23class Jetpack_Comments extends Highlander_Comments_Base {
24
25    /** Variables *************************************************************/
26
27    /**
28     * Possible comment form sources - empty array as default
29     *
30     * @var array
31     */
32    public $id_sources = array();
33
34    /**
35     * Remote comment URL - empty string as default
36     *
37     * @var string
38     */
39    public $signed_url = '';
40
41    /**
42     * The default comment form color scheme - default is light
43     *
44     * @var string
45     * @see ::set_default_color_theme_based_on_theme_settings()
46     */
47    public $default_color_scheme = 'light';
48
49    /** Methods ***************************************************************/
50
51    /**
52     * Initialize class
53     */
54    public static function init() {
55        static $instance = false;
56
57        if ( ! $instance ) {
58            $instance = new Jetpack_Comments();
59        }
60
61        return $instance;
62    }
63
64    /**
65     * Main constructor for Comments
66     *
67     * @since 1.4
68     */
69    public function __construct() {
70        parent::__construct();
71
72        // Comments is loaded.
73
74        /**
75         * Fires after the Jetpack_Comments object has been instantiated
76         *
77         * @module comments
78         *
79         * @since  1.4.0
80         *
81         * @param array $jetpack_comments_loaded First element in array of type Jetpack_Comments
82         */
83        do_action_ref_array( 'jetpack_comments_loaded', array( $this ) );
84        add_action( 'after_setup_theme', array( $this, 'set_default_color_theme_based_on_theme_settings' ), 100 );
85    }
86
87    /**
88     * Set the default comments color theme based on theme settings
89     */
90    public function set_default_color_theme_based_on_theme_settings() {
91        if ( function_exists( 'twentyeleven_get_theme_options' ) ) {
92            $theme_options      = twentyeleven_get_theme_options();
93            $theme_color_scheme = $theme_options['color_scheme'] ?? 'transparent';
94        } else {
95            $theme_color_scheme = get_theme_mod( 'color_scheme', 'transparent' );
96        }
97        // Default for $theme_color_scheme is 'transparent' just so it doesn't match 'light' or 'dark'.
98        // The default for Jetpack's color scheme is still defined above as 'light'.
99
100        if ( false !== stripos( $theme_color_scheme, 'light' ) ) {
101            $this->default_color_scheme = 'light';
102        } elseif ( false !== stripos( $theme_color_scheme, 'dark' ) ) {
103            $this->default_color_scheme = 'dark';
104        }
105    }
106
107    /** Private Methods *******************************************************/
108
109    /**
110     * Set any global variables or class variables
111     *
112     * This is primarily defining the comment form sources.
113     *
114     * @since 1.4
115     */
116    protected function setup_globals() {
117        parent::setup_globals();
118
119        // Sources.
120        $this->id_sources = array(
121            'guest',
122            'jetpack',
123            'wordpress',
124            'facebook',
125        );
126    }
127
128    /**
129     * Setup actions for methods in this class
130     *
131     * @since 1.4
132     */
133    protected function setup_actions() {
134        if ( Comments::is_enabled() ) {
135            return;
136        }
137
138        parent::setup_actions();
139
140        // Selfishly remove everything from the existing comment form.
141        remove_all_actions( 'comment_form_before' );
142
143        // Selfishly add only our actions back to the comment form.
144        add_action( 'comment_form_before', array( $this, 'manage_post_cookie' ) );
145        add_action( 'comment_form_before', array( $this, 'comment_form_before' ) );
146        add_action( 'comment_form_after', array( $this, 'comment_form_after' ), 1 ); // Set very early since we remove everything outputed before our action.
147
148        // Before a comment is posted.
149        add_action( 'pre_comment_on_post', array( $this, 'pre_comment_on_post' ), 1 );
150
151        // After a comment is posted.
152        add_action( 'comment_post', array( $this, 'add_comment_meta' ) );
153    }
154
155    /**
156     * Setup filters for methods in this class
157     *
158     * @since 1.6.2
159     */
160    protected function setup_filters() {
161        if ( Comments::is_enabled() ) {
162            return;
163        }
164
165        parent::setup_filters();
166
167        add_filter( 'comment_post_redirect', array( $this, 'capture_comment_post_redirect_to_reload_parent_frame' ), 100 );
168        add_filter( 'comment_duplicate_trigger', array( $this, 'capture_comment_duplicate_trigger' ), 100 );
169        add_filter( 'get_avatar', array( $this, 'get_avatar' ), 10, 4 );
170        // Fix comment reply link when `comment_registration` is required.
171        add_filter( 'comment_reply_link', array( $this, 'comment_reply_link' ), 10, 4 );
172    }
173
174    /**
175     * In order for comments to work properly for password-protected posts we need to set `wp-postpass` cookie to SameSite none.
176     */
177    public function manage_post_cookie() {
178        if ( headers_sent() ) {
179            return;
180        }
181
182        $postpass_cookie_key = 'wp-postpass_' . COOKIEHASH;
183
184        if ( empty( $_COOKIE[ $postpass_cookie_key ] ) ) {
185            return;
186        }
187
188        $postpass_cookie_value = sanitize_text_field( wp_unslash( $_COOKIE[ $postpass_cookie_key ] ) );
189
190        if ( empty( $_COOKIE['verbum-wp-postpass'] ) || ( $_COOKIE['verbum-wp-postpass'] !== $postpass_cookie_value ) ) {
191            $expire = apply_filters( 'post_password_expires', time() + 10 * DAY_IN_SECONDS );
192
193            setcookie(
194                $postpass_cookie_key,
195                $postpass_cookie_value,
196                array(
197                    'expires'  => $expire,
198                    'samesite' => 'None',
199                    'path'     => '/',
200                    'domain'   => COOKIE_DOMAIN,
201                    'secure'   => is_ssl(),
202                    'httponly' => false, // phpcs:ignore Jetpack.Functions.SetCookie.FoundNonHTTPOnlyFalse -- @todo Can this be set true?
203                )
204            );
205
206            setcookie(
207                'verbum-wp-postpass',
208                $postpass_cookie_value,
209                array(
210                    'expires'  => $expire,
211                    'samesite' => 'None',
212                    'path'     => '/',
213                    'domain'   => COOKIE_DOMAIN,
214                    'secure'   => is_ssl(),
215                    'httponly' => false, // phpcs:ignore Jetpack.Functions.SetCookie.FoundNonHTTPOnlyFalse -- @todo Can this be set true?
216                )
217            );
218        }
219    }
220
221    /**
222     * Get the comment avatar from Gravatar or Facebook.
223     *
224     * @since 1.4
225     *
226     * @param string $avatar  Current avatar URL.
227     * @param mixed  $comment Comment, user, or email the avatar is for.
228     * @param int    $size    Size of the avatar.
229     *
230     * @return string New avatar
231     */
232    public function get_avatar( $avatar, $comment, $size ) {
233        if ( ! isset( $comment->comment_post_ID ) || ! isset( $comment->comment_ID ) ) {
234            // it's not a comment - bail.
235            return $avatar;
236        }
237
238        // Detect whether it's a Facebook avatar.
239        $foreign_avatar          = get_comment_meta( $comment->comment_ID, 'hc_avatar', true );
240        $foreign_avatar_hostname = wp_parse_url( $foreign_avatar, PHP_URL_HOST );
241        if ( ! $foreign_avatar_hostname ||
242            ! preg_match( '/\.?graph\.facebook\.com$/', $foreign_avatar_hostname ) ) {
243            return $avatar;
244        }
245
246        // Insert the escaped URL through a callback: a preg_replace() replacement string would expand a
247        // `$1` inside it into the captured quote, breaking out of the src attribute (stored-XSS vector).
248        $photon_url = esc_url( set_url_scheme( $this->photon_avatar( $foreign_avatar, $size ), 'https' ) );
249        return preg_replace_callback(
250            '#src=([\'"])[^\'"]+\\1#',
251            static function ( $matches ) use ( $photon_url ) {
252                return 'src=' . $matches[1] . $photon_url . $matches[1];
253            },
254            $avatar
255        );
256    }
257
258    /**
259     * Set comment reply link.
260     * This is to fix the reply link when comment registration is required.
261     *
262     * @param string     $reply_link The HTML markup for the comment reply link.
263     * @param array      $args An array of arguments overriding the defaults.
264     * @param WP_Comment $comment The object of the comment being replied.
265     * @param WP_Post    $post    The WP_Post object.
266     *
267     * @return string New reply link.
268     */
269    public function comment_reply_link( $reply_link, $args, $comment, $post ) {
270        // This is only necessary if comment_registration is required to post comments
271        if ( ! get_option( 'comment_registration' ) ) {
272            return $reply_link;
273        }
274
275        $respond_id = esc_attr( $args['respond_id'] );
276        $add_below  = esc_attr( $args['add_below'] );
277        /* This is to accommodate some themes that add an SVG to the Reply link like twenty-seventeen. */
278        $reply_text  = wp_kses(
279            $args['reply_text'],
280            array(
281                'svg' => array(
282                    'class'           => true,
283                    'aria-hidden'     => true,
284                    'aria-labelledby' => true,
285                    'role'            => true,
286                    'xmlns'           => true,
287                    'width'           => true,
288                    'height'          => true,
289                    'viewbox'         => true,
290                ),
291                'use' => array(
292                    'href'       => true,
293                    'xlink:href' => true,
294                ),
295            )
296        );
297        $before_link = wp_kses( $args['before'], wp_kses_allowed_html( 'post' ) );
298        $after_link  = wp_kses( $args['after'], wp_kses_allowed_html( 'post' ) );
299
300        $reply_url = esc_url( add_query_arg( 'replytocom', $comment->comment_ID . '#' . $respond_id ) );
301
302        return <<<HTML
303            $before_link
304            <a class="comment-reply-link" href="$reply_url" onclick="return addComment.moveForm( '$add_below-$comment->comment_ID', '$comment->comment_ID', '$respond_id', '$post->ID' )">$reply_text</a>
305            $after_link
306HTML;
307    }
308
309    /**
310     * Get the site's blog token.
311     * This can be used to bypass Comments entirely if Jetpack is not properly connected.
312     *
313     * @since 11.2
314     *
315     * @return bool|object False if not properly connected. Object with the blog token if connected.
316     */
317    private function get_blog_token() {
318        $blog_token = ( new Tokens() )->get_access_token();
319        // If we have no token, bail.
320        if ( ! $blog_token || is_wp_error( $blog_token ) ) {
321            return false;
322        }
323
324        return $blog_token;
325    }
326
327    /** Output Methods ********************************************************/
328
329    /**
330     * Start capturing the core comment_form() output
331     *
332     * Comment form output will only be captured if comments are enabled - we return otherwise.
333     *
334     * @since 1.4
335     */
336    public function comment_form_before() {
337        /**
338         * Filters the setting that determines if Jetpack comments should be enabled for
339         * the current post type.
340         *
341         * @module comments
342         *
343         * @since  3.8.1
344         *
345         * @param boolean $return Should comments be enabled?
346         */
347        if ( ! apply_filters( 'jetpack_comment_form_enabled_for_' . get_post_type(), true ) ) {
348            return;
349        }
350
351        // If the Jetpack connection is not healthy, bail.
352        if ( ! $this->get_blog_token() ) {
353            return;
354        }
355
356        // Add some JS to the footer.
357        add_action( 'wp_footer', array( $this, 'watch_comment_parent' ), 100 );
358
359        ob_start();
360    }
361
362    /**
363     * Noop the default comment form output, get some options, and output our
364     * tricked out totally radical comment form.
365     *
366     * @since 1.4
367     */
368    public function comment_form_after() {
369        /** This filter is documented in modules/comments/comments.php */
370        if ( ! apply_filters( 'jetpack_comment_form_enabled_for_' . get_post_type(), true ) ) {
371            return;
372        }
373
374        $blog_token = $this->get_blog_token();
375        // If the Jetpack connection is not healthy, bail.
376        if ( ! $blog_token ) {
377            return;
378        }
379
380        // Throw it all out and drop in our replacement.
381        ob_end_clean();
382
383        if ( in_array( 'subscriptions', Jetpack::get_active_modules(), true ) ) {
384            $stb_enabled = get_option( 'stb_enabled', 1 );
385            $stb_enabled = empty( $stb_enabled ) ? 0 : 1;
386
387            $stc_enabled = get_option( 'stc_enabled', 1 );
388            $stc_enabled = empty( $stc_enabled ) ? 0 : 1;
389        } else {
390            $stb_enabled = 0;
391            $stc_enabled = 0;
392        }
393
394        $params = array(
395            'blogid'                 => Jetpack_Options::get_option( 'id' ),
396            'postid'                 => get_the_ID(),
397            'comment_registration'   => ( get_option( 'comment_registration' ) ? '1' : '0' ), // Need to explicitly send a '1' or a '0' for these.
398            'require_name_email'     => ( get_option( 'require_name_email' ) ? '1' : '0' ),
399            'stc_enabled'            => $stc_enabled,
400            'stb_enabled'            => $stb_enabled,
401            'show_avatars'           => ( get_option( 'show_avatars' ) ? '1' : '0' ),
402            'avatar_default'         => get_option( 'avatar_default' ),
403            'greeting'               => get_option( 'highlander_comment_form_prompt', __( 'Leave a Reply', 'jetpack' ) ),
404            'jetpack_comments_nonce' => wp_create_nonce( 'jetpack_comments_nonce-' . get_the_ID() ),
405            /**
406             * Changes the comment form prompt.
407             *
408             * @module comments
409             *
410             * @since  2.3.0
411             *
412             * @param string $var Default is "Leave a Reply to %s."
413             */
414            'greeting_reply'         => apply_filters(
415                'jetpack_comment_form_prompt_reply',
416                /* translators: %s is the displayed username of the post (or comment) author */
417                __( 'Leave a Reply to %s', 'jetpack' )
418            ),
419            'color_scheme'           => get_option( 'jetpack_comment_form_color_scheme', $this->default_color_scheme ),
420            'lang'                   => get_locale(),
421            'jetpack_version'        => JETPACK__VERSION,
422            'iframe_unique_id'       => wp_unique_id(),
423        );
424
425        // Extra parameters for logged in user.
426        if ( is_user_logged_in() ) {
427            $current_user           = wp_get_current_user();
428            $params['hc_post_as']   = 'jetpack';
429            $params['hc_userid']    = $current_user->ID;
430            $params['hc_username']  = $current_user->display_name;
431            $params['hc_userurl']   = $current_user->user_url;
432            $params['hc_useremail'] = md5( strtolower( trim( $current_user->user_email ) ) );
433            if ( current_user_can( 'unfiltered_html' ) ) {
434                $params['_wp_unfiltered_html_comment'] = wp_create_nonce( 'unfiltered-html-comment_' . get_the_ID() );
435            }
436        } else {
437            $commenter                     = wp_get_current_commenter();
438            $params['show_cookie_consent'] = (int) has_action( 'set_comment_cookies', 'wp_set_comment_cookies' );
439            $params['has_cookie_consent']  = (int) ! empty( $commenter['comment_author_email'] );
440            // Jetpack_Memberships for logged out users only checks for the wp-jp-premium-content-session cookie
441            $params['is_current_user_subscribed'] = class_exists( '\Jetpack_Memberships' ) ? (int) Jetpack_Memberships::is_current_user_subscribed() : 0;
442        }
443
444        list( $token_key ) = explode( '.', $blog_token->secret, 2 );
445        // Prophylactic check: anything else should never happen.
446        if ( $token_key && $token_key !== $blog_token->secret ) {
447            // Is the token a Special Token (@see class.tokens.php)?
448            if ( preg_match( '/^;.\d+;\d+;$/', $token_key, $matches ) ) {
449                // The token key for a Special Token is public.
450                $params['token_key'] = $token_key;
451            } else {
452                /*
453                 * The token key for a Normal Token is public but
454                 * looks like sensitive data. Since there can only be
455                 * one Normal Token per site, avoid concern by
456                 * sending the magic "use the Normal Token" token key.
457                 */
458                $params['token_key'] = Tokens::MAGIC_NORMAL_TOKEN_KEY;
459            }
460        }
461
462        $signature = self::sign_remote_comment_parameters( $params, $blog_token->secret );
463        if ( is_wp_error( $signature ) ) {
464            $signature = 'error';
465        }
466
467        $params['sig'] = $signature;
468        $url_origin    = 'https://jetpack.wordpress.com';
469        $url           = "{$url_origin}/jetpack-comment/?" . http_build_query( $params );
470        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sniff misses the esc_url_raw.
471        $url              = "{$url}#parent=" . rawurlencode( esc_url_raw( set_url_scheme( 'http://' . ( isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : '' ) . ( isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '' ) ) ) );
472        $this->signed_url = $url;
473        $height           = $params['comment_registration'] || is_user_logged_in() ? '315' : '430'; // Iframe can be shorter if we're not allowing guest commenting.
474        $transparent      = ( 'transparent' === $params['color_scheme'] ) ? 'true' : 'false';
475
476        if ( isset( $_GET['replytocom'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
477            $url .= '&replytocom=' . (int) $_GET['replytocom']; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
478        }
479
480        /**
481         * Filter whether the comment title can be displayed.
482         *
483         * @module comments
484         *
485         * @since  4.7.0
486         *
487         * @param bool $show Can the comment be displayed? Default to true.
488         */
489        $show_greeting = apply_filters( 'jetpack_comment_form_display_greeting', true );
490
491        /**
492         * Filter the comment title tag.
493         *
494         * @module comments
495         * @since 12.4
496         *
497         * @param string $comment_reply_title_tag The comment title tag. Default to h3.
498         */
499        $comment_reply_title_tag = apply_filters( 'jetpack_comment_reply_title_tag', 'h3' );
500
501        // The actual iframe (loads comment form from Jetpack server).
502
503        $is_amp = class_exists( Jetpack_AMP_Support::class ) && Jetpack_AMP_Support::is_amp_request();
504        ?>
505
506        <div id="respond" class="comment-respond">
507            <?php
508            if ( true === $show_greeting ) :
509                printf(
510                    '<%1$s id="reply-title" class="comment-reply-title">',
511                    esc_html( $comment_reply_title_tag )
512                );
513
514                comment_form_title(
515                    esc_html( $params['greeting'] ),
516                    esc_html( $params['greeting_reply'] )
517                );
518                echo '<small>';
519                cancel_comment_reply_link( esc_html__( 'Cancel reply', 'jetpack' ) );
520                echo '</small>';
521
522                printf(
523                    '</%1$s>',
524                    esc_html( $comment_reply_title_tag )
525                );
526            endif;
527            ?>
528            <form id="commentform" class="comment-form">
529                <iframe
530                    title="<?php esc_attr_e( 'Comment Form', 'jetpack' ); ?>"
531                    src="<?php echo esc_url( $url ); ?>"
532                    <?php if ( $is_amp ) : ?>
533                        resizable
534                        layout="fixed-height"
535                        height="<?php echo esc_attr( $height ); ?>"
536                    <?php else : ?>
537                        name="jetpack_remote_comment"
538                        style="width:100%; height: <?php echo esc_attr( $height ); ?>px; border:0;"
539                    <?php endif; ?>
540                    class="jetpack_remote_comment"
541                    id="jetpack_remote_comment"
542                    sandbox="allow-same-origin allow-top-navigation allow-scripts allow-forms allow-popups"
543                >
544                    <?php if ( $is_amp ) : ?>
545                        <button overflow><?php esc_html_e( 'Show more', 'jetpack' ); ?></button>
546                    <?php endif; ?>
547                </iframe>
548                <?php if ( ! $is_amp ) : ?>
549                    <!--[if !IE]><!-->
550                    <script>
551                        document.addEventListener('DOMContentLoaded', function () {
552                            var commentForms = document.getElementsByClassName('jetpack_remote_comment');
553                            for (var i = 0; i < commentForms.length; i++) {
554                                commentForms[i].allowTransparency = <?php echo esc_html( $transparent ); ?>;
555                                commentForms[i].scrolling = 'no';
556                            }
557                        });
558                    </script>
559                    <!--<![endif]-->
560                <?php endif; ?>
561            </form>
562        </div>
563
564        <?php // Below is required for comment reply JS to work. ?>
565
566        <input type="hidden" name="comment_parent" id="comment_parent" value="" />
567
568        <?php
569    }
570
571    /**
572     * Add some JS to wp_footer to watch for hierarchical reply parent change
573     *
574     * If AMP is enabled, we don't make any changes.
575     *
576     * @since 1.4
577     */
578    public function watch_comment_parent() {
579        if ( class_exists( Jetpack_AMP_Support::class ) && Jetpack_AMP_Support::is_amp_request() ) {
580            // @todo Implement AMP support.
581            return;
582        }
583        ?>
584        <script type="text/javascript">
585            (function () {
586                const iframe = document.getElementById( 'jetpack_remote_comment' );
587                <?php if ( get_option( 'thread_comments' ) && get_option( 'thread_comments_depth' ) ) : ?>
588                const watchReply = function() {
589                    // Check addComment._Jetpack_moveForm to make sure we don't monkey-patch twice.
590                    if ( 'undefined' !== typeof addComment && ! addComment._Jetpack_moveForm ) {
591                        // Cache the Core function.
592                        addComment._Jetpack_moveForm = addComment.moveForm;
593                        const commentParent = document.getElementById( 'comment_parent' );
594                        const cancel = document.getElementById( 'cancel-comment-reply-link' );
595
596                        function tellFrameNewParent ( commentParentValue ) {
597                            const url = new URL( iframe.src );
598                            if ( commentParentValue ) {
599                                url.searchParams.set( 'replytocom', commentParentValue )
600                            } else {
601                                url.searchParams.delete( 'replytocom' );
602                            }
603                            if( iframe.src !== url.href ) {
604                                iframe.src = url.href;
605                            }
606                        };
607
608                        cancel.addEventListener( 'click', function () {
609                            tellFrameNewParent( false );
610                        } );
611
612                        addComment.moveForm = function ( _, parentId ) {
613                            tellFrameNewParent( parentId );
614                            return addComment._Jetpack_moveForm.apply( null, arguments );
615                        };
616                    }
617                }
618                document.addEventListener( 'DOMContentLoaded', watchReply );
619                // In WP 6.4+, the script is loaded asynchronously, so we need to wait for it to load before we monkey-patch the functions it introduces.
620                document.querySelector('#comment-reply-js')?.addEventListener( 'load', watchReply );
621
622                <?php endif; ?>
623                
624                const commentIframes = document.getElementsByClassName('jetpack_remote_comment');
625
626                window.addEventListener('message', function(event) {
627                    if (event.origin !== 'https://jetpack.wordpress.com') {
628                        return;
629                    }
630
631                    if (!event?.data?.iframeUniqueId && !event?.data?.height) {
632                        return;
633                    }
634
635                    const eventDataUniqueId = event.data.iframeUniqueId;
636
637                    // Change height for the matching comment iframe
638                    for (let i = 0; i < commentIframes.length; i++) {
639                        const iframe = commentIframes[i];
640                        const url = new URL(iframe.src);
641                        const iframeUniqueIdParam = url.searchParams.get('iframe_unique_id');
642                        if (iframeUniqueIdParam == event.data.iframeUniqueId) {
643                            iframe.style.height = event.data.height + 'px';
644                            return;
645                        }
646                    }
647                });
648            })();
649        </script>
650        <?php
651    }
652
653    /**
654     * Verify the hash included in remote comments.
655     *
656     * If the Jetpack token is missing we return nothing,
657     * and if the token is unknown or invalid, or comments not allowed, an error is returned.
658     *
659     * @since 1.4
660     */
661    public function pre_comment_on_post() {
662        $post_array = stripslashes_deep( $_POST );
663
664        // Bail if missing the Jetpack token.
665        if ( ! isset( $post_array['sig'] ) || ! isset( $post_array['token_key'] ) || ! is_string( $post_array['sig'] ) || ! is_string( $post_array['token_key'] ) ) {
666            unset( $_POST['hc_post_as'] );
667            return;
668        }
669
670        if ( empty( $post_array['jetpack_comments_nonce'] ) || ! wp_verify_nonce( $post_array['jetpack_comments_nonce'], "jetpack_comments_nonce-{$post_array['comment_post_ID']}" ) ) {
671            if ( ! isset( $_GET['only_once'] ) ) {
672                self::retry_submit_comment_form_locally();
673            }
674            wp_die( esc_html__( 'Nonce verification failed.', 'jetpack' ), 400 );
675        }
676
677        if ( isset( $post_array['hc_avatar'] ) && is_string( $post_array['hc_avatar'] ) && str_contains( $post_array['hc_avatar'], '.gravatar.com' ) ) {
678            $post_array['hc_avatar'] = htmlentities( $post_array['hc_avatar'], ENT_COMPAT );
679        }
680
681        $blog_token = ( new Tokens() )->get_access_token( false, $post_array['token_key'] );
682        if ( ! $blog_token || is_wp_error( $blog_token ) ) {
683            wp_die( esc_html__( 'Unknown security token.', 'jetpack' ), 400 );
684        }
685        $check = self::sign_remote_comment_parameters( $post_array, $blog_token->secret );
686        if ( is_wp_error( $check ) ) {
687            wp_die( esc_html( $check ) );
688        }
689
690        // Bail if token is expired or not valid.
691        if ( ! hash_equals( $check, $post_array['sig'] ) ) {
692            wp_die( esc_html__( 'Invalid security token.', 'jetpack' ), 400 );
693        }
694
695        /** This filter is documented in modules/comments/comments.php */
696        if ( ! apply_filters( 'jetpack_comment_form_enabled_for_' . get_post_type( $post_array['comment_post_ID'] ), true ) ) {
697            // In case the comment POST is legit, but the comments are
698            // now disabled, we don't allow the comment.
699
700            wp_die( esc_html__( 'Comments are not allowed.', 'jetpack' ), 403 );
701        }
702    }
703
704    /**
705     * Handle Jetpack Comments POST requests: process the comment form, then client-side POST the results to the self-hosted blog
706     *
707     * This function exists because when we submit the form via the jetpack.wordpress.com iframe
708     * in Chrome the request comes in to Jetpack but for some reason the request doesn't have access to cookies yet.
709     * By submitting the form again locally with the same data the process works as expected.
710     *
711     * @return never
712     */
713    public function retry_submit_comment_form_locally() {
714        // We are not doing any validation here since all the validation will be done again by pre_comment_on_post().
715        // phpcs:ignore WordPress.Security.NonceVerification.Missing
716        $comment_data = stripslashes_deep( $_POST );
717        ?>
718        <!DOCTYPE html>
719        <html>
720        <head>
721        <meta charset="utf-8">
722        <title><?php echo esc_html__( 'Submitting Comment', 'jetpack' ); ?></title>
723        <style type="text/css">
724            body {
725                display: table;
726                width: 100%;
727                height: 60%;
728                position: absolute;
729                top: 0;
730                left: 0;
731                overflow: hidden;
732                color: #333;
733            }
734            .jetpack-comment-spinner {
735                display: table-cell;
736                vertical-align: middle;
737                text-align: center;
738            }
739        </style>
740        </head>
741        <body>
742        <div class="jetpack-comment-spinner">
743            <?php
744            require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-spinner.php';
745            echo Jetpack_Spinner::render( 28 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG markup.
746            ?>
747        </div>
748        <form id="jetpack-remote-comment-post-form" action="<?php echo esc_url( get_site_url() ); ?>/wp-comments-post.php?for=jetpack&only_once=true" method="POST">
749            <?php foreach ( $comment_data as $key => $val ) : ?>
750                <input type="hidden" name="<?php echo esc_attr( $key ); ?>" value="<?php echo esc_attr( $val ); ?>" />
751            <?php endforeach; ?>
752        </form>
753
754        <script type="text/javascript">
755            document.getElementById("jetpack-remote-comment-post-form").submit();
756        </script>
757        </body>
758        </html>
759        <?php
760        exit( 0 );
761    }
762
763    /** Capabilities **********************************************************/
764
765    /**
766     * Add some additional comment meta after comment is saved about what
767     * service the comment is from, the avatar, user_id, etc...
768     *
769     * @since 1.4
770     *
771     * @param int $comment_id The comment ID.
772     */
773    public function add_comment_meta( $comment_id ) {
774        // phpcs:disable WordPress.Security.NonceVerification.Missing -- The hc_* fields are authenticated by the HMAC check below.
775        $post_array = stripslashes_deep( $_POST );
776
777        // The hc_* identity fields are only trustworthy on a signed request. pre_comment_on_post() checks
778        // that, but only on wp-comments-post.php, so re-check here for any other producer that reaches
779        // comment_post (e.g. Carousel's unauthenticated post_attachment_comment endpoint).
780        if ( ! isset( $post_array['sig'] ) || ! isset( $post_array['token_key'] ) || ! is_string( $post_array['sig'] ) || ! is_string( $post_array['token_key'] ) ) {
781            return;
782        }
783        if ( isset( $post_array['hc_avatar'] ) && is_string( $post_array['hc_avatar'] ) && str_contains( $post_array['hc_avatar'], '.gravatar.com' ) ) {
784            $post_array['hc_avatar'] = htmlentities( $post_array['hc_avatar'], ENT_COMPAT );
785        }
786        $blog_token = ( new Tokens() )->get_access_token( false, $post_array['token_key'] );
787        if ( ! $blog_token || is_wp_error( $blog_token ) ) {
788            return;
789        }
790        $check = self::sign_remote_comment_parameters( $post_array, $blog_token->secret );
791        if ( is_wp_error( $check ) || ! hash_equals( $check, $post_array['sig'] ) ) {
792            return;
793        }
794
795        $comment_meta = array();
796
797        switch ( $this->is_highlander_comment_post() ) {
798            case 'facebook':
799                $comment_meta['hc_post_as']         = 'facebook';
800                $comment_meta['hc_avatar']          = isset( $_POST['hc_avatar'] ) ? esc_url_raw( wp_unslash( $_POST['hc_avatar'] ) ) : null;
801                $comment_meta['hc_foreign_user_id'] = isset( $_POST['hc_userid'] ) ? sanitize_text_field( wp_unslash( $_POST['hc_userid'] ) ) : null;
802                break;
803
804            // phpcs:ignore WordPress.WP.CapitalPDangit
805            case 'wordpress':
806                // phpcs:ignore WordPress.WP.CapitalPDangit
807                $comment_meta['hc_post_as']         = 'wordpress';
808                $comment_meta['hc_avatar']          = isset( $_POST['hc_avatar'] ) ? esc_url_raw( wp_unslash( $_POST['hc_avatar'] ) ) : null;
809                $comment_meta['hc_foreign_user_id'] = isset( $_POST['hc_userid'] ) ? sanitize_text_field( wp_unslash( $_POST['hc_userid'] ) ) : null;
810                $comment_meta['hc_wpcom_id_sig']    = isset( $_POST['hc_wpcom_id_sig'] ) ? sanitize_text_field( wp_unslash( $_POST['hc_wpcom_id_sig'] ) ) : null; // since 1.9.
811                break;
812
813            case 'jetpack':
814                $comment_meta['hc_post_as']         = 'jetpack';
815                $comment_meta['hc_avatar']          = isset( $_POST['hc_avatar'] ) ? esc_url_raw( wp_unslash( $_POST['hc_avatar'] ) ) : null;
816                $comment_meta['hc_foreign_user_id'] = isset( $_POST['hc_userid'] ) ? sanitize_text_field( wp_unslash( $_POST['hc_userid'] ) ) : null;
817                break;
818
819        }
820        // phpcs:enable WordPress.Security.NonceVerification.Missing
821
822        // Bail if no extra comment meta.
823        if ( empty( $comment_meta ) ) {
824            return;
825        }
826
827        // Loop through extra meta and add values.
828        foreach ( $comment_meta as $key => $value ) {
829            add_comment_meta( $comment_id, $key, $value, true );
830        }
831    }
832
833    /**
834     * Should show the subscription modal
835     *
836     * @return boolean
837     */
838    public function should_show_subscription_modal() {
839
840        // Not allow it to run on self-hosted or simple sites
841        if ( ! ( new Host() )->is_wpcom_platform() || ( new Host() )->is_wpcom_simple() ) {
842            return false;
843        }
844
845        // phpcs:disable WordPress.Security.NonceVerification.Missing
846        $is_current_user_subscribed = isset( $_POST['is_current_user_subscribed'] ) ? filter_var( wp_unslash( $_POST['is_current_user_subscribed'] ) ) : null;
847
848        // Atomic sites with jetpack_verbum_subscription_modal option enabled
849        $modal_enabled = ( new Host() )->is_woa_site() && get_option( 'jetpack_verbum_subscription_modal', true );
850
851        return $modal_enabled && ! $is_current_user_subscribed;
852    }
853
854    /**
855     * Get the data to send as an event to the parent window on subscription modal
856     *
857     * @param string $url url to redirect to.
858     *
859     * @return array
860     */
861    public function get_subscription_modal_data_to_parent( $url ) {
862        // phpcs:ignore WordPress.Security.NonceVerification.Missing
863        $current_user_email = isset( $_POST['email'] ) ? filter_var( wp_unslash( $_POST['email'] ) ) : null;
864        // phpcs:ignore WordPress.Security.NonceVerification.Missing
865        $post_id = isset( $_POST['comment_post_ID'] ) ? filter_var( wp_unslash( $_POST['comment_post_ID'] ) ) : null;
866        return array(
867            'url'          => $url,
868            'email'        => $current_user_email,
869            'blog_id'      => esc_attr( \Jetpack_Options::get_option( 'id' ) ),
870            'post_id'      => esc_attr( $post_id ),
871            'lang'         => esc_attr( get_locale() ),
872            'is_logged_in' => isset( $_POST['hc_userid'] ),
873        );
874    }
875
876    /**
877     * Track the hidden event for the subscription modal
878     */
879    public function subscription_modal_status_track_event() {
880        $tracking_event = 'hidden_disabled';
881        // Not allow it to run on self-hosted or simple sites
882        if ( ! ( new Host() )->is_wpcom_platform() || ( new Host() )->is_wpcom_simple() ) {
883            $tracking_event = 'hidden_self_hosted';
884        }
885
886        // phpcs:disable WordPress.Security.NonceVerification.Missing
887        $is_current_user_subscribed = isset( $_POST['is_current_user_subscribed'] ) ? filter_var( wp_unslash( $_POST['is_current_user_subscribed'] ) ) : null;
888
889        if ( $is_current_user_subscribed ) {
890            $tracking_event = 'hidden_already_subscribed';
891        }
892
893        $jetpack = Jetpack::init();
894        // $jetpack->stat automatically prepends the stat group with 'jetpack-'
895        $jetpack->stat( 'subscribe-modal-comm', $tracking_event );
896        $jetpack->do_stats( 'server_side' );
897    }
898
899    /**
900     * Catch the duplicated comment error and show a custom error page
901     *
902     * @return never
903     */
904    public function capture_comment_duplicate_trigger() {
905        if ( ! isset( $_GET['for'] ) || 'jetpack' !== $_GET['for'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
906            exit( 0 );
907        }
908
909        ?>
910        <!DOCTYPE html>
911        <html <?php language_attributes(); ?>>
912        <!--<![endif]-->
913        <head>
914            <meta charset="<?php bloginfo( 'charset' ); ?>" />
915            <title>
916                <?php
917                    wp_kses_post(
918                        printf(
919                            /* translators: %s is replaced by an ellipsis */
920                            __( 'Submitting Comment%s', 'jetpack' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
921                            '&hellip;'
922                        )
923                    );
924                ?>
925                </title>
926            <style type="text/css">
927                body {
928                    display: table;
929                    width: 100%;
930                    height: 60%;
931                    position: absolute;
932                    top: 0;
933                    left: 0;
934                    overflow: hidden;
935                    color: #333;
936                    padding-top: 3%;
937                }
938                div {
939                    text-align: left;
940                    margin: 0;
941                    padding: 0;
942                    display: table-cell;
943                    vertical-align: top;
944                    font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", sans-serif;
945                    font-weight: normal;
946                }
947
948                h3 {
949                    margin: 0;
950                    padding-bottom: 3%;
951                    font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", sans-serif;
952                    font-weight: normal;
953                }
954                a {
955                    text-decoration: underline;
956                    color: #333 !important;
957                }
958            </style>
959        </head>
960        <body>
961        <div>
962            <h3>
963                <?php
964                    esc_html_e( 'Duplicate comment detected; it looks as though you’ve already said that!', 'jetpack' );
965                ?>
966            </h3>
967            <a href="javascript:backToComments()"><?php esc_html_e( '&laquo; Back', 'jetpack' ); ?></a>
968        </div>
969        <script type="text/javascript">
970            function backToComments() {
971                const test = regexp => {
972                        return regexp.test(navigator.userAgent);
973                };
974                if (test(/chrome|chromium|crios|safari|edg/i)) {
975                        history.go(-2);
976                        return;
977                }
978                history.back();
979            }
980        </script>
981
982        </body>
983        </html>
984        <?php
985        exit( 0 );
986    }
987
988    /**
989     * POST the submitted comment to the iframe
990     *
991     * @param string $url The comment URL origin.
992     */
993    public function capture_comment_post_redirect_to_reload_parent_frame( $url ) {
994        if ( ! isset( $_GET['for'] ) || 'jetpack' !== $_GET['for'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
995            return $url;
996        }
997
998        $should_show_subscription_modal = $this->should_show_subscription_modal();
999
1000        // Track event when not showing the subscription modal
1001        if ( ! $should_show_subscription_modal ) {
1002            $this->subscription_modal_status_track_event();
1003        }
1004        ?>
1005        <!DOCTYPE html>
1006        <html <?php language_attributes(); ?>>
1007        <!--<![endif]-->
1008        <head>
1009            <meta charset="<?php bloginfo( 'charset' ); ?>" />
1010            <title>
1011                <?php
1012                    wp_kses_post(
1013                        printf(
1014                            /* translators: %s is replaced by an ellipsis */
1015                            __( 'Submitting Comment%s', 'jetpack' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1016                            '&hellip;'
1017                        )
1018                    );
1019                ?>
1020                </title>
1021            <style type="text/css">
1022                body {
1023                    display: table;
1024                    width: 100%;
1025                    height: 60%;
1026                    position: absolute;
1027                    top: 0;
1028                    left: 0;
1029                    overflow: hidden;
1030                    color: #333;
1031                    padding-top: 3%;
1032                }
1033
1034                h3 {
1035                    text-align: center;
1036                    margin: 0;
1037                    padding: 0;
1038                    display: table-cell;
1039                    vertical-align: top;
1040                    font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", sans-serif;
1041                    font-weight: normal;
1042                }
1043
1044                .hidden {
1045                    opacity: 0;
1046                }
1047
1048                h3 span {
1049                    -moz-transition-property: opacity;
1050                    -moz-transition-duration: 1s;
1051                    -moz-transition-timing-function: ease-in-out;
1052
1053                    -webkit-transition-property: opacity;
1054                    -webkit-transition-duration: 1s;
1055                    -webbit-transition-timing-function: ease-in-out;
1056
1057                    -o-transition-property: opacity;
1058                    -o-transition-duration: 1s;
1059                    -o-transition-timing-function: ease-in-out;
1060
1061                    -ms-transition-property: opacity;
1062                    -ms-transition-duration: 1s;
1063                    -ms-transition-timing-function: ease-in-out;
1064
1065                    transition-property: opacity;
1066                    transition-duration: 1s;
1067                    transition-timing-function: ease-in-out;
1068                }
1069            </style>
1070        </head>
1071        <body>
1072        <?php if ( ! $should_show_subscription_modal ) { ?>
1073        <h3>
1074            <?php
1075                wp_kses_post(
1076                    printf(
1077                        /* translators: %s is replaced by HTML markup to include an ellipsis */
1078                        __( 'Submitting Comment%s', 'jetpack' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1079                        '<span id="ellipsis" class="hidden">&hellip;</span>'
1080                    )
1081                );
1082            ?>
1083        </h3>
1084        <script type="text/javascript">
1085            try {
1086                window.parent.location.href = <?php echo wp_json_encode( $url, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>;
1087                window.parent.location.reload( true );
1088            } catch (e) {
1089                window.location.href = <?php echo wp_json_encode( $url, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>;
1090                window.location.reload( true );
1091            }
1092            ellipsis = document.getElementById('ellipsis');
1093
1094            function toggleEllipsis() {
1095                ellipsis.className = ellipsis.className ? '' : 'hidden';
1096            }
1097
1098            setInterval(toggleEllipsis, 1200);
1099        </script>
1100        <?php } else { ?>
1101        <h3>
1102            <?php
1103                wp_kses_post(
1104                    print __( 'Comment sent', 'jetpack' ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1105                );
1106            ?>
1107        </h3>
1108        <script type="text/javascript">
1109            if ( window.parent && window.parent !== window ) {
1110
1111                window.parent.postMessage(
1112                    {
1113                        type: 'subscriptionModalShow',
1114                        data: <?php echo wp_json_encode( $this->get_subscription_modal_data_to_parent( $url ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
1115                    },
1116                    window.location.origin
1117                );
1118            }
1119        </script>
1120        <?php } ?>
1121        </body>
1122        </html>
1123        <?php
1124        exit( 0 );
1125    }
1126}
1127
1128Jetpack_Comments::init();