Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
10.43% covered (danger)
10.43%
29 / 278
20.00% covered (danger)
20.00%
3 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Comment_Form
10.43% covered (danger)
10.43%
29 / 278
20.00% covered (danger)
20.00%
3 / 15
3100.92
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 comment_reply_link
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
56
 enabled_for_post_type
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 comment_form_fields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 comment_form_logged_in
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 comment_form_defaults
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 render
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
56
 render_must_log_in
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 markup
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
110
 post_id
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 register_assets
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 print_noscript_style
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_assets
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 1
110
 verify_nonce
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
9.02
1<?php
2/**
3 * The comment form.
4 *
5 * @package automattic/jetpack-comments
6 */
7
8namespace Automattic\Jetpack\Comments;
9
10use Automattic\Jetpack\Assets;
11
12/**
13 * Replaces the core comment form, and accepts what it submits.
14 */
15class Comment_Form {
16
17    const HANDLE       = 'jetpack-comments';
18    const NONCE_ACTION = 'jetpack_comments_form';
19    const NONCE_NAME   = 'jetpack_comments_form_nonce';
20    const THEME_FORM   = 'jetpack_comments_theme_form';
21
22    /**
23     * Singleton instance.
24     *
25     * @var Comment_Form|null
26     */
27    private static $instance = null;
28
29    /**
30     * Whether the settings blob has been printed.
31     *
32     * @var bool
33     */
34    private $settings_printed = false;
35
36    /**
37     * The form defaults last seen, for the must-log-in branch, which core fires with no arguments.
38     *
39     * @var array
40     */
41    private $defaults = array();
42
43    /**
44     * Register the form's hooks. Safe to call more than once.
45     *
46     * @return Comment_Form
47     */
48    public static function init() {
49        if ( null === self::$instance ) {
50            self::$instance = new self();
51        }
52
53        return self::$instance;
54    }
55
56    /**
57     * Take over the core comment form.
58     */
59    private function __construct() {
60        add_filter( 'comment_form_fields', array( $this, 'comment_form_fields' ) );
61        add_filter( 'comment_form_logged_in', array( $this, 'comment_form_logged_in' ) );
62        add_filter( 'comment_form_defaults', array( $this, 'comment_form_defaults' ), 20 );
63        // Past 10, where Jetpack Subscriptions adds its checkboxes, so this sees them before replacing the field.
64        add_filter( 'comment_form_submit_field', array( $this, 'render' ), 20, 2 );
65        add_action( 'comment_form_must_log_in_after', array( $this, 'render_must_log_in' ) );
66        add_filter( 'comment_reply_link', array( $this, 'comment_reply_link' ), 10, 4 );
67        add_action( 'wp_enqueue_scripts', array( $this, 'register_assets' ) );
68        add_action( 'pre_comment_on_post', array( $this, 'verify_nonce' ) );
69    }
70
71    /**
72     * Keep Reply moving the form when the site requires registration.
73     *
74     * @param string      $reply_link Markup for the reply link.
75     * @param array       $args       Reply link arguments.
76     * @param \WP_Comment $comment    Comment being replied to.
77     * @param \WP_Post    $post       Post being commented on.
78     * @return string
79     */
80    public function comment_reply_link( $reply_link, $args, $comment, $post ) {
81        if ( ! get_option( 'comment_registration' ) || ! self::enabled_for_post_type() ) {
82            return $reply_link;
83        }
84
85        $comment = get_comment( $comment );
86        $post    = get_post( $post );
87
88        if ( ! $comment instanceof \WP_Comment || ! $post instanceof \WP_Post ) {
89            return $reply_link;
90        }
91
92        $respond_id = esc_attr( $args['respond_id'] );
93        $reply_to   = sprintf( $args['reply_to_text'], get_comment_author( $comment ) );
94
95        // A theme may put an icon inside its reply link.
96        $reply_text_html = array(
97            'svg' => array(
98                'class'           => true,
99                'aria-hidden'     => true,
100                'aria-labelledby' => true,
101                'role'            => true,
102                'xmlns'           => true,
103                'width'           => true,
104                'height'          => true,
105                'viewbox'         => true,
106            ),
107            'use' => array(
108                'href'       => true,
109                'xlink:href' => true,
110            ),
111        );
112
113        $link = sprintf(
114            '<a class="comment-reply-link" href="%s"%s onclick="return addComment.moveForm( \'%s-%d\', \'%d\', \'%s\', \'%d\' )">%s</a>',
115            esc_url( add_query_arg( 'replytocom', $comment->comment_ID . '#' . $respond_id ) ),
116            $args['show_reply_to_text'] ? '' : ' aria-label="' . esc_attr( $reply_to ) . '"',
117            esc_attr( $args['add_below'] ),
118            $comment->comment_ID,
119            $comment->comment_ID,
120            $respond_id,
121            $post->ID,
122            wp_kses( $args['show_reply_to_text'] ? $reply_to : $args['reply_text'], $reply_text_html )
123        );
124
125        return wp_kses( $args['before'], wp_kses_allowed_html( 'post' ) )
126            . $link
127            . wp_kses( $args['after'], wp_kses_allowed_html( 'post' ) );
128    }
129
130    /**
131     * Whether this form replaces core's for a post's type.
132     *
133     * @param int|null $post_id Post being commented on. Defaults to the current one.
134     * @return bool
135     */
136    public static function enabled_for_post_type( $post_id = null ) {
137        $post_type = $post_id ? get_post_type( $post_id ) : get_post_type();
138
139        /** This filter is documented in projects/plugins/jetpack/modules/comments/comments.php */
140        return (bool) apply_filters( 'jetpack_comment_form_enabled_for_' . $post_type, true );
141    }
142
143    /**
144     * Drop every field core would draw.
145     *
146     * @param array $fields Comment form fields, the textarea included.
147     * @return array
148     */
149    public function comment_form_fields( $fields ) {
150        return self::enabled_for_post_type() ? array() : $fields;
151    }
152
153    /**
154     * Suppress core's logged-in line.
155     *
156     * @param string $logged_in_as The "logged in as" markup.
157     * @return string
158     */
159    public function comment_form_logged_in( $logged_in_as ) {
160        return self::enabled_for_post_type() ? '' : $logged_in_as;
161    }
162
163    /**
164     * Set the form arguments the app reads back out.
165     *
166     * @param array|mixed $args Comment form arguments. Null when an earlier filter drops them.
167     * @return array
168     */
169    public function comment_form_defaults( $args ) {
170        if ( ! self::enabled_for_post_type() ) {
171            return $args;
172        }
173
174        $defaults = array(
175            'logged_in_as'         => '',
176            'comment_notes_before' => '',
177            'must_log_in'          => '',
178            'label_submit'         => _x( 'Comment', 'verb', 'jetpack-comments' ),
179        );
180
181        $greeting = get_option( 'highlander_comment_form_prompt' );
182        if ( is_string( $greeting ) && $greeting !== '' ) {
183            $defaults['title_reply'] = $greeting;
184        }
185
186        $this->defaults = array_merge( (array) $args, $defaults );
187
188        return $this->defaults;
189    }
190
191    /**
192     * Replace the submit field with the app.
193     *
194     * @param string      $submit_field The submit field markup this replaces.
195     * @param array|mixed $args         Comment form arguments, after the theme's own. Anything from a theme-built form.
196     * @return string
197     */
198    public function render( $submit_field, $args = array() ) {
199        if ( ! self::enabled_for_post_type() ) {
200            return $submit_field;
201        }
202
203        // A theme that builds its own form (Thesis) never runs comment_form(), so leave its form alone.
204        if ( ! is_array( $args ) || ! $this->defaults ) {
205            // The marker gives the comment its own Tracks event, and is where the script finds the form to
206            // add the nonce to, as enqueue_assets() does for the app. Again on DOMContentLoaded, for an optimizer.
207            return $submit_field
208                . '<input type="hidden" name="' . self::THEME_FORM . '" value="1" />'
209                . wp_get_inline_script_tag(
210                    sprintf(
211                        '(function(){function n(){Array.prototype.forEach.call(document.querySelectorAll(\'[name="%1$s"]\'),function(m){var f=m.form,i;if(f&&!f.querySelector(\'[name="%2$s"]\')){i=document.createElement("input");i.type="hidden";i.name="%2$s";i.value=%3$s;f.appendChild(i);}});}n();document.addEventListener("DOMContentLoaded",n);})();',
212                        self::THEME_FORM,
213                        self::NONCE_NAME,
214                        wp_json_encode( wp_create_nonce( self::NONCE_ACTION ), JSON_HEX_TAG | JSON_HEX_AMP )
215                    )
216                );
217        }
218
219        // The subscribe checkboxes the host drew: Jetpack's in this field, WordPress.com's from its own
220        // function. Drawn in the dialog under the host's names, so its gating and handlers still apply.
221        $drawn = $submit_field;
222        if ( function_exists( 'subscription_comment_form' ) ) {
223            // Echoed and caught: its stub declares no return value.
224            ob_start();
225            subscription_comment_form( self::post_id() );
226            $drawn .= (string) ob_get_clean();
227        }
228
229        $labels = array(
230            'subscribe_comments' => __( 'Notify me of new comments by email.', 'jetpack-comments' ),
231            'subscribe'          => __( 'Notify me of new comments by email.', 'jetpack-comments' ),
232            'subscribe_blog'     => sprintf(
233                /* translators: %s is the site's name. */
234                __( 'Subscribe to keep up with %s.', 'jetpack-comments' ),
235                get_bloginfo( 'name' )
236            ),
237        );
238
239        $args['subscriptions'] = array();
240        foreach ( $labels as $name => $label ) {
241            if ( preg_match( '/<input\b[^>]*\bname="' . $name . '"[^>]*>/', $drawn, $input ) ) {
242                $args['subscriptions'][] = array(
243                    'name'    => $name,
244                    'label'   => $label,
245                    'checked' => false !== strpos( $input[0], 'checked' ),
246                );
247            }
248        }
249
250        // Fires after this filter, and would draw the subscribe options again below the form.
251        remove_action( 'comment_form', 'subscription_comment_form' );
252
253        $this->enqueue_assets( $args );
254
255        return $this->markup( $args );
256    }
257
258    /**
259     * Draw the app, and a form to hold it, on the must-log-in branch.
260     *
261     * @return void
262     */
263    public function render_must_log_in() {
264        if ( ! self::enabled_for_post_type() ) {
265            return;
266        }
267
268        $args = $this->defaults;
269
270        $this->enqueue_assets( $args );
271
272        printf(
273            '<form action="%s" method="post" id="commentform" class="comment-form">%s</form>',
274            esc_url( site_url( '/wp-comments-post.php' ) ),
275            $this->markup( $args ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped as it is built.
276        );
277    }
278
279    /**
280     * The app's mount point, holding a plain form until the script takes over, and the hidden fields both post with.
281     *
282     * @param array $args Comment form arguments.
283     * @return string
284     */
285    private function markup( $args = array() ) {
286        $post_id   = self::post_id();
287        $permalink = get_permalink( $post_id );
288        $button    = sprintf(
289            $args['submit_button'] ?? '<input name="%1$s" type="submit" id="%2$s" class="%3$s" value="%4$s" />',
290            esc_attr( $args['name_submit'] ?? 'submit' ),
291            esc_attr( $args['id_submit'] ?? 'submit' ),
292            esc_attr( $args['class_submit'] ?? 'submit' ),
293            esc_attr( $args['label_submit'] ?? _x( 'Comment', 'verb', 'jetpack-comments' ) )
294        );
295
296        // The classes come from the button template, not class_submit: on a block
297        // theme the Post Comments Form block bakes the theme's button classes into it.
298        $class = preg_match( '/\bclass="([^"]*)"/', $button, $match ) ? $match[1] : 'submit';
299
300        // Values belonging to this form rather than to the page it sits on.
301        $settings = array(
302            'postId'        => $post_id,
303            'loginUrl'      => wp_login_url( $permalink ),
304            // wp_logout_url() runs the URL through esc_html(), which encodes single quotes too.
305            'logoutUrl'     => is_user_logged_in() ? html_entity_decode( wp_logout_url( $permalink ), ENT_QUOTES ) : '',
306            'submit'        => array(
307                'id'        => $args['id_submit'] ?? 'submit',
308                'name'      => $args['name_submit'] ?? 'submit',
309                'class'     => $class,
310                // The block wraps its button the way the Buttons block does, so block-level button styles reach it.
311                'wrapClass' => false !== strpos( $class, 'wp-block-button__link' ) ? 'wp-block-button' : '',
312                'label'     => $args['label_submit'] ?? _x( 'Comment', 'verb', 'jetpack-comments' ),
313            ),
314            'subscriptions' => $args['subscriptions'] ?? array(),
315        );
316
317        $noscript = '';
318
319        // Core's own fields and submit, for a page whose settings another release rendered or whose script never ran.
320        if ( get_option( 'comment_registration' ) && ! is_user_logged_in() ) {
321            $plain = '<p class="must-log-in">' . sprintf(
322                /* translators: %s is a link to the log-in page. */
323                esc_html__( 'You must be %s to post a comment.', 'jetpack-comments' ),
324                '<a href="' . esc_url( wp_login_url( $permalink ) ) . '">' . esc_html__( 'logged in', 'jetpack-comments' ) . '</a>'
325            ) . '</p>';
326        } else {
327            $required = (bool) get_option( 'require_name_email' );
328            $plain    = '<p class="comment-form-comment"><label for="comment">' . esc_html_x( 'Comment', 'noun', 'jetpack-comments' ) . '</label>'
329                . '<textarea id="comment" name="comment" rows="4" required></textarea></p>';
330
331            if ( ! is_user_logged_in() ) {
332                // Without script a guest has no nonce to post, so they get a note in place of fields that cannot send.
333                $noscript = '<noscript><style>.jetpack-comments{display:none!important}</style><p>'
334                    . esc_html__( 'Turn on JavaScript to leave a comment.', 'jetpack-comments' )
335                    . '</p></noscript>';
336
337                $commenter = wp_get_current_commenter();
338                $fields    = array(
339                    'author' => array( __( 'Name', 'jetpack-comments' ), 'text', $commenter['comment_author'], $required ),
340                    'email'  => array( __( 'Email', 'jetpack-comments' ), 'email', $commenter['comment_author_email'], $required ),
341                    'url'    => array( __( 'Website', 'jetpack-comments' ), 'text', $commenter['comment_author_url'], false ),
342                );
343
344                foreach ( $fields as $name => list( $label, $type, $value, $is_required ) ) {
345                    $plain .= sprintf(
346                        '<p class="comment-form-%1$s"><label for="%1$s">%2$s</label><input id="%1$s" name="%1$s" type="%3$s" value="%4$s"%5$s%6$s /></p>',
347                        $name,
348                        esc_html( $label ),
349                        $type,
350                        esc_attr( $value ),
351                        $is_required ? ' required' : '',
352                        'url' === $name ? ' maxlength="200"' : ''
353                    );
354                }
355            }
356
357            $plain .= sprintf( $args['submit_field'] ?? '<p class="form-submit">%1$s %2$s</p>', $button, '' );
358        }
359
360        return '<div class="jetpack-comments"'
361            . ' data-jetpack-comments="' . esc_attr( (string) wp_json_encode( $settings, JSON_UNESCAPED_SLASHES | JSON_HEX_AMP ) ) . '">'
362            . $plain
363            . '</div>'
364            . $noscript
365            . get_comment_id_fields( $post_id );
366    }
367
368    /**
369     * The post being commented on.
370     *
371     * @return int
372     */
373    public static function post_id() {
374        $post = get_post();
375
376        return $post ? $post->ID : 0;
377    }
378
379    /**
380     * Register the bundle, and the stylesheet on a singular view.
381     *
382     * @return void
383     */
384    public function register_assets() {
385        if ( wp_script_is( self::HANDLE, 'registered' ) ) {
386            return;
387        }
388
389        // The asset version is the script's hash, so a stylesheet-only change would ship under a cached URL.
390        $asset = include dirname( __DIR__, 2 ) . '/build/comments.asset.php';
391
392        Assets::register_script(
393            self::HANDLE,
394            '../../build/comments.js',
395            __FILE__,
396            array(
397                'in_footer' => true,
398                'strategy'  => 'defer',
399                'version'   => $asset['version'] . '-' . (string) filemtime( dirname( __DIR__, 2 ) . '/build/comments.css' ),
400            )
401        );
402
403        if ( is_singular() && comments_open() ) {
404            wp_enqueue_style( self::HANDLE );
405            add_action( 'wp_head', array( __CLASS__, 'print_noscript_style' ) );
406        }
407    }
408
409    /**
410     * Show the plain form where no script will ever replace it.
411     *
412     * @return void
413     */
414    public static function print_noscript_style() {
415        echo '<noscript><style>.jetpack-comments{visibility:visible!important}</style></noscript>';
416    }
417
418    /**
419     * Enqueue the bundle and hand it the settings for this form.
420     *
421     * @param array $args Comment form arguments.
422     * @return void
423     */
424    public function enqueue_assets( $args = array() ) {
425        $this->register_assets();
426
427        if ( ! $this->settings_printed ) {
428            $strings = array(
429                'reply'               => _x( 'Reply', 'verb', 'jetpack-comments' ),
430                'commentLabel'        => _x( 'Comment', 'noun', 'jetpack-comments' ),
431                'replyLabel'          => _x( 'Reply', 'noun', 'jetpack-comments' ),
432                /* translators: The empty comment box's placeholder. */
433                'placeholder'         => __( 'Write a comment...', 'jetpack-comments' ),
434                /* translators: The empty reply box's placeholder. */
435                'replyPlaceholder'    => __( 'Write a reply...', 'jetpack-comments' ),
436                'name'                => __( 'Name', 'jetpack-comments' ),
437                'email'               => __( 'Email', 'jetpack-comments' ),
438                'emailHint'           => __( 'Address never made public', 'jetpack-comments' ),
439                'emailHasAccount'     => __( 'That email belongs to a WordPress.com account. Log in with WordPress.com to use it, or enter a different email.', 'jetpack-comments' ),
440                'website'             => __( 'Website (optional)', 'jetpack-comments' ),
441                'intro'               => __( 'Enter your name and email to comment.', 'jetpack-comments' ),
442                'continueAsGuest'     => __( 'Continue as a guest', 'jetpack-comments' ),
443                'postWithoutSaving'   => __( 'No, thanks. I just want to post a comment', 'jetpack-comments' ),
444                'save'                => __( 'Save', 'jetpack-comments' ),
445                'saveDetails'         => __( 'Save my name, email, and website in this browser for the next time I comment.', 'jetpack-comments' ),
446                'close'               => __( 'Close', 'jetpack-comments' ),
447                'manageSubscriptions' => __( 'Manage subscription', 'jetpack-comments' ),
448                'mustLogIn'           => __( 'You must be logged in to post a comment.', 'jetpack-comments' ),
449                'logIn'               => __( 'Log in', 'jetpack-comments' ),
450                'logInWithWordPress'  => __( 'Log in with WordPress.com', 'jetpack-comments' ),
451                'logOut'              => __( 'Log out', 'jetpack-comments' ),
452                'change'              => _x( 'Change', 'edit the name and email a comment posts under', 'jetpack-comments' ),
453                'editProfile'         => __( 'Edit profile', 'jetpack-comments' ),
454                'addYourName'         => __( 'Add your name', 'jetpack-comments' ),
455                'cancel'              => __( 'Cancel', 'jetpack-comments' ),
456                'signInFailed'        => __( 'We could not sign you in. Please try again.', 'jetpack-comments' ),
457                'tooLong'             => __( 'This comment is too long to post. Shorten it to send it.', 'jetpack-comments' ),
458                'signInRateLimited'   => __( 'Too many sign-in attempts. Please wait a moment and try again.', 'jetpack-comments' ),
459            );
460
461            /**
462             * Filter the copy the comment form renders.
463             *
464             * @since 0.1.0
465             *
466             * @param array $strings Keyed by the name the app reads.
467             * @param array $args    Comment form arguments.
468             */
469            $strings = apply_filters( 'jetpack_comments_strings', $strings, $args );
470            $style   = wp_styles()->query( self::HANDLE );
471
472            // Where a reader manages subscriptions to this site, which sends a WordPress.com account
473            // to the Reader and anyone else to the email portal. Only where the Newsletter offers them
474            // on this form; Simple stores an option's "off" as an empty string, Jetpack as 0.
475            $offered = false;
476            foreach ( array( 'stb_enabled', 'stc_enabled' ) as $option ) {
477                $offered = $offered || ! in_array( get_option( $option, 1 ), array( '', '0', 0 ), true );
478            }
479
480            $manage = '';
481            if ( $offered && ( function_exists( 'subscription_comment_form' ) || class_exists( 'Jetpack_Subscriptions' ) ) ) {
482                $manage = 'https://subscribe.wordpress.com/?site=' . rawurlencode( (string) wp_parse_url( home_url(), PHP_URL_HOST ) );
483            }
484
485            // Everything the app needs that only PHP knows.
486            $settings = array_merge(
487                array(
488                    'version'                => Comments::PACKAGE_VERSION,
489                    // The dialog's shadow root links it again; page styles stop at that boundary.
490                    // Decoded: WordPress.com's static-file filter joins its query with &amp;.
491                    'styleUrl'               => $style ? html_entity_decode( (string) add_query_arg( 'ver', $style->ver, $style->src ), ENT_QUOTES ) : '',
492                    'requireNameEmail'       => (bool) get_option( 'require_name_email' ),
493                    'mustLogIn'              => (bool) get_option( 'comment_registration' ) && ! is_user_logged_in(),
494                    'site'                   => array(
495                        'name'    => get_bloginfo( 'name' ),
496                        'iconUrl' => (string) get_site_icon_url( 64 ),
497                    ),
498                    'manageSubscriptionsUrl' => $manage,
499                    'tracks'                 => Tracks::is_enabled() ? array( 'platform' => Tracks::platform() ) : null,
500                    'strings'                => $strings,
501                ),
502                Block_Editor::settings(),
503                Identity::settings()
504            );
505
506            // The nonce is added by script, so a bot posting the form's fields has none. Inline and ES5, so
507            // a reader whose bundle never runs gets it too, and the plain form once the page has loaded.
508            // Added again on DOMContentLoaded, for an optimizer that moves this script above the form.
509            $fallback = sprintf(
510                '(function(){function n(){Array.prototype.forEach.call(document.forms,function(f){var i;if(f.querySelector(".jetpack-comments")&&!f.querySelector(\'[name="%1$s"]\')){i=document.createElement("input");i.type="hidden";i.name="%1$s";i.value=%2$s;f.appendChild(i);}});}n();document.addEventListener("DOMContentLoaded",n);})();'
511                . 'window.addEventListener("load",function(){Array.prototype.forEach.call(document.querySelectorAll(".jetpack-comments"),function(e){if(!/\\bis-(mounted|plain)\\b/.test(e.className)){e.className+=" is-plain";}});});',
512                self::NONCE_NAME,
513                wp_json_encode( wp_create_nonce( self::NONCE_ACTION ), JSON_HEX_TAG | JSON_HEX_AMP )
514            );
515
516            wp_add_inline_script(
517                self::HANDLE,
518                'window.JetpackComments = ' . wp_json_encode( $settings, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';' . $fallback,
519                'before'
520            );
521            $this->settings_printed = true;
522        }
523
524        Assets::enqueue_script( self::HANDLE );
525        wp_enqueue_style( self::HANDLE );
526    }
527
528    /**
529     * Require a comment to arrive with the nonce the script adds to the form, or a logged-in reader's from this site's own origin.
530     *
531     * A logged-out reader's is the same string for everybody, which a page cache serves
532     * past its expiry, so for them it only has to be there: a bot posting the form's fields has none.
533     *
534     * @param int $comment_post_id The post being commented on.
535     * @return void
536     */
537    public function verify_nonce( $comment_post_id = 0 ) {
538        if ( ! self::enabled_for_post_type( $comment_post_id ) ) {
539            return;
540        }
541
542        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- this is the nonce check.
543        $nonce = isset( $_POST[ self::NONCE_NAME ] ) ? sanitize_text_field( wp_unslash( $_POST[ self::NONCE_NAME ] ) ) : '';
544
545        if ( ! is_user_logged_in() && '' !== $nonce ) {
546            return;
547        }
548
549        if ( wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) {
550            return;
551        }
552
553        // A forged post carries the other site's Origin, so this site's own covers a nonce that outlived
554        // its day or its login. Logged in only: a bot can send any Origin it likes.
555        if ( is_user_logged_in() && ! empty( $_SERVER['HTTP_ORIGIN'] ) && Checkpoint::is_same_site_request() ) {
556            return;
557        }
558
559        wp_die(
560            esc_html__( 'Sorry, this comment could not be posted. Go back and try again.', 'jetpack-comments' ),
561            esc_html__( 'Comment Submission Failure', 'jetpack-comments' ),
562            array(
563                'response'  => 403,
564                'back_link' => true,
565            )
566        );
567    }
568}