Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 384
0.00% covered (danger)
0.00%
0 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
Verbum_Comments
0.00% covered (danger)
0.00%
0 / 380
0.00% covered (danger)
0.00%
0 / 18
10506
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
12
 get_form_action
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 verbum_render_element
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 enqueue_assets
0.00% covered (danger)
0.00%
0 / 155
0.00% covered (danger)
0.00%
0 / 1
600
 comment_form_defaults
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 comment_reply_link
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
6
 comment_form_default_fields
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 clear_fb_cookies
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 verify_facebook_identity
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 allow_logged_out_user_to_comment_as_external
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 verify_external_account
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 check_comment_allowed
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
90
 add_verbum_meta_data
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
342
 hidden_fields
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 should_load_gutenberg_comments
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 should_show_subscription_modal
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 subscription_modal_status
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 add_jetpack_script_data
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * Plugin Name: Verbum Comments Experience
4 * Description: Preact app for commenting on WordPress.com sites
5 * Author: Vertex
6 * Text Domain: jetpack-mu-wpcom
7 *
8 * @package automattic/jetpack-mu-wpcom
9 */
10
11namespace Automattic\Jetpack;
12
13use WP_Error;
14
15require_once __DIR__ . '/assets/class-wpcom-rest-api-v2-verbum-auth.php';
16require_once __DIR__ . '/assets/class-wpcom-rest-api-v2-verbum-oembed.php';
17require_once __DIR__ . '/assets/class-verbum-gutenberg-editor.php';
18require_once __DIR__ . '/assets/class-verbum-block-utils.php';
19
20/**
21 * Verbum Comments Experience
22 *
23 * This file loads the Verbum Comment user experience on WordPress.com and Jetpack sites.
24 *
25 * @phan-constructor-used-for-side-effects
26 */
27class Verbum_Comments {
28    /**
29     * Internal reference for the current blog id.
30     *
31     * @var int
32     */
33    public $blog_id;
34
35    /**
36     * Comment forms can appear anywhere (page, post, query loop, etc), there is no reliable way to determine if there are comments on the page,
37     * So we hook into `comment_form_before` and set this flag to true when a comment form is found.
38     *
39     * @var bool
40     */
41    public $should_enqueue_assets = false;
42
43    /**
44     * Class constructor
45     */
46    public function __construct() {
47        $this->blog_id = get_current_blog_id();
48
49        // Jetpack loads the app via an iframe, so we need to get the blog id from the query string.
50        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
51        if ( isset( $_GET['blogid'] ) ) {
52            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
53            $this->blog_id = intval( $_GET['blogid'] );
54        }
55
56        add_action(
57            'comment_form_before',
58            function () {
59                $this->should_enqueue_assets = true;
60            }
61        );
62
63        // Selfishly remove everything from the existing comment form
64        add_filter( 'comment_form_field_comment', '__return_false', 11 );
65        add_filter( 'comment_form_logged_in', '__return_empty_string' );
66        add_filter( 'comment_form_defaults', array( $this, 'comment_form_defaults' ), 20 );
67        remove_action( 'comment_form', 'subscription_comment_form' );
68        remove_all_filters( 'comment_form_default_fields' );
69        add_filter( 'comment_form_default_fields', array( $this, 'comment_form_default_fields' ) );
70        add_action( 'clear_auth_cookie', array( $this, 'clear_fb_cookies' ) );
71
72        // Fix comment reply link when `comment_registration` is required.
73        add_filter( 'comment_reply_link', array( $this, 'comment_reply_link' ), 10, 4 );
74
75        // Add Verbum.
76        add_action( 'comment_form_must_log_in_after', array( $this, 'verbum_render_element' ) );
77        add_filter( 'comment_form_submit_field', array( $this, 'verbum_render_element' ) );
78        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
79
80        // Do things before the comment is accepted.
81        add_action( 'pre_comment_on_post', array( $this, 'check_comment_allowed' ), 10, 1 );
82        add_action( 'pre_comment_on_post', array( $this, 'allow_logged_out_user_to_comment_as_external' ), 100 ); // Set priority high to run after check to make sure they are logged in to the external service.
83        add_filter( 'preprocess_comment', array( $this, 'verify_external_account' ), 0 );
84
85        // After the comment is saved, we add meta data to the comment.
86        add_action( 'comment_post', array( $this, 'add_verbum_meta_data' ) );
87
88        // Load the Gutenberg editor for comments.
89        if (
90            $this->should_load_gutenberg_comments()
91        ) {
92            new \Verbum_Gutenberg_Editor();
93        }
94
95        // Filter to ensure JetpackScriptData.site.host and is_wpcom_platform is set, to ensure Jetpack blocks work as expected via Verbum Comments.
96        add_filter( 'jetpack_public_js_script_data', array( $this, 'add_jetpack_script_data' ), 10, 1 );
97    }
98
99    /**
100     * Get the comment form action url
101     */
102    public function get_form_action() {
103        return is_jetpack_comments() ?
104            esc_url_raw( http() . '://' . JETPACK_SERVER__DOMAIN . '/jetpack-comment/' ) : site_url( '/wp-comments-post.php' );
105    }
106
107    /**
108     * Load the div where Verbum app is rendered.
109     */
110    public function verbum_render_element() {
111        $color_scheme = get_blog_option( $this->blog_id, 'jetpack_comment_form_color_scheme' );
112        $comment_url  = $this->get_form_action();
113
114        if ( ! $color_scheme ) {
115            // Default to transparent because it is more adaptable than white or dark.
116            $color_scheme = 'transparent';
117        }
118
119        $verbum = '<div class="comment-form__verbum ' . $color_scheme . '"></div>' . $this->hidden_fields();
120
121        // If the blog requires login, Verbum need to be wrapped in a <form> to work.
122        // Verbum is given `mustLogIn` to handle the login flow.
123        if ( get_option( 'comment_registration' ) && ! is_user_logged_in() ) {
124            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
125            echo "<form action=\"$comment_url\" method=\"post\" id=\"commentform\" class=\"comment-form\">$verbum</form>";
126        } else {
127            return $verbum;
128        }
129    }
130
131    /**
132     * Enqueue Assets
133     */
134    public function enqueue_assets() {
135        if ( ! \Verbum_Block_Utils::should_show_verbum_comments() && ! $this->should_enqueue_assets ) {
136            return;
137        }
138
139        $connect_url      = site_url( '/public.api/connect/?action=request' );
140        $primary_redirect = get_primary_redirect();
141
142        if ( strpos( $primary_redirect, '.wordpress.com' ) === false ) {
143            $connect_url = add_query_arg( 'domain', $primary_redirect, $connect_url );
144        } else {
145            $connect_url = add_query_arg( 'from_comments', 'yes', $connect_url );
146        }
147
148        // Enqueue styles and scripts
149        Assets::register_script(
150            'verbum',
151            '../../build/verbum-comments/verbum-comments.js',
152            __FILE__,
153            array(
154                'strategy'  => 'defer',
155                'in_footer' => true,
156            )
157        );
158
159        wp_enqueue_script( 'wp-i18n' );
160
161        wp_enqueue_style( 'verbum' );
162        \WP_Enqueue_Dynamic_Script::enqueue_script( 'verbum' );
163
164        // Enqueue settings separately since the main script is dynamic.
165        // We need the VerbumComments object to be available before the main script is loaded.
166        wp_register_script(
167            'verbum-settings',
168            false,
169            array(),
170            null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- No script, so no version needed.
171            array(
172                'strategy'  => 'defer',
173                'in_footer' => true,
174            )
175        );
176
177        $blog_details    = get_blog_details( $this->blog_id );
178        $is_blog_atomic  = is_blog_atomic( $blog_details );
179        $is_blog_jetpack = is_blog_jetpack( $blog_details );
180
181        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
182        $subscribe_to_blog = isset( $_GET['stb_enabled'] ) ? boolval( $_GET['stb_enabled'] ) : false;
183        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
184        $subscribe_to_comment = isset( $_GET['stc_enabled'] ) ? boolval( $_GET['stc_enabled'] ) : false;
185
186        // If it is simple, we set it to true. Simple sites return inconsistent results.
187        if ( ! $is_blog_atomic && ! $is_blog_jetpack ) {
188            $subscribe_to_blog    = true;
189            $subscribe_to_comment = true;
190        }
191
192        // Jetpack Comments client side logged in user data
193        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
194        $__get                        = stripslashes_deep( $_GET );
195        $email_hash                   = isset( $__get['hc_useremail'] ) && is_string( $__get['hc_useremail'] ) ? $__get['hc_useremail'] : '';
196        $jetpack_username             = isset( $__get['hc_username'] ) && is_string( $__get['hc_username'] ) ? $__get['hc_username'] : '';
197        $jetpack_user_id              = isset( $__get['hc_userid'] ) && is_numeric( $__get['hc_userid'] ) ? (int) $__get['hc_userid'] : 0;
198        $jetpack_signature            = isset( $__get['sig'] ) && is_string( $__get['sig'] ) ? $__get['sig'] : '';
199        $iframe_unique_id             = isset( $__get['iframe_unique_id'] ) && is_numeric( $__get['iframe_unique_id'] ) ? (int) $__get['iframe_unique_id'] : 0;
200        list( $jetpack_avatar )       = wpcom_get_avatar_url( "$email_hash@md5.gravatar.com" );
201        $comment_registration_enabled = boolval( get_blog_option( $this->blog_id, 'comment_registration' ) );
202        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
203        $post_id = isset( $_GET['postid'] ) ? intval( $_GET['postid'] ) : get_queried_object_id();
204        $locale  = get_locale();
205
206        $css_mtime        = filemtime( ABSPATH . '/widgets.wp.com/verbum-block-editor/block-editor.css' );
207        $js_mtime         = filemtime( ABSPATH . '/widgets.wp.com/verbum-block-editor/block-editor.min.js' );
208        $vbe_cache_buster = max( $js_mtime, $css_mtime );
209        $color_scheme     = get_blog_option( $this->blog_id, 'jetpack_comment_form_color_scheme' );
210
211        // Only users with unfiltered_html get this signed nonce, the closest signed proxy for admin.
212        $retirement_notice_url = '';
213        if ( ! $is_blog_atomic && is_jetpack_comments() && ! empty( $__get['_wp_unfiltered_html_comment'] ) ) {
214            $retirement_notice_url = esc_url_raw( get_admin_url( $this->blog_id, 'plugins.php' ) );
215        }
216
217        $hovercard_i18n = array(
218            'Edit your profile â†’'      => __( 'Edit your profile â†’', 'jetpack-mu-wpcom' ),
219            'View profile â†’'           => __( 'View profile â†’', 'jetpack-mu-wpcom' ),
220            'Contact'                  => __( 'Contact', 'jetpack-mu-wpcom' ),
221            'Send money'               => __( 'Send money', 'jetpack-mu-wpcom' ),
222            'Gravatar not found.'      => __( 'Gravatar not found.', 'jetpack-mu-wpcom' ),
223            'This profile is private.' => __( 'This profile is private.', 'jetpack-mu-wpcom' ),
224            'Too Many Requests.'       => __( 'Too Many Requests.', 'jetpack-mu-wpcom' ),
225            'Internal Server Error.'   => __( 'Internal Server Error.', 'jetpack-mu-wpcom' ),
226            'Sorry, we are unable to load this Gravatar profile.' => __( 'Sorry, we are unable to load this Gravatar profile.', 'jetpack-mu-wpcom' ),
227        );
228
229        wp_add_inline_script(
230            'verbum-settings',
231            'window.VerbumComments = ' . wp_json_encode(
232                array(
233                    'Log in or provide your name and email to leave a reply.' => __( 'Log in or provide your name and email to leave a reply.', 'jetpack-mu-wpcom' ),
234                    'Log in or provide your name and email to leave a comment.' => __( 'Log in or provide your name and email to leave a comment.', 'jetpack-mu-wpcom' ),
235                    'Receive web and mobile notifications for posts on this site.' => __( 'Receive web and mobile notifications for posts on this site.', 'jetpack-mu-wpcom' ),
236                    'Name'                               => __( 'Name', 'jetpack-mu-wpcom' ),
237                    'Email (address never made public)'  => __( 'Email (address never made public)', 'jetpack-mu-wpcom' ),
238                    'Website (optional)'                 => __( 'Website (optional)', 'jetpack-mu-wpcom' ),
239                    'Leave a reply. (log in optional)'   => __( 'Leave a reply. (log in optional)', 'jetpack-mu-wpcom' ),
240                    'Leave a comment. (log in optional)' => __( 'Leave a comment. (log in optional)', 'jetpack-mu-wpcom' ),
241                    'Log in to leave a reply.'           => __( 'Log in to leave a reply.', 'jetpack-mu-wpcom' ),
242                    'Log in to leave a comment.'         => __( 'Log in to leave a comment.', 'jetpack-mu-wpcom' ),
243                    /* translators: %s is the name of the provider (WordPress, Facebook, Twitter) */
244                    'Logged in via %s'                   => __( 'Logged in via %s', 'jetpack-mu-wpcom' ),
245                    'Log out'                            => __( 'Log out', 'jetpack-mu-wpcom' ),
246                    'Your browser is blocking the cookies needed to log in and comment here. Allow cookies in your privacy settings, then reload the page.' => __( 'Your browser is blocking the cookies needed to log in and comment here. Allow cookies in your privacy settings, then reload the page.', 'jetpack-mu-wpcom' ),
247                    'Email'                              => __( 'Email', 'jetpack-mu-wpcom' ),
248                    '(Address never made public)'        => __( '(Address never made public)', 'jetpack-mu-wpcom'), // phpcs:ignore PEAR.Functions.FunctionCallSignature.SpaceBeforeCloseBracket
249                    'Instantly'                          => __( 'Instantly', 'jetpack-mu-wpcom' ),
250                    'Daily'                              => __( 'Daily', 'jetpack-mu-wpcom' ),
251                    'Reply'                              => __( 'Reply', 'jetpack-mu-wpcom' ),
252                    'Comment'                            => __( 'Comment', 'jetpack-mu-wpcom' ),
253                    'WordPress'                          => __( 'WordPress', 'jetpack-mu-wpcom' ),
254                    'Weekly'                             => __( 'Weekly', 'jetpack-mu-wpcom' ),
255                    'Notify me of new posts'             => __( 'Notify me of new posts', 'jetpack-mu-wpcom' ),
256                    'Email me new posts'                 => __( 'Email me new posts', 'jetpack-mu-wpcom' ),
257                    'Email me new comments'              => __( 'Email me new comments', 'jetpack-mu-wpcom' ),
258                    'Cancel'                             => __( 'Cancel', 'jetpack-mu-wpcom' ),
259                    'Write a comment...'                 => __( 'Write a comment...', 'jetpack-mu-wpcom' ),
260                    'Write a reply...'                   => __( 'Write a reply...', 'jetpack-mu-wpcom' ),
261                    'Website'                            => __( 'Website', 'jetpack-mu-wpcom' ),
262                    'Optional'                           => __( 'Optional', 'jetpack-mu-wpcom' ),
263                    /* translators: Success message of a modal when user subscribes */
264                    'We\'ll keep you in the loop!'       => __( 'We\'ll keep you in the loop!', 'jetpack-mu-wpcom' ),
265                    'Loading your comment...'            => __( 'Loading your comment...', 'jetpack-mu-wpcom' ),
266                    /* translators: %s is the name of the site */
267                    'Discover more from'                 => sprintf( __( 'Discover more from %s', 'jetpack-mu-wpcom' ), html_entity_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ),
268                    'Subscribe now to keep reading and get access to the full archive.' => __( 'Subscribe now to keep reading and get access to the full archive.', 'jetpack-mu-wpcom' ),
269                    'Continue reading'                   => __( 'Continue reading', 'jetpack-mu-wpcom' ),
270                    'Never miss a beat!'                 => __( 'Never miss a beat!', 'jetpack-mu-wpcom' ),
271                    'Interested in getting blog post updates? Simply click the button below to stay in the loop!' => __( 'Interested in getting blog post updates? Simply click the button below to stay in the loop!', 'jetpack-mu-wpcom' ),
272                    'Enter your email address'           => __( 'Enter your email address', 'jetpack-mu-wpcom' ),
273                    'Subscribe'                          => __( 'Subscribe', 'jetpack-mu-wpcom' ),
274                    'Comment sent successfully'          => __( 'Comment sent successfully', 'jetpack-mu-wpcom' ),
275                    'This comment form is being retired. Update Jetpack to keep comments working on your site.' => __( 'This comment form is being retired. Update Jetpack to keep comments working on your site.', 'jetpack-mu-wpcom' ),
276                    'Update Jetpack'                     => __( 'Update Jetpack', 'jetpack-mu-wpcom' ),
277                    'Save my name, email, and website in this browser for the next time I comment.' => __( 'Save my name, email, and website in this browser for the next time I comment.', 'jetpack-mu-wpcom' ),
278                    'hovercardi18n'                      => $hovercard_i18n,
279                    'siteId'                             => $this->blog_id,
280                    'postId'                             => $post_id,
281                    'mustLogIn'                          => $comment_registration_enabled && ! is_user_logged_in(),
282                    'requireNameEmail'                   => boolval( get_blog_option( $this->blog_id, 'require_name_email' ) ),
283                    'commentRegistration'                => $comment_registration_enabled,
284                    'connectURL'                         => $connect_url,
285                    'logoutURL'                          => html_entity_decode( wp_logout_url(), ENT_COMPAT ),
286                    'homeURL'                            => home_url( '/' ),
287                    'subscribeToBlog'                    => $subscribe_to_blog,
288                    'subscribeToComment'                 => $subscribe_to_comment,
289                    'isJetpackCommentsLoggedIn'          => is_jetpack_comments() && is_jetpack_comments_user_logged_in(),
290                    'jetpackUsername'                    => $jetpack_username,
291                    'jetpackUserId'                      => $jetpack_user_id,
292                    'jetpackSignature'                   => $jetpack_signature,
293                    'jetpackAvatar'                      => $jetpack_avatar,
294                    'enableBlocks'                       => boolval( $this->should_load_gutenberg_comments() ),
295                    'enableSubscriptionModal'            => boolval( $this->should_show_subscription_modal() ),
296                    'currentLocale'                      => $locale,
297                    'isJetpackComments'                  => is_jetpack_comments(),
298                    'allowedBlocks'                      => \Verbum_Block_Utils::get_allowed_blocks(),
299                    'embedNonce'                         => wp_create_nonce( 'embed_nonce' ),
300                    'verbumBundleUrl'                    => plugins_url( 'dist/index.js', __FILE__ ),
301                    'isRTL'                              => is_rtl(),
302                    'vbeCacheBuster'                     => $vbe_cache_buster,
303                    'iframeUniqueId'                     => $iframe_unique_id,
304                    'colorScheme'                        => $color_scheme,
305                    'retirementNoticeUrl'                => $retirement_notice_url,
306                ),
307                JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
308            ),
309            'before'
310        );
311
312        wp_enqueue_script( 'verbum-settings' );
313
314        Assets::register_script(
315            'verbum-dynamic-loader',
316            '../../build/verbum-comments/assets/dynamic-loader.js',
317            __FILE__,
318            array(
319                'strategy'  => 'defer',
320                'in_footer' => true,
321                'enqueue'   => true,
322            )
323        );
324    }
325
326    /**
327     * Remove some of the default comment_form args because they are not needed.
328     *
329     * @param  array $args - The default comment form arguments.
330     */
331    public function comment_form_defaults( $args ) {
332        $title_reply_default = __( 'Leave a comment', 'jetpack-mu-wpcom' );
333        $title_reply         = get_option( 'highlander_comment_form_prompt', $title_reply_default );
334
335        if ( $title_reply === 'Leave a comment' || empty( $title_reply ) ) {
336            $title_reply = $title_reply_default;
337        }
338
339        return array_merge(
340            $args,
341            array(
342                'comment_field'        => '',
343                'must_log_in'          => '',
344                'logged_in_as'         => '',
345                'comment_notes_before' => '',
346                'comment_notes_after'  => '',
347                'title_reply'          => $title_reply,
348                /* translators: % is the original posters name */
349                'title_reply_to'       => __( 'Leave a reply to %s', 'jetpack-mu-wpcom' ),
350                'cancel_reply_link'    => __( 'Cancel reply', 'jetpack-mu-wpcom' ),
351                'action'               => $this->get_form_action(),
352            )
353        );
354    }
355
356    /**
357     * Set comment reply link.
358     * This is to fix the reply link when comment registration is required.
359     *
360     * @param  string $reply_link - HTML for reply link.
361     * @param  array  $args - Default options for reply link.
362     * @param  object $comment - Comment being replied to.
363     * @param  object $post - PostID or WP_Post object comment is going to be displayed on.
364     */
365    public function comment_reply_link( $reply_link, $args, $comment, $post ) {
366        // This is only necessary if comment_registration is required to post comments
367        if ( ! get_option( 'comment_registration' ) ) {
368            return $reply_link;
369        }
370
371        $comment    = get_comment( $comment );
372        $respond_id = esc_attr( $args['respond_id'] );
373        $add_below  = esc_attr( $args['add_below'] );
374        /* This is to accommodate some themes that add an SVG to the Reply link like twenty-seventeen. */
375        $reply_text  = wp_kses(
376            $args['reply_text'],
377            array(
378                'svg' => array(
379                    'class'           => true,
380                    'aria-hidden'     => true,
381                    'aria-labelledby' => true,
382                    'role'            => true,
383                    'xmlns'           => true,
384                    'width'           => true,
385                    'height'          => true,
386                    'viewbox'         => true,
387                ),
388                'use' => array(
389                    'href'       => true,
390                    'xlink:href' => true,
391                ),
392            )
393        );
394        $before_link = wp_kses( $args['before'], wp_kses_allowed_html( 'post' ) );
395        $after_link  = wp_kses( $args['after'], wp_kses_allowed_html( 'post' ) );
396
397        $reply_url = esc_url( add_query_arg( 'replytocom', $comment->comment_ID . '#' . $respond_id ) );
398
399        $link = <<<HTML
400            $before_link
401            <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>
402            $after_link
403HTML;
404
405        return $link;
406    }
407
408    /**
409     * Loop through all available fields and remove them.
410     *
411     * @param  array $fields - Default comment fields.
412     * @return array $fields with no HTML.
413     */
414    public function comment_form_default_fields( $fields ) {
415        foreach ( $fields as $field => $html ) {
416            remove_all_filters( "comment_form_field_{$field}" );
417            add_filter( "comment_form_field_{$field}", '__return_false', 100 );
418        }
419
420        return $fields;
421    }
422
423    /**
424     * Clear FB comments on logout. wp-login.php doesn't clear these by default.
425     *
426     * @return void
427     */
428    public function clear_fb_cookies() {
429        setcookie( 'wpc_fbc', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, true );
430    }
431
432    /**
433     * Check Facebook token and return the user data.
434     */
435    public static function verify_facebook_identity() {
436        $data = isset( $_COOKIE['wpc_fbc'] ) ? wp_parse_args( sanitize_text_field( wp_unslash( $_COOKIE['wpc_fbc'] ) ) ) : array();
437
438        if ( empty( $data['access_token'] ) ) {
439            return new WP_Error( 'facebook', __( 'Error: your Facebook login has expired.', 'jetpack-mu-wpcom' ) );
440        }
441
442        // Make a new request using the access token we were given.
443        $request = wp_remote_get( 'https://graph.facebook.com/v6.0/me?fields=name,email,picture,id&access_token=' . rawurlencode( $data['access_token'] ) );
444        if ( 200 !== wp_remote_retrieve_response_code( $request ) ) {
445            return new WP_Error( 'facebook', __( 'Error: your Facebook login has expired.', 'jetpack-mu-wpcom' ) );
446        }
447
448        $body = wp_remote_retrieve_body( $request );
449        $json = json_decode( $body );
450
451        if ( ! $body || ! $json ) {
452            return new WP_Error( 'facebook', __( 'Error: your Facebook login has expired.', 'jetpack-mu-wpcom' ) );
453        }
454
455        return $json;
456    }
457
458    /**
459     * Allows a logged out user to leave a comment as a facebook credentialed user.
460     * Overrides WordPress' core comment_registration option to treat the commenter as "registered" (verified) users.
461     */
462    public function allow_logged_out_user_to_comment_as_external() {
463        $service = isset( $_POST['hc_post_as'] ) ? sanitize_text_field( wp_unslash( $_POST['hc_post_as'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked before saving comment
464
465        if ( $service !== 'facebook' ) {
466            return;
467        }
468
469        add_filter( 'pre_option_comment_registration', '__return_zero' );
470        add_filter( 'pre_option_require_name_email', '__return_zero' );
471    }
472
473    /**
474     * Check if the comment is allowed by verifying the Facebook token.
475     *
476     * @param array $comment_data - The comment data.
477     * @return WP_Error|array The comment data if the comment is allowed, or a WP_Error if not.
478     */
479    public function verify_external_account( $comment_data ) {
480        $service = isset( $_POST['hc_post_as'] ) ? sanitize_text_field( wp_unslash( $_POST['hc_post_as'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked before saving comment
481
482        if ( $service === 'facebook' ) {
483            $fb_comment_data = self::verify_facebook_identity();
484
485            if ( is_wp_error( $fb_comment_data ) ) {
486                wp_die( esc_html( $fb_comment_data->get_error_message() ) );
487            }
488
489            $comment_data['highlander'] = 'facebook';
490        }
491
492        return $comment_data;
493    }
494
495    /**
496     * Verify nonce before accepting comment.
497     *
498     * @param int $comment_id The comment ID.
499     * @return void
500     */
501    public function check_comment_allowed( int $comment_id ) {
502        // Don't check if we're using Jetpack Comments.
503        if ( is_jetpack_comments() ) {
504            return;
505        }
506
507        // Check for Highlander Nonce.
508        if ( isset( $_POST['highlander_comment_nonce'] ) ) {
509            $valid_nonce     = false;
510            $current_user_id = get_current_user_id();
511
512            if ( wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['highlander_comment_nonce'] ) ), 'highlander_comment' ) ) {
513                $valid_nonce = true;
514            } elseif ( function_exists( 'wp_set_current_user' ) ) {
515                // There randomly occurs a race condition between the logged in/out state of the user.
516                // Check if their nonce is a logged out nonce.
517                wp_set_current_user( 0 );
518                $valid_nonce = wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['highlander_comment_nonce'] ) ), 'highlander_comment' );
519                wp_set_current_user( $current_user_id );
520            }
521
522            // All good, proceed.
523            if ( $valid_nonce ) {
524                return;
525            }
526
527            // Log the error to Log2Logstash.
528            // Related to https://github.com/Automattic/wp-calypso/issues/99436
529            if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
530                require_once WP_CONTENT_DIR . '/lib/log2logstash/log2logstash.php';
531
532                $headers = getallheaders();
533                $data    = array(
534                    'session_token' => wp_get_session_token(),
535                    'editor_type'   => isset( $_POST['verbum_loaded_editor'] ) ? sanitize_text_field( wp_unslash( $_POST['verbum_loaded_editor'] ) ) : '',
536                    'user_agent'    => sanitize_text_field( $headers['User-Agent'] ?? '' ),
537                    'referrer'      => esc_url_raw( $headers['Referer'] ?? '' ),
538                );
539
540                log2logstash(
541                    array(
542                        'feature'    => 'verbum-comments',
543                        'message'    => 'Pre-comment nonce failed',
544                        'blog_id'    => get_current_blog_id(),
545                        'user_id'    => $current_user_id,
546                        'host'       => sanitize_text_field( $headers['Host'] ?? '' ),
547                        'comment_id' => $comment_id,
548                        'extra'      => wp_json_encode( $data, JSON_UNESCAPED_SLASHES ),
549                    )
550                );
551            }
552        }
553
554        wp_die( esc_html__( 'Sorry, this comment could not be posted.', 'jetpack-mu-wpcom' ) );
555    }
556
557    /**
558     * Add all our custom fields to the comment meta after it is saved.
559     *
560     * @param int $comment_id The comment ID.
561     */
562    public function add_verbum_meta_data( $comment_id ) {
563        $comment_meta = array();
564        // phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce checked before saving comment
565        $allowed_subscription_modal_statuses = array( 'showed', 'hidden_is_blog_member', 'hidden_jetpack', 'hidden_disabled', 'hidden_cookies_disabled', 'hidden_subscribe_not_enabled', 'hidden_already_subscribed', 'hidden_views_limit' );
566        $hc_avatar                           = isset( $_POST['hc_avatar'] ) ? esc_url_raw( wp_unslash( $_POST['hc_avatar'] ) ) : '';
567        $hc_userid                           = isset( $_POST['hc_foreign_user_id'] ) ? sanitize_text_field( wp_unslash( $_POST['hc_foreign_user_id'] ) ) : '';
568        $service                             = isset( $_POST['hc_post_as'] ) ? sanitize_text_field( wp_unslash( $_POST['hc_post_as'] ) ) : '';
569        $verbum_loaded_editor                = isset( $_POST['verbum_loaded_editor'] ) ? sanitize_text_field( wp_unslash( $_POST['verbum_loaded_editor'] ) ) : '';
570        $verbum_subscription_modal_show      = isset( $_POST['verbum_show_subscription_modal'] ) && in_array( $_POST['verbum_show_subscription_modal'], $allowed_subscription_modal_statuses, true ) ? sanitize_text_field( wp_unslash( $_POST['verbum_show_subscription_modal'] ) ) : '';
571        // phpcs:enable WordPress.Security.NonceVerification.Missing -- nonce checked before saving comment
572        $allowed_comments_sources = array( 'gutenberg', 'textarea', 'textarea-slow-connection' );
573        if ( in_array( $verbum_loaded_editor, $allowed_comments_sources, true ) ) {
574            bump_stats_extras( 'verbum-comment-editor', $verbum_loaded_editor );
575        }
576        if ( $verbum_subscription_modal_show ) {
577            bump_stats_extras( 'verbum-subscription-modal', $verbum_subscription_modal_show );
578        }
579        switch ( $service ) {
580            case 'facebook':
581                $comment_meta['hc_post_as']         = 'facebook';
582                $comment_meta['hc_avatar']          = $hc_avatar;
583                $comment_meta['hc_foreign_user_id'] = $hc_userid;
584
585                bump_stats_extras( 'verbum-comment-posted', 'facebook' );
586                break;
587
588            case 'wordpress': // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText
589                if ( 'wpcom' === wpcom_blog_site_id_label() ) {
590                    do_action( 'highlander_wpcom_post_comment_bump_stat', $comment_id );
591                }
592                bump_stats_extras( 'verbum-comment-posted', 'wordpress' ); // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText
593                break;
594
595            case 'jetpack':
596                if ( is_jetpack_comments() && is_jetpack_comments_user_logged_in() ) {
597                    $comment_meta['hc_post_as']         = 'jetpack';
598                    $comment_meta['hc_avatar']          = check_and_return_post_string( 'hc_avatar' );
599                    $comment_meta['hc_foreign_user_id'] = check_and_return_post_string( 'hc_userid' );
600
601                    bump_stats_extras( 'verbum-comment-posted', 'jetpack' );
602                } else {
603                    jetpack_comments_die( 'JPC_HIGHLANDER_ADD_COMMENT_META' );
604                }
605
606                break;
607            default:
608                if ( is_user_logged_in() ) {
609                    bump_stats_extras( 'verbum-comment-posted', 'guest-logged-in' );
610                } else {
611                    bump_stats_extras( 'verbum-comment-posted', 'guest' );
612                }
613                break;
614        }
615
616        foreach ( $comment_meta as $key => $value ) {
617            add_comment_meta( $comment_id, $key, $value, true );
618        }
619    }
620
621    /**
622     * Get the hidden fields for the comment form.
623     */
624    public function hidden_fields() {
625        // Ironically, get_queried_post_id doesn't work inside query loop.
626        // See: https://github.com/Automattic/wp-calypso/issues/98136
627        $queried_post    = get_post();
628        $queried_post_id = $queried_post ? $queried_post->ID : 0;
629        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
630        $post_id = isset( $_GET['postid'] ) ? intval( $_GET['postid'] ) : $queried_post_id;
631        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
632        $is_current_user_subscribed = isset( $_GET['is_current_user_subscribed'] ) ? intval( $_GET['is_current_user_subscribed'] ) : 0;
633        $nonce                      = wp_create_nonce( 'highlander_comment' );
634        $hidden_fields              = get_comment_id_fields( $post_id ) . '
635            <input type="hidden" name="highlander_comment_nonce" id="highlander_comment_nonce" value="' . esc_attr( $nonce ) . '" />
636            <input type="hidden" name="verbum_show_subscription_modal" value="' . $this->subscription_modal_status() . '" />';
637
638        if ( is_jetpack_comments() ) {
639            $hidden_fields .= '
640                <input type="hidden" name="jetpack-remote-blogid" value="' . $this->blog_id . '" />
641                <input type="hidden" name="jetpack-remote-action" value="comment-post" />
642                <input type="hidden" name="is_current_user_subscribed" value="' . $is_current_user_subscribed . '" />';
643
644            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
645            $jetpack_nonce = isset( $_GET['jetpack_comments_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['jetpack_comments_nonce'] ) ) : false;
646            if ( $jetpack_nonce ) {
647                $hidden_fields .= '<input type="hidden" name="jetpack_comments_nonce" value="' . esc_attr( $jetpack_nonce ) . '" />';
648            }
649        }
650
651        return '<div class="verbum-form-meta">' . $hidden_fields . '</div>';
652    }
653
654    /***
655     * Check if we should load the Gutenberg comments.
656     *
657     * Block should be carefully loaded to avoid Forums, P2, etc.
658     */
659    public function should_load_gutenberg_comments() {
660        // Don't load when jetpack or atomic for now, it does not look cool on dark themes.
661        $is_jetpack_site = 522232 === get_current_blog_id();
662        if ( $is_jetpack_site ) {
663            return false;
664        }
665
666        // Blocks in comments have been disabled on a simple site
667        if ( empty( get_option( 'enable_blocks_comments', true ) ) ) {
668            return false;
669        }
670
671        return true;
672    }
673
674    /**
675     * Check if we should show the subscription modal.
676     */
677    public function should_show_subscription_modal() {
678        $modal_enabled = boolval( get_blog_option( $this->blog_id, 'jetpack_verbum_subscription_modal', true ) );
679
680        $is_jetpack_site = 522232 === get_current_blog_id(); // Disable if verbum is served via 'jetpack.wordpress.com'
681        return ! $is_jetpack_site && ! is_user_member_of_blog( '', $this->blog_id ) && $modal_enabled;
682    }
683
684    /**
685     * Get the status of the subscription modal.
686     */
687    public function subscription_modal_status() {
688        if ( is_user_member_of_blog( '', $this->blog_id ) ) {
689            return 'hidden_is_blog_member';
690        }
691        if ( is_jetpack_comments() ) {
692            return 'hidden_jetpack';
693        }
694        if ( ! get_option( 'jetpack_verbum_subscription_modal', true ) ) {
695            return 'hidden_disabled';
696        }
697        return '';
698    }
699
700    /**
701     * Add Jetpack script data.
702     *
703     * @param array $data - The Jetpack script data.
704     * @return array - The modified Jetpack script data.
705     */
706    public function add_jetpack_script_data( $data ) {
707        if ( \Verbum_Block_Utils::should_show_verbum_comments() ) {
708            if ( ! isset( $data['site']['host'] ) ) {
709                $data['site']['host'] = ( new \Automattic\Jetpack\Status\Host() )->get_known_host_guess();
710            }
711            if ( ! isset( $data['site']['is_wpcom_platform'] ) ) {
712                $data['site']['is_wpcom_platform'] = ( new \Automattic\Jetpack\Status\Host() )->is_wpcom_platform();
713            }
714        }
715        return $data;
716    }
717}