PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | modules/comments/base.php +363 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,363 @@
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 +
8 +use Automattic\Jetpack\Image_CDN\Image_CDN_Core;
9 +
10 +/**
11 + * All the code shared between WP.com Highlander and Jetpack Highlander
12 + */
13 +class 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 +}