Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 378
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 / 374
0.00% covered (danger)
0.00%
0 / 18
9900
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 / 149
0.00% covered (danger)
0.00%
0 / 1
462
 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        $hovercard_i18n = array(
212            'Edit your profile â†’'      => __( 'Edit your profile â†’', 'jetpack-mu-wpcom' ),
213            'View profile â†’'           => __( 'View profile â†’', 'jetpack-mu-wpcom' ),
214            'Contact'                  => __( 'Contact', 'jetpack-mu-wpcom' ),
215            'Send money'               => __( 'Send money', 'jetpack-mu-wpcom' ),
216            'Gravatar not found.'      => __( 'Gravatar not found.', 'jetpack-mu-wpcom' ),
217            'This profile is private.' => __( 'This profile is private.', 'jetpack-mu-wpcom' ),
218            'Too Many Requests.'       => __( 'Too Many Requests.', 'jetpack-mu-wpcom' ),
219            'Internal Server Error.'   => __( 'Internal Server Error.', 'jetpack-mu-wpcom' ),
220            'Sorry, we are unable to load this Gravatar profile.' => __( 'Sorry, we are unable to load this Gravatar profile.', 'jetpack-mu-wpcom' ),
221        );
222
223        wp_add_inline_script(
224            'verbum-settings',
225            'window.VerbumComments = ' . wp_json_encode(
226                array(
227                    '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' ),
228                    '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' ),
229                    'Receive web and mobile notifications for posts on this site.' => __( 'Receive web and mobile notifications for posts on this site.', 'jetpack-mu-wpcom' ),
230                    'Name'                               => __( 'Name', 'jetpack-mu-wpcom' ),
231                    'Email (address never made public)'  => __( 'Email (address never made public)', 'jetpack-mu-wpcom' ),
232                    'Website (optional)'                 => __( 'Website (optional)', 'jetpack-mu-wpcom' ),
233                    'Leave a reply. (log in optional)'   => __( 'Leave a reply. (log in optional)', 'jetpack-mu-wpcom' ),
234                    'Leave a comment. (log in optional)' => __( 'Leave a comment. (log in optional)', 'jetpack-mu-wpcom' ),
235                    'Log in to leave a reply.'           => __( 'Log in to leave a reply.', 'jetpack-mu-wpcom' ),
236                    'Log in to leave a comment.'         => __( 'Log in to leave a comment.', 'jetpack-mu-wpcom' ),
237                    /* translators: %s is the name of the provider (WordPress, Facebook, Twitter) */
238                    'Logged in via %s'                   => __( 'Logged in via %s', 'jetpack-mu-wpcom' ),
239                    'Log out'                            => __( 'Log out', 'jetpack-mu-wpcom' ),
240                    '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' ),
241                    'Email'                              => __( 'Email', 'jetpack-mu-wpcom' ),
242                    '(Address never made public)'        => __( '(Address never made public)', 'jetpack-mu-wpcom'), // phpcs:ignore PEAR.Functions.FunctionCallSignature.SpaceBeforeCloseBracket
243                    'Instantly'                          => __( 'Instantly', 'jetpack-mu-wpcom' ),
244                    'Daily'                              => __( 'Daily', 'jetpack-mu-wpcom' ),
245                    'Reply'                              => __( 'Reply', 'jetpack-mu-wpcom' ),
246                    'Comment'                            => __( 'Comment', 'jetpack-mu-wpcom' ),
247                    'WordPress'                          => __( 'WordPress', 'jetpack-mu-wpcom' ),
248                    'Weekly'                             => __( 'Weekly', 'jetpack-mu-wpcom' ),
249                    'Notify me of new posts'             => __( 'Notify me of new posts', 'jetpack-mu-wpcom' ),
250                    'Email me new posts'                 => __( 'Email me new posts', 'jetpack-mu-wpcom' ),
251                    'Email me new comments'              => __( 'Email me new comments', 'jetpack-mu-wpcom' ),
252                    'Cancel'                             => __( 'Cancel', 'jetpack-mu-wpcom' ),
253                    'Write a comment...'                 => __( 'Write a comment...', 'jetpack-mu-wpcom' ),
254                    'Write a reply...'                   => __( 'Write a reply...', 'jetpack-mu-wpcom' ),
255                    'Website'                            => __( 'Website', 'jetpack-mu-wpcom' ),
256                    'Optional'                           => __( 'Optional', 'jetpack-mu-wpcom' ),
257                    /* translators: Success message of a modal when user subscribes */
258                    'We\'ll keep you in the loop!'       => __( 'We\'ll keep you in the loop!', 'jetpack-mu-wpcom' ),
259                    'Loading your comment...'            => __( 'Loading your comment...', 'jetpack-mu-wpcom' ),
260                    /* translators: %s is the name of the site */
261                    'Discover more from'                 => sprintf( __( 'Discover more from %s', 'jetpack-mu-wpcom' ), html_entity_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ),
262                    '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' ),
263                    'Continue reading'                   => __( 'Continue reading', 'jetpack-mu-wpcom' ),
264                    'Never miss a beat!'                 => __( 'Never miss a beat!', 'jetpack-mu-wpcom' ),
265                    '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' ),
266                    'Enter your email address'           => __( 'Enter your email address', 'jetpack-mu-wpcom' ),
267                    'Subscribe'                          => __( 'Subscribe', 'jetpack-mu-wpcom' ),
268                    'Comment sent successfully'          => __( 'Comment sent successfully', 'jetpack-mu-wpcom' ),
269                    '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' ),
270                    'hovercardi18n'                      => $hovercard_i18n,
271                    'siteId'                             => $this->blog_id,
272                    'postId'                             => $post_id,
273                    'mustLogIn'                          => $comment_registration_enabled && ! is_user_logged_in(),
274                    'requireNameEmail'                   => boolval( get_blog_option( $this->blog_id, 'require_name_email' ) ),
275                    'commentRegistration'                => $comment_registration_enabled,
276                    'connectURL'                         => $connect_url,
277                    'logoutURL'                          => html_entity_decode( wp_logout_url(), ENT_COMPAT ),
278                    'homeURL'                            => home_url( '/' ),
279                    'subscribeToBlog'                    => $subscribe_to_blog,
280                    'subscribeToComment'                 => $subscribe_to_comment,
281                    'isJetpackCommentsLoggedIn'          => is_jetpack_comments() && is_jetpack_comments_user_logged_in(),
282                    'jetpackUsername'                    => $jetpack_username,
283                    'jetpackUserId'                      => $jetpack_user_id,
284                    'jetpackSignature'                   => $jetpack_signature,
285                    'jetpackAvatar'                      => $jetpack_avatar,
286                    'enableBlocks'                       => boolval( $this->should_load_gutenberg_comments() ),
287                    'enableSubscriptionModal'            => boolval( $this->should_show_subscription_modal() ),
288                    'currentLocale'                      => $locale,
289                    'isJetpackComments'                  => is_jetpack_comments(),
290                    'allowedBlocks'                      => \Verbum_Block_Utils::get_allowed_blocks(),
291                    'embedNonce'                         => wp_create_nonce( 'embed_nonce' ),
292                    'verbumBundleUrl'                    => plugins_url( 'dist/index.js', __FILE__ ),
293                    'isRTL'                              => is_rtl(),
294                    'vbeCacheBuster'                     => $vbe_cache_buster,
295                    'iframeUniqueId'                     => $iframe_unique_id,
296                    'colorScheme'                        => $color_scheme,
297                ),
298                JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
299            ),
300            'before'
301        );
302
303        wp_enqueue_script( 'verbum-settings' );
304
305        Assets::register_script(
306            'verbum-dynamic-loader',
307            '../../build/verbum-comments/assets/dynamic-loader.js',
308            __FILE__,
309            array(
310                'strategy'  => 'defer',
311                'in_footer' => true,
312                'enqueue'   => true,
313            )
314        );
315    }
316
317    /**
318     * Remove some of the default comment_form args because they are not needed.
319     *
320     * @param  array $args - The default comment form arguments.
321     */
322    public function comment_form_defaults( $args ) {
323        $title_reply_default = __( 'Leave a comment', 'jetpack-mu-wpcom' );
324        $title_reply         = get_option( 'highlander_comment_form_prompt', $title_reply_default );
325
326        if ( $title_reply === 'Leave a comment' || empty( $title_reply ) ) {
327            $title_reply = $title_reply_default;
328        }
329
330        return array_merge(
331            $args,
332            array(
333                'comment_field'        => '',
334                'must_log_in'          => '',
335                'logged_in_as'         => '',
336                'comment_notes_before' => '',
337                'comment_notes_after'  => '',
338                'title_reply'          => $title_reply,
339                /* translators: % is the original posters name */
340                'title_reply_to'       => __( 'Leave a reply to %s', 'jetpack-mu-wpcom' ),
341                'cancel_reply_link'    => __( 'Cancel reply', 'jetpack-mu-wpcom' ),
342                'action'               => $this->get_form_action(),
343            )
344        );
345    }
346
347    /**
348     * Set comment reply link.
349     * This is to fix the reply link when comment registration is required.
350     *
351     * @param  string $reply_link - HTML for reply link.
352     * @param  array  $args - Default options for reply link.
353     * @param  object $comment - Comment being replied to.
354     * @param  object $post - PostID or WP_Post object comment is going to be displayed on.
355     */
356    public function comment_reply_link( $reply_link, $args, $comment, $post ) {
357        // This is only necessary if comment_registration is required to post comments
358        if ( ! get_option( 'comment_registration' ) ) {
359            return $reply_link;
360        }
361
362        $comment    = get_comment( $comment );
363        $respond_id = esc_attr( $args['respond_id'] );
364        $add_below  = esc_attr( $args['add_below'] );
365        /* This is to accommodate some themes that add an SVG to the Reply link like twenty-seventeen. */
366        $reply_text  = wp_kses(
367            $args['reply_text'],
368            array(
369                'svg' => array(
370                    'class'           => true,
371                    'aria-hidden'     => true,
372                    'aria-labelledby' => true,
373                    'role'            => true,
374                    'xmlns'           => true,
375                    'width'           => true,
376                    'height'          => true,
377                    'viewbox'         => true,
378                ),
379                'use' => array(
380                    'href'       => true,
381                    'xlink:href' => true,
382                ),
383            )
384        );
385        $before_link = wp_kses( $args['before'], wp_kses_allowed_html( 'post' ) );
386        $after_link  = wp_kses( $args['after'], wp_kses_allowed_html( 'post' ) );
387
388        $reply_url = esc_url( add_query_arg( 'replytocom', $comment->comment_ID . '#' . $respond_id ) );
389
390        $link = <<<HTML
391            $before_link
392            <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>
393            $after_link
394HTML;
395
396        return $link;
397    }
398
399    /**
400     * Loop through all available fields and remove them.
401     *
402     * @param  array $fields - Default comment fields.
403     * @return array $fields with no HTML.
404     */
405    public function comment_form_default_fields( $fields ) {
406        foreach ( $fields as $field => $html ) {
407            remove_all_filters( "comment_form_field_{$field}" );
408            add_filter( "comment_form_field_{$field}", '__return_false', 100 );
409        }
410
411        return $fields;
412    }
413
414    /**
415     * Clear FB comments on logout. wp-login.php doesn't clear these by default.
416     *
417     * @return void
418     */
419    public function clear_fb_cookies() {
420        setcookie( 'wpc_fbc', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, true );
421    }
422
423    /**
424     * Check Facebook token and return the user data.
425     */
426    public static function verify_facebook_identity() {
427        $data = isset( $_COOKIE['wpc_fbc'] ) ? wp_parse_args( sanitize_text_field( wp_unslash( $_COOKIE['wpc_fbc'] ) ) ) : array();
428
429        if ( empty( $data['access_token'] ) ) {
430            return new WP_Error( 'facebook', __( 'Error: your Facebook login has expired.', 'jetpack-mu-wpcom' ) );
431        }
432
433        // Make a new request using the access token we were given.
434        $request = wp_remote_get( 'https://graph.facebook.com/v6.0/me?fields=name,email,picture,id&access_token=' . rawurlencode( $data['access_token'] ) );
435        if ( 200 !== wp_remote_retrieve_response_code( $request ) ) {
436            return new WP_Error( 'facebook', __( 'Error: your Facebook login has expired.', 'jetpack-mu-wpcom' ) );
437        }
438
439        $body = wp_remote_retrieve_body( $request );
440        $json = json_decode( $body );
441
442        if ( ! $body || ! $json ) {
443            return new WP_Error( 'facebook', __( 'Error: your Facebook login has expired.', 'jetpack-mu-wpcom' ) );
444        }
445
446        return $json;
447    }
448
449    /**
450     * Allows a logged out user to leave a comment as a facebook credentialed user.
451     * Overrides WordPress' core comment_registration option to treat the commenter as "registered" (verified) users.
452     */
453    public function allow_logged_out_user_to_comment_as_external() {
454        $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
455
456        if ( $service !== 'facebook' ) {
457            return;
458        }
459
460        add_filter( 'pre_option_comment_registration', '__return_zero' );
461        add_filter( 'pre_option_require_name_email', '__return_zero' );
462    }
463
464    /**
465     * Check if the comment is allowed by verifying the Facebook token.
466     *
467     * @param array $comment_data - The comment data.
468     * @return WP_Error|array The comment data if the comment is allowed, or a WP_Error if not.
469     */
470    public function verify_external_account( $comment_data ) {
471        $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
472
473        if ( $service === 'facebook' ) {
474            $fb_comment_data = self::verify_facebook_identity();
475
476            if ( is_wp_error( $fb_comment_data ) ) {
477                wp_die( esc_html( $fb_comment_data->get_error_message() ) );
478            }
479
480            $comment_data['highlander'] = 'facebook';
481        }
482
483        return $comment_data;
484    }
485
486    /**
487     * Verify nonce before accepting comment.
488     *
489     * @param int $comment_id The comment ID.
490     * @return void
491     */
492    public function check_comment_allowed( int $comment_id ) {
493        // Don't check if we're using Jetpack Comments.
494        if ( is_jetpack_comments() ) {
495            return;
496        }
497
498        // Check for Highlander Nonce.
499        if ( isset( $_POST['highlander_comment_nonce'] ) ) {
500            $valid_nonce     = false;
501            $current_user_id = get_current_user_id();
502
503            if ( wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['highlander_comment_nonce'] ) ), 'highlander_comment' ) ) {
504                $valid_nonce = true;
505            } elseif ( function_exists( 'wp_set_current_user' ) ) {
506                // There randomly occurs a race condition between the logged in/out state of the user.
507                // Check if their nonce is a logged out nonce.
508                wp_set_current_user( 0 );
509                $valid_nonce = wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['highlander_comment_nonce'] ) ), 'highlander_comment' );
510                wp_set_current_user( $current_user_id );
511            }
512
513            // All good, proceed.
514            if ( $valid_nonce ) {
515                return;
516            }
517
518            // Log the error to Log2Logstash.
519            // Related to https://github.com/Automattic/wp-calypso/issues/99436
520            if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
521                require_once WP_CONTENT_DIR . '/lib/log2logstash/log2logstash.php';
522
523                $headers = getallheaders();
524                $data    = array(
525                    'session_token' => wp_get_session_token(),
526                    'editor_type'   => isset( $_POST['verbum_loaded_editor'] ) ? sanitize_text_field( wp_unslash( $_POST['verbum_loaded_editor'] ) ) : '',
527                    'user_agent'    => sanitize_text_field( $headers['User-Agent'] ?? '' ),
528                    'referrer'      => esc_url_raw( $headers['Referer'] ?? '' ),
529                );
530
531                log2logstash(
532                    array(
533                        'feature'    => 'verbum-comments',
534                        'message'    => 'Pre-comment nonce failed',
535                        'blog_id'    => get_current_blog_id(),
536                        'user_id'    => $current_user_id,
537                        'host'       => sanitize_text_field( $headers['Host'] ?? '' ),
538                        'comment_id' => $comment_id,
539                        'extra'      => wp_json_encode( $data, JSON_UNESCAPED_SLASHES ),
540                    )
541                );
542            }
543        }
544
545        wp_die( esc_html__( 'Sorry, this comment could not be posted.', 'jetpack-mu-wpcom' ) );
546    }
547
548    /**
549     * Add all our custom fields to the comment meta after it is saved.
550     *
551     * @param int $comment_id The comment ID.
552     */
553    public function add_verbum_meta_data( $comment_id ) {
554        $comment_meta = array();
555        // phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce checked before saving comment
556        $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' );
557        $hc_avatar                           = isset( $_POST['hc_avatar'] ) ? esc_url_raw( wp_unslash( $_POST['hc_avatar'] ) ) : '';
558        $hc_userid                           = isset( $_POST['hc_foreign_user_id'] ) ? sanitize_text_field( wp_unslash( $_POST['hc_foreign_user_id'] ) ) : '';
559        $service                             = isset( $_POST['hc_post_as'] ) ? sanitize_text_field( wp_unslash( $_POST['hc_post_as'] ) ) : '';
560        $verbum_loaded_editor                = isset( $_POST['verbum_loaded_editor'] ) ? sanitize_text_field( wp_unslash( $_POST['verbum_loaded_editor'] ) ) : '';
561        $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'] ) ) : '';
562        // phpcs:enable WordPress.Security.NonceVerification.Missing -- nonce checked before saving comment
563        $allowed_comments_sources = array( 'gutenberg', 'textarea', 'textarea-slow-connection' );
564        if ( in_array( $verbum_loaded_editor, $allowed_comments_sources, true ) ) {
565            bump_stats_extras( 'verbum-comment-editor', $verbum_loaded_editor );
566        }
567        if ( $verbum_subscription_modal_show ) {
568            bump_stats_extras( 'verbum-subscription-modal', $verbum_subscription_modal_show );
569        }
570        switch ( $service ) {
571            case 'facebook':
572                $comment_meta['hc_post_as']         = 'facebook';
573                $comment_meta['hc_avatar']          = $hc_avatar;
574                $comment_meta['hc_foreign_user_id'] = $hc_userid;
575
576                bump_stats_extras( 'verbum-comment-posted', 'facebook' );
577                break;
578
579            case 'wordpress': // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText
580                if ( 'wpcom' === wpcom_blog_site_id_label() ) {
581                    do_action( 'highlander_wpcom_post_comment_bump_stat', $comment_id );
582                }
583                bump_stats_extras( 'verbum-comment-posted', 'wordpress' ); // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText
584                break;
585
586            case 'jetpack':
587                if ( is_jetpack_comments() && is_jetpack_comments_user_logged_in() ) {
588                    $comment_meta['hc_post_as']         = 'jetpack';
589                    $comment_meta['hc_avatar']          = check_and_return_post_string( 'hc_avatar' );
590                    $comment_meta['hc_foreign_user_id'] = check_and_return_post_string( 'hc_userid' );
591
592                    bump_stats_extras( 'verbum-comment-posted', 'jetpack' );
593                } else {
594                    jetpack_comments_die( 'JPC_HIGHLANDER_ADD_COMMENT_META' );
595                }
596
597                break;
598            default:
599                if ( is_user_logged_in() ) {
600                    bump_stats_extras( 'verbum-comment-posted', 'guest-logged-in' );
601                } else {
602                    bump_stats_extras( 'verbum-comment-posted', 'guest' );
603                }
604                break;
605        }
606
607        foreach ( $comment_meta as $key => $value ) {
608            add_comment_meta( $comment_id, $key, $value, true );
609        }
610    }
611
612    /**
613     * Get the hidden fields for the comment form.
614     */
615    public function hidden_fields() {
616        // Ironically, get_queried_post_id doesn't work inside query loop.
617        // See: https://github.com/Automattic/wp-calypso/issues/98136
618        $queried_post    = get_post();
619        $queried_post_id = $queried_post ? $queried_post->ID : 0;
620        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
621        $post_id = isset( $_GET['postid'] ) ? intval( $_GET['postid'] ) : $queried_post_id;
622        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
623        $is_current_user_subscribed = isset( $_GET['is_current_user_subscribed'] ) ? intval( $_GET['is_current_user_subscribed'] ) : 0;
624        $nonce                      = wp_create_nonce( 'highlander_comment' );
625        $hidden_fields              = get_comment_id_fields( $post_id ) . '
626            <input type="hidden" name="highlander_comment_nonce" id="highlander_comment_nonce" value="' . esc_attr( $nonce ) . '" />
627            <input type="hidden" name="verbum_show_subscription_modal" value="' . $this->subscription_modal_status() . '" />';
628
629        if ( is_jetpack_comments() ) {
630            $hidden_fields .= '
631                <input type="hidden" name="jetpack-remote-blogid" value="' . $this->blog_id . '" />
632                <input type="hidden" name="jetpack-remote-action" value="comment-post" />
633                <input type="hidden" name="is_current_user_subscribed" value="' . $is_current_user_subscribed . '" />';
634
635            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
636            $jetpack_nonce = isset( $_GET['jetpack_comments_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['jetpack_comments_nonce'] ) ) : false;
637            if ( $jetpack_nonce ) {
638                $hidden_fields .= '<input type="hidden" name="jetpack_comments_nonce" value="' . esc_attr( $jetpack_nonce ) . '" />';
639            }
640        }
641
642        return '<div class="verbum-form-meta">' . $hidden_fields . '</div>';
643    }
644
645    /***
646     * Check if we should load the Gutenberg comments.
647     *
648     * Block should be carefully loaded to avoid Forums, P2, etc.
649     */
650    public function should_load_gutenberg_comments() {
651        // Don't load when jetpack or atomic for now, it does not look cool on dark themes.
652        $is_jetpack_site = 522232 === get_current_blog_id();
653        if ( $is_jetpack_site ) {
654            return false;
655        }
656
657        // Blocks in comments have been disabled on a simple site
658        if ( empty( get_option( 'enable_blocks_comments', true ) ) ) {
659            return false;
660        }
661
662        return true;
663    }
664
665    /**
666     * Check if we should show the subscription modal.
667     */
668    public function should_show_subscription_modal() {
669        $modal_enabled = boolval( get_blog_option( $this->blog_id, 'jetpack_verbum_subscription_modal', true ) );
670
671        $is_jetpack_site = 522232 === get_current_blog_id(); // Disable if verbum is served via 'jetpack.wordpress.com'
672        return ! $is_jetpack_site && ! is_user_member_of_blog( '', $this->blog_id ) && $modal_enabled;
673    }
674
675    /**
676     * Get the status of the subscription modal.
677     */
678    public function subscription_modal_status() {
679        if ( is_user_member_of_blog( '', $this->blog_id ) ) {
680            return 'hidden_is_blog_member';
681        }
682        if ( is_jetpack_comments() ) {
683            return 'hidden_jetpack';
684        }
685        if ( ! get_option( 'jetpack_verbum_subscription_modal', true ) ) {
686            return 'hidden_disabled';
687        }
688        return '';
689    }
690
691    /**
692     * Add Jetpack script data.
693     *
694     * @param array $data - The Jetpack script data.
695     * @return array - The modified Jetpack script data.
696     */
697    public function add_jetpack_script_data( $data ) {
698        if ( \Verbum_Block_Utils::should_show_verbum_comments() ) {
699            if ( ! isset( $data['site']['host'] ) ) {
700                $data['site']['host'] = ( new \Automattic\Jetpack\Status\Host() )->get_known_host_guess();
701            }
702            if ( ! isset( $data['site']['is_wpcom_platform'] ) ) {
703                $data['site']['is_wpcom_platform'] = ( new \Automattic\Jetpack\Status\Host() )->is_wpcom_platform();
704            }
705        }
706        return $data;
707    }
708}