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