Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 122
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Highlander_Comments_Base
0.00% covered (danger)
0.00%
0 / 122
0.00% covered (danger)
0.00%
0 / 13
3540
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 setup_globals
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setup_actions
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setup_filters
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 is_highlander_comment_post
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
56
 sign_remote_comment_parameters
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 comments_array
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 sort_comments_by_comment_date_gmt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_current_commenter
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 allow_logged_out_user_to_comment_as_external
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 allow_logged_in_user_to_comment_as_guest
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
420
 set_comment_cookies
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
156
 photon_avatar
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Jetpack comments base file - where the code shared between WP.com Highlander and Jetpack Highlander is defined
4 *
5 * @package automattic/jetpack
6 */
7
8use Automattic\Jetpack\Image_CDN\Image_CDN_Core;
9
10/**
11 * All the code shared between WP.com Highlander and Jetpack Highlander
12 */
13class Highlander_Comments_Base {
14    /**
15     * ID sources.
16     *
17     * @var array
18     */
19    public $id_sources;
20
21    /**
22     * The default comment scheme, if set.
23     *
24     * @var ?string
25     */
26    public $default_color_scheme;
27
28    /**
29     * Constructor
30     */
31    public function __construct() {
32        $this->setup_globals();
33        $this->setup_actions();
34        $this->setup_filters();
35    }
36
37    /**
38     * Set any global variables or class variables
39     *
40     * @since 1.4
41     */
42    protected function setup_globals() {}
43
44    /**
45     * Setup actions for methods in this class
46     *
47     * @since 1.4
48     */
49    protected function setup_actions() {
50        // Before a comment is posted.
51        add_action( 'pre_comment_on_post', array( $this, 'allow_logged_out_user_to_comment_as_external' ) );
52
53        // After a comment is posted.
54        add_action( 'comment_post', array( $this, 'set_comment_cookies' ) );
55    }
56
57    /**
58     * Setup filters for methods in this class
59     *
60     * @since 1.4
61     */
62    protected function setup_filters() {
63        add_filter( 'comments_array', array( $this, 'comments_array' ) );
64        add_filter( 'preprocess_comment', array( $this, 'allow_logged_in_user_to_comment_as_guest' ), 0 );
65    }
66
67    /**
68     * Is this a Highlander POST request?
69     * Optionally restrict to one or more credentials slug (facebook, ...)
70     *
71     * @param mixed ...$args Comments credentials slugs.
72     * @return false|string false if it's not a Highlander POST request.  The matching credentials slug if it is.
73     */
74    public function is_highlander_comment_post( ...$args ) {
75
76        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verification should happen in Jetpack_Comments::pre_comment_on_post(). Internal ref for details: p1645643468937519/1645189749.180299-slack-C02HQGKMFJ8
77        if ( empty( $_POST['hc_post_as'] ) ) {
78            return false;
79        }
80        $hc_post_as = wp_unslash( $_POST['hc_post_as'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized here by comparing against known values.
81        // phpcs:enable WordPress.Security.NonceVerification.Missing
82
83        if ( $args ) {
84            foreach ( $args as $id_source ) {
85                if ( $id_source === $hc_post_as ) {
86                    return $id_source;
87                }
88            }
89            return false;
90        }
91        return is_string( $hc_post_as ) && in_array( $hc_post_as, $this->id_sources, true ) ? $hc_post_as : false;
92    }
93
94    /**
95     * Signs an array of scalars with the self-hosted blog's Jetpack Token
96     *
97     * If parameter values are not scalars a WP_Error is  returned, otherwise a keyed hash value is returned using the HMAC method.
98     *
99     * @param array  $parameters Comment parameters.
100     * @param string $key Key used for generating the HMAC variant of the message digest.
101     * @return string HMAC
102     */
103    public static function sign_remote_comment_parameters( $parameters, $key ) {
104        unset(
105            $parameters['sig'],       // Don't sign the signature.
106            $parameters['replytocom'] // This parameter is unsigned - it changes dynamically as the comment form moves from parent comment to parent comment.
107        );
108
109        ksort( $parameters );
110
111        $signing = array();
112        foreach ( $parameters as $k => $v ) {
113            if ( ! is_scalar( $v ) ) {
114                return new WP_Error( 'invalid_input', __( 'Invalid request', 'jetpack' ), array( 'status' => 400 ) );
115            }
116
117            $signing[] = "{$k}={$v}";
118        }
119
120        return hash_hmac( 'sha1', implode( ':', $signing ), $key );
121    }
122
123    /**
124     * Adds comment author email and whether the comment is approved to the comments array
125     *
126     * After commenting as a guest while logged in, the user needs to see both:
127     * ( user_id = blah AND comment_approved = 0 )
128     * and ( comment_author_email = blah AND comment_approved = 0 )
129     * Core only does the first since the user is logged in, so this adds the second to the comments array.
130     *
131     * @param array $comments All comment data.
132     * @return array A modified array of comment data.
133     */
134    public function comments_array( $comments ) {
135        global $wpdb, $post;
136
137        $commenter = $this->get_current_commenter();
138
139        if ( ! $commenter['user_id'] ) {
140            return $comments;
141        }
142
143        if ( ! $commenter['comment_author'] ) {
144            return $comments;
145        }
146
147        $in_moderation_comments = $wpdb->get_results(
148            $wpdb->prepare(
149                "SELECT * FROM `$wpdb->comments` WHERE `comment_post_ID` = %d AND `user_id` = 0 AND `comment_author` = %s AND `comment_author_email` = %s AND `comment_approved` = '0' ORDER BY `comment_date_gmt` /* Highlander_Comments_Base::comments_array() */",
150                $post->ID,
151                wp_specialchars_decode( $commenter['comment_author'], ENT_QUOTES ),
152                $commenter['comment_author_email']
153            )
154        );
155
156        if ( ! $in_moderation_comments ) {
157            return $comments;
158        }
159
160        // @todo ZOMG this is a bad idea
161        $comments = array_merge( $comments, $in_moderation_comments );
162        usort( $comments, array( $this, 'sort_comments_by_comment_date_gmt' ) );
163
164        return $comments;
165    }
166
167    /**
168     * Comment sort comparator: comment_date_gmt
169     *
170     * @since 1.4
171     * @param object $a The first comment to compare dates with.
172     * @param object $b The second comment to compare dates with.
173     * @return int
174     */
175    public function sort_comments_by_comment_date_gmt( $a, $b ) {
176        return $a->comment_date_gmt <=> $b->comment_date_gmt;
177    }
178
179    /**
180     * Get the current commenter's information from their cookie
181     *
182     * @since 1.4
183     * @return array Commenters information from cookie
184     */
185    protected function get_current_commenter() {
186        // Defaults.
187        $user_id              = 0;
188        $comment_author       = '';
189        $comment_author_email = '';
190        $comment_author_url   = '';
191
192        if ( isset( $_COOKIE[ 'comment_author_' . COOKIEHASH ] ) ) {
193            $comment_author = sanitize_text_field( wp_unslash( $_COOKIE[ 'comment_author_' . COOKIEHASH ] ) );
194        }
195
196        if ( isset( $_COOKIE[ 'comment_author_email_' . COOKIEHASH ] ) ) {
197            $comment_author_email = sanitize_email( wp_unslash( $_COOKIE[ 'comment_author_email_' . COOKIEHASH ] ) );
198        }
199
200        if ( isset( $_COOKIE[ 'comment_author_url_' . COOKIEHASH ] ) ) {
201            $comment_author_url = esc_url_raw( wp_unslash( $_COOKIE[ 'comment_author_url_' . COOKIEHASH ] ) );
202        }
203
204        if ( is_user_logged_in() ) {
205            $user    = wp_get_current_user();
206            $user_id = $user->ID;
207        }
208
209        return compact( 'comment_author', 'comment_author_email', 'comment_author_url', 'user_id' );
210    }
211
212    /**
213     * Allows a logged out user to leave a comment as a facebook/wp.com credentialed user.
214     * Overrides WordPress' core comment_registration option to treat these commenters as "registered" (verified) users.
215     *
216     * @since 1.4
217     */
218    public function allow_logged_out_user_to_comment_as_external() {
219        // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText
220        if ( ! $this->is_highlander_comment_post( 'facebook', 'wordpress' ) ) {
221            return;
222        }
223
224        add_filter( 'pre_option_comment_registration', '__return_zero' );
225        add_filter( 'pre_option_require_name_email', '__return_zero' );
226    }
227
228    /**
229     * Allow a logged in user to post as a guest, or FB credentialed request.
230     * Bypasses WordPress' core overrides that force a logged in user to comment as that user.
231     * Respects comment_registration option.
232     *
233     * @since 1.4
234     * @param array $comment_data All data for a specific comment.
235     * @return array Modified comment data, or an error if the required fields or a valid email address are not entered.
236     */
237    public function allow_logged_in_user_to_comment_as_guest( $comment_data ) {
238        // Bail if user registration is allowed.
239        if ( get_option( 'comment_registration' ) ) {
240            return $comment_data;
241        }
242
243        // Bail if user is not logged in or not a post request.
244        if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'POST' !== strtoupper( $_SERVER['REQUEST_METHOD'] ) || ! is_user_logged_in() ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- simple comparison
245            return $comment_data;
246        }
247
248        // Bail if this is not a guest or external service credentialed request.
249        if ( ! $this->is_highlander_comment_post( 'guest', 'facebook' ) ) {
250            return $comment_data;
251        }
252
253        $user = wp_get_current_user();
254
255        foreach ( array(
256            'comment_author'       => 'display_name',
257            'comment_author_email' => 'user_email',
258            'comment_author_url'   => 'user_url',
259        ) as $comment_field => $user_field ) {
260            if ( addslashes( $user->$user_field ) !== $comment_data[ $comment_field ] ) {
261                return $comment_data; // some other plugin already did something funky.
262            }
263        }
264
265        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verification should happen in Jetpack_Comments::pre_comment_on_post()
266        // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitization too
267        if ( get_option( 'require_name_email' ) ) {
268            if ( isset( $_POST['email'] ) && 6 > strlen( wp_unslash( $_POST['email'] ) ) || empty( $_POST['author'] ) ) {
269                wp_die( esc_html__( 'Error: please fill the required fields (name, email).', 'jetpack' ), 400 );
270            } elseif ( ! isset( $_POST['email'] ) || ! is_email( wp_unslash( $_POST['email'] ) ) ) {
271                wp_die( esc_html__( 'Error: please enter a valid email address.', 'jetpack' ), 400 );
272            }
273        }
274
275        $author_change = false;
276        foreach ( array(
277            'comment_author'       => 'author',
278            'comment_author_email' => 'email',
279            'comment_author_url'   => 'url',
280        ) as $comment_field => $post_field ) {
281            if ( ( ! isset( $_POST[ $post_field ] ) || $comment_data[ $comment_field ] !== $_POST[ $post_field ] ) && 'url' !== $post_field ) {
282                $author_change = true;
283            }
284            $comment_data[ $comment_field ] = isset( $_POST[ $post_field ] ) ? wp_unslash( $_POST[ $post_field ] ) : null;
285        }
286        // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
287
288        // Mark as guest comment if name or email were changed.
289        if ( $author_change ) {
290            $comment_data['user_ID'] = 0;
291            $comment_data['user_id'] = $comment_data['user_ID'];
292        }
293
294        return $comment_data;
295    }
296
297    /**
298     * Set the comment cookies or bail if comment is invalid
299     *
300     * @since 1.4
301     * @param int $comment_id The comment ID.
302     */
303    public function set_comment_cookies( $comment_id ) {
304        // Get comment and bail if it's invalid somehow.
305        $comment = get_comment( $comment_id );
306        if ( empty( $comment ) || is_wp_error( $comment ) ) {
307            return;
308        }
309
310        $id_source = $this->is_highlander_comment_post();
311        if ( empty( $id_source ) ) {
312            return;
313        }
314
315        // Set comment author cookies.
316        // We don't set the cookies if they are logged in with WordPress.com because they already have a cookie set.
317        // phpcs:ignore WordPress.WP.CapitalPDangit
318        if ( 'wordpress' !== $id_source ) {
319            // phpcs:disable WordPress.Security.NonceVerification -- Nonce verification should happen in Jetpack_Comments::pre_comment_on_post().
320            $is_consenting_to_cookies = ( isset( $_POST['wp-comment-cookies-consent'] ) );
321
322            $cookie_options = array(
323                'expires'  => time() + apply_filters( 'comment_cookie_lifetime', YEAR_IN_SECONDS ),
324                'path'     => COOKIEPATH,
325                'domain'   => COOKIE_DOMAIN,
326                'secure'   => is_ssl(),
327                'httponly' => true,
328            );
329
330            // If there is no consent, remove any cookies that may have been set.
331            if ( ( 'guest' === $id_source ) && ! $is_consenting_to_cookies ) {
332                $cookie_options['expires'] = time() - YEAR_IN_SECONDS;
333            }
334
335            // Set samesite to None if the request is from Jetpack iframe.
336            // This is needed because it is considered third party.
337            if ( isset( $_REQUEST['for'] ) && 'jetpack' === $_REQUEST['for'] ) {
338                $cookie_options['samesite'] = 'None';
339            }
340            // phpcs:enable WordPress.Security.NonceVerification
341
342            // phpcs:disable Jetpack.Functions.SetCookie.MissingTrueHTTPOnly
343            isset( $comment->comment_author ) ? setcookie( 'comment_author_' . COOKIEHASH, $comment->comment_author, $cookie_options ) : null;
344            isset( $comment->comment_author_email ) ? setcookie( 'comment_author_email_' . COOKIEHASH, $comment->comment_author_email, $cookie_options ) : null;
345            isset( $comment->comment_author_url ) ? setcookie( 'comment_author_url_' . COOKIEHASH, esc_url( $comment->comment_author_url ), $cookie_options ) : null;
346            // phpcs:enable Jetpack.Functions.SetCookie.MissingTrueHTTPOnly
347        }
348    }
349
350    /**
351     * Get an avatar from Photon
352     *
353     * @since 1.4
354     * @param string $url The avatar URL.
355     * @param int    $size The avatar size.
356     * @return string
357     */
358    protected function photon_avatar( $url, $size ) {
359        $size = (int) $size;
360
361        return Image_CDN_Core::cdn_url( $url, array( 'resize' => "$size,$size" ) );
362    }
363}