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