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 | jetpack_vendor/automattic/jetpack-sync/src/modules/class-comments.php +610 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,610 @@
1 +<?php
2 +/**
3 + * Comments sync module.
4 + *
5 + * @package automattic/jetpack-sync
6 + */
7 +
8 +namespace Automattic\Jetpack\Sync\Modules;
9 +
10 +use Automattic\Jetpack\Sync\Defaults;
11 +use Automattic\Jetpack\Sync\Modules;
12 +use Automattic\Jetpack\Sync\Settings;
13 +
14 +if ( ! defined( 'ABSPATH' ) ) {
15 + exit( 0 );
16 +}
17 +
18 +/**
19 + * Class to handle sync for comments.
20 + */
21 +class Comments extends Module {
22 +
23 + /**
24 + * Sync module name.
25 + *
26 + * @access public
27 + *
28 + * @return string
29 + */
30 + public function name() {
31 + return 'comments';
32 + }
33 +
34 + /**
35 + * The id field in the database.
36 + *
37 + * @access public
38 + *
39 + * @return string
40 + */
41 + public function id_field() {
42 + return 'comment_ID';
43 + }
44 +
45 + /**
46 + * The table name.
47 + *
48 + * @access public
49 + *
50 + * @return string
51 + * @deprecated since 3.11.0 Use table() instead.
52 + */
53 + public function table_name() {
54 + _deprecated_function( __METHOD__, '3.11.0', 'Automattic\\Jetpack\\Sync\\Comments->table' );
55 + return 'comments';
56 + }
57 +
58 + /**
59 + * The table in the database with the prefix.
60 + *
61 + * @access public
62 + *
63 + * @return string|bool
64 + */
65 + public function table() {
66 + global $wpdb;
67 + return $wpdb->comments;
68 + }
69 +
70 + /**
71 + * Retrieve a comment by its ID.
72 + *
73 + * @access public
74 + *
75 + * @param string $object_type Type of the sync object.
76 + * @param int $id ID of the sync object.
77 + * @return \WP_Comment|bool Filtered \WP_Comment object, or false if the object is not a comment.
78 + */
79 + public function get_object_by_id( $object_type, $id ) {
80 + $comment_id = (int) $id;
81 + if ( 'comment' === $object_type ) {
82 + $comment = get_comment( $comment_id );
83 + if ( $comment ) {
84 + return $this->filter_comment( $comment );
85 + }
86 + }
87 +
88 + return false;
89 + }
90 +
91 + /**
92 + * Initialize comments action listeners.
93 + * Also responsible for initializing comment meta listeners.
94 + *
95 + * @access public
96 + *
97 + * @param callable $callable Action handler callable.
98 + */
99 + public function init_listeners( $callable ) {
100 + add_action( 'wp_insert_comment', $callable, 10, 2 );
101 + add_action( 'deleted_comment', $callable );
102 + add_action( 'trashed_comment', $callable );
103 + add_action( 'spammed_comment', $callable );
104 + add_action( 'trashed_post_comments', $callable, 10, 2 );
105 + add_action( 'untrash_post_comments', $callable );
106 + add_action( 'comment_approved_to_unapproved', $callable );
107 + add_action( 'comment_unapproved_to_approved', $callable );
108 + add_action( 'jetpack_modified_comment_contents', $callable, 10, 2 );
109 + add_action( 'untrashed_comment', $callable, 10, 2 );
110 + add_action( 'unspammed_comment', $callable, 10, 2 );
111 + add_filter( 'wp_update_comment_data', array( $this, 'handle_comment_contents_modification' ), 10, 3 );
112 +
113 + // comment actions.
114 + add_filter( 'jetpack_sync_before_enqueue_wp_insert_comment', array( $this, 'filter_jetpack_sync_before_enqueue_wp_insert_comment' ) );
115 + add_filter( 'jetpack_sync_before_enqueue_deleted_comment', array( $this, 'only_allow_white_listed_comment_types' ) );
116 + add_filter( 'jetpack_sync_before_enqueue_trashed_comment', array( $this, 'only_allow_white_listed_comment_types' ) );
117 + add_filter( 'jetpack_sync_before_enqueue_untrashed_comment', array( $this, 'only_allow_white_listed_comment_types' ) );
118 + add_filter( 'jetpack_sync_before_enqueue_spammed_comment', array( $this, 'only_allow_white_listed_comment_types' ) );
119 + add_filter( 'jetpack_sync_before_enqueue_unspammed_comment', array( $this, 'only_allow_white_listed_comment_types' ) );
120 +
121 + // comment status transitions.
122 + add_filter( 'jetpack_sync_before_enqueue_comment_approved_to_unapproved', array( $this, 'only_allow_white_listed_comment_type_transitions' ) );
123 + add_filter( 'jetpack_sync_before_enqueue_comment_unapproved_to_approved', array( $this, 'only_allow_white_listed_comment_type_transitions' ) );
124 +
125 + // Post Actions.
126 + add_filter( 'jetpack_sync_before_enqueue_trashed_post_comments', array( $this, 'filter_blacklisted_post_types' ) );
127 + add_filter( 'jetpack_sync_before_enqueue_untrash_post_comments', array( $this, 'filter_blacklisted_post_types' ) );
128 +
129 + /**
130 + * Even though it's messy, we implement these hooks because
131 + * the edit_comment hook doesn't include the data
132 + * so this saves us a DB read for every comment event.
133 + */
134 + foreach ( $this->get_whitelisted_comment_types() as $comment_type ) {
135 + foreach ( array( 'unapproved', 'approved' ) as $comment_status ) {
136 + $comment_action_name = "comment_{$comment_status}_{$comment_type}";
137 + add_action( $comment_action_name, $callable, 10, 2 );
138 + add_filter(
139 + 'jetpack_sync_before_enqueue_' . $comment_action_name,
140 + array(
141 + $this,
142 + 'expand_wp_insert_comment',
143 + )
144 + );
145 + }
146 + }
147 +
148 + // Listen for meta changes.
149 + $this->init_listeners_for_meta_type( 'comment', $callable );
150 + $this->init_meta_whitelist_handler( 'comment', array( $this, 'filter_meta' ) );
151 + }
152 +
153 + /**
154 + * Handler for any comment content updates.
155 + *
156 + * @access public
157 + *
158 + * @param array $new_comment The new, processed comment data.
159 + * @param array $old_comment The old, unslashed comment data.
160 + * @param array $new_comment_with_slashes The new, raw comment data.
161 + * @return array The new, processed comment data.
162 + */
163 + public function handle_comment_contents_modification( $new_comment, $old_comment, $new_comment_with_slashes ) {
164 + $changes = array();
165 + $content_fields = array(
166 + 'comment_author',
167 + 'comment_author_email',
168 + 'comment_author_url',
169 + 'comment_content',
170 + );
171 + foreach ( $content_fields as $field ) {
172 + if ( $new_comment_with_slashes[ $field ] !== $old_comment[ $field ] ) {
173 + $changes[ $field ] = array( $new_comment[ $field ], $old_comment[ $field ] );
174 + }
175 + }
176 +
177 + if ( ! empty( $changes ) ) {
178 + /**
179 + * Signals to the sync listener that this comment's contents were modified and a sync action
180 + * reflecting the change(s) to the content should be sent
181 + *
182 + * @since 1.6.3
183 + * @since-jetpack 4.9.0
184 + *
185 + * @param int $new_comment['comment_ID'] ID of comment whose content was modified
186 + * @param mixed $changes Array of changed comment fields with before and after values
187 + */
188 + do_action( 'jetpack_modified_comment_contents', $new_comment['comment_ID'], $changes );
189 + }
190 + return $new_comment;
191 + }
192 +
193 + /**
194 + * Initialize comments action listeners for full sync.
195 + *
196 + * @access public
197 + *
198 + * @param callable $callable Action handler callable.
199 + */
200 + public function init_full_sync_listeners( $callable ) {
201 + add_action( 'jetpack_full_sync_comments', $callable ); // Also send comments meta.
202 + }
203 +
204 + /**
205 + * Gets a filtered list of comment types that sync can hook into.
206 + *
207 + * @access public
208 + *
209 + * @return array Defaults to [ '', 'comment', 'trackback', 'pingback', 'review', 'note' ].
210 + */
211 + public function get_whitelisted_comment_types() {
212 + return Defaults::get_comment_types_whitelist();
213 + }
214 +
215 + /**
216 + * Returns escaped SQL for whitelisted comment types.
217 + * Can be injected directly into a WHERE clause.
218 + *
219 + * @access public
220 + *
221 + * @return string SQL WHERE clause.
222 + */
223 + public function get_whitelisted_comment_types_sql() {
224 + return 'comment_type IN (\'' . implode( '\', \'', array_map( 'esc_sql', $this->get_whitelisted_comment_types() ) ) . '\')';
225 + }
226 +
227 + /**
228 + * Prevents any comment types that are not in the whitelist from being enqueued and sent to WordPress.com.
229 + *
230 + * @param array $args Arguments passed to wp_insert_comment, deleted_comment, spammed_comment, etc.
231 + *
232 + * @return bool or array $args Arguments passed to wp_insert_comment, deleted_comment, spammed_comment, etc.
233 + */
234 + public function only_allow_white_listed_comment_types( $args ) {
235 + if ( empty( $args ) ) {
236 + return false;
237 + }
238 +
239 + $comment = false;
240 +
241 + if ( isset( $args[1] ) ) {
242 + // comment object is available.
243 + $comment = $args[1];
244 + } elseif ( isset( $args[0] ) && is_numeric( $args[0] ) ) {
245 + // comment_id is available.
246 + $comment = get_comment( $args[0] );
247 + }
248 +
249 + if ( ! $comment instanceof \WP_Comment ) {
250 + return false;
251 + }
252 +
253 + if ( ! in_array( $comment->comment_type, $this->get_whitelisted_comment_types(), true ) ) {
254 + return false;
255 + }
256 +
257 + return $args;
258 + }
259 +
260 + /**
261 + * Filter all blacklisted post types.
262 + *
263 + * @param array $args Hook arguments.
264 + * @return array|false Hook arguments, or false if the post type is a blacklisted one.
265 + */
266 + public function filter_blacklisted_post_types( $args ) {
267 + $post_id = $args[0];
268 + $posts_module = Modules::get_module( 'posts' );
269 + '@phan-var Posts $posts_module';
270 +
271 + if ( false !== $posts_module && ! $posts_module->is_post_type_allowed( $post_id ) ) {
272 + return false;
273 + }
274 +
275 + return $args;
276 + }
277 +
278 + /**
279 + * Prevents any comment types that are not in the whitelist from being enqueued and sent to WordPress.com.
280 + *
281 + * @param array $args Arguments passed to wp_{old_status}_to_{new_status}.
282 + *
283 + * @return bool or array $args Arguments passed to wp_{old_status}_to_{new_status}
284 + */
285 + public function only_allow_white_listed_comment_type_transitions( $args ) {
286 + $comment = $args[0];
287 +
288 + if ( ! in_array( $comment->comment_type, $this->get_whitelisted_comment_types(), true ) ) {
289 + return false;
290 + }
291 +
292 + return $args;
293 + }
294 +
295 + /**
296 + * Prevents any comment types that are not in the whitelist from being enqueued and sent to WordPress.com.
297 + * Also expands comment data before being enqueued.
298 + *
299 + * @param array $args Arguments passed to wp_insert_comment.
300 + *
301 + * @return false or array $args Arguments passed to wp_insert_comment or false if the comment type is a blacklisted one.
302 + */
303 + public function filter_jetpack_sync_before_enqueue_wp_insert_comment( $args ) {
304 + if ( false === $this->only_allow_white_listed_comment_types( $args ) ) {
305 + return false;
306 + }
307 +
308 + return $this->expand_wp_insert_comment( $args );
309 + }
310 +
311 + /**
312 + * Whether a comment type is allowed.
313 + * A comment type is allowed if it's present in the comment type whitelist.
314 + *
315 + * @param int $comment_id ID of the comment.
316 + * @return boolean Whether the comment type is allowed.
317 + */
318 + public function is_comment_type_allowed( $comment_id ) {
319 + $comment = get_comment( $comment_id );
320 +
321 + if ( isset( $comment->comment_type ) ) {
322 + return in_array( $comment->comment_type, $this->get_whitelisted_comment_types(), true );
323 + }
324 + return false;
325 + }
326 +
327 + /**
328 + * Initialize the module in the sender.
329 + *
330 + * @access public
331 + */
332 + public function init_before_send() {
333 +
334 + // Full sync.
335 + $sync_module = Modules::get_module( 'full-sync' );
336 + if ( $sync_module instanceof Full_Sync_Immediately ) {
337 + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_comments', array( $this, 'extract_comments_and_meta' ) );
338 + } else {
339 + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_comments', array( $this, 'expand_comment_ids' ) );
340 + }
341 + }
342 +
343 + /**
344 + * Enqueue the comments actions for full sync.
345 + *
346 + * @access public
347 + *
348 + * @param array $config Full sync configuration for this sync module.
349 + * @param int $max_items_to_enqueue Maximum number of items to enqueue.
350 + * @param boolean $state True if full sync has finished enqueueing this module, false otherwise.
351 + * @return array Number of actions enqueued, and next module state.
352 + */
353 + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
354 + global $wpdb;
355 + return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_comments', $wpdb->comments, 'comment_ID', $this->get_where_sql( $config ), $max_items_to_enqueue, $state );
356 + }
357 +
358 + /**
359 + * Retrieve an estimated number of actions that will be enqueued.
360 + *
361 + * @access public
362 + *
363 + * @param array $config Full sync configuration for this sync module.
364 + * @return int Number of items yet to be enqueued.
365 + */
366 + public function estimate_full_sync_actions( $config ) {
367 + global $wpdb;
368 +
369 + $query = "SELECT count(*) FROM $wpdb->comments";
370 +
371 + $where_sql = $this->get_where_sql( $config );
372 + if ( $where_sql ) {
373 + $query .= ' WHERE ' . $where_sql;
374 + }
375 +
376 + // TODO: Call $wpdb->prepare on the following query.
377 + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
378 + $count = (int) $wpdb->get_var( $query );
379 +
380 + return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
381 + }
382 +
383 + /**
384 + * Retrieve the WHERE SQL clause based on the module config.
385 + *
386 + * @access public
387 + *
388 + * @param array $config Full sync configuration for this sync module.
389 + * @return string WHERE SQL clause, or `null` if no comments are specified in the module config.
390 + */
391 + public function get_where_sql( $config ) {
392 + $where_sql = $this->get_whitelisted_comment_types_sql();
393 +
394 + if ( is_array( $config ) && ! empty( $config ) ) {
395 + return 'comment_ID IN (' . implode( ',', array_map( 'intval', $config ) ) . ')';
396 + }
397 +
398 + return $where_sql;
399 + }
400 +
401 + /**
402 + * Retrieve the actions that will be sent for this module during a full sync.
403 + *
404 + * @access public
405 + *
406 + * @return array Full sync actions of this module.
407 + */
408 + public function get_full_sync_actions() {
409 + return array( 'jetpack_full_sync_comments' );
410 + }
411 +
412 + /**
413 + * Count all the actions that are going to be sent.
414 + *
415 + * @access public
416 + *
417 + * @param array $action_names Names of all the actions that will be sent.
418 + * @return int Number of actions.
419 + */
420 + public function count_full_sync_actions( $action_names ) {
421 + return $this->count_actions( $action_names, array( 'jetpack_full_sync_comments' ) );
422 + }
423 +
424 + /**
425 + * Expand the comment status change before the data is serialized and sent to the server.
426 + *
427 + * @access public
428 + * @todo This is not used currently - let's implement it.
429 + *
430 + * @param array $args The hook parameters.
431 + * @return array The expanded hook parameters.
432 + */
433 + public function expand_wp_comment_status_change( $args ) {
434 + return array( $args[0], $this->filter_comment( $args[1] ) );
435 + }
436 +
437 + /**
438 + * Expand the comment creation before the data is added to the Sync queue.
439 + *
440 + * @access public
441 + *
442 + * @param array $args The hook parameters.
443 + * @return array The expanded hook parameters.
444 + */
445 + public function expand_wp_insert_comment( $args ) {
446 + return array( $args[0], $this->filter_comment( $args[1] ) );
447 + }
448 +
449 + /**
450 + * Filter a comment object to the fields we need.
451 + *
452 + * @access public
453 + *
454 + * @param \WP_Comment $comment The unfiltered comment object.
455 + * @return \WP_Comment Filtered comment object.
456 + */
457 + public function filter_comment( $comment ) {
458 + /**
459 + * Filters whether to prevent sending comment data to .com
460 + *
461 + * Passing true to the filter will prevent the comment data from being sent
462 + * to the WordPress.com.
463 + * Instead we pass data that will still enable us to do a checksum against the
464 + * Jetpacks data but will prevent us from displaying the data on in the API as well as
465 + * other services.
466 + *
467 + * @since 1.6.3
468 + * @since-jetpack 4.2.0
469 + *
470 + * @param boolean false prevent post data from bing synced to WordPress.com
471 + * @param mixed $comment WP_COMMENT object
472 + */
473 + if ( apply_filters( 'jetpack_sync_prevent_sending_comment_data', false, $comment ) ) {
474 + $blocked_comment = new \stdClass();
475 + $blocked_comment->comment_ID = $comment->comment_ID;
476 + $blocked_comment->comment_date = $comment->comment_date;
477 + $blocked_comment->comment_date_gmt = $comment->comment_date_gmt;
478 + $blocked_comment->comment_approved = 'jetpack_sync_blocked';
479 + return $blocked_comment;
480 + }
481 +
482 + return $comment;
483 + }
484 +
485 + /**
486 + * Whether a certain comment meta key is whitelisted for sync.
487 + *
488 + * @access public
489 + *
490 + * @param string $meta_key Comment meta key.
491 + * @return boolean Whether the meta key is whitelisted.
492 + */
493 + public function is_whitelisted_comment_meta( $meta_key ) {
494 + return in_array( $meta_key, Settings::get_setting( 'comment_meta_whitelist' ), true );
495 + }
496 +
497 + /**
498 + * Handler for filtering out non-whitelisted comment meta.
499 + *
500 + * @access public
501 + *
502 + * @param array $args Hook args.
503 + * @return array|boolean False if not whitelisted, the original hook args otherwise.
504 + */
505 + public function filter_meta( $args ) {
506 + if ( ! is_array( $args ) || count( $args ) < 3 ) {
507 + return false;
508 + }
509 + if ( $this->is_comment_type_allowed( $args[1] ) && $this->is_whitelisted_comment_meta( $args[2] ) ) {
510 + return $args;
511 + }
512 +
513 + return false;
514 + }
515 +
516 + /**
517 + * Expand the comment IDs to comment objects and meta before being serialized and sent to the server.
518 + *
519 + * @access public
520 + *
521 + * @param array $args The hook parameters.
522 + * @return array The expanded hook parameters.
523 + */
524 + public function expand_comment_ids( $args ) {
525 + list( $comment_ids, $previous_interval_end ) = $args;
526 + $comments = get_comments(
527 + array(
528 + 'include_unapproved' => true,
529 + 'comment__in' => $comment_ids,
530 + 'orderby' => 'comment_ID',
531 + 'order' => 'DESC',
532 + )
533 + );
534 +
535 + return array(
536 + $comments,
537 + $this->get_metadata( $comment_ids, 'comment', Settings::get_setting( 'comment_meta_whitelist' ) ),
538 + $previous_interval_end,
539 + );
540 + }
541 +
542 + /**
543 + * Expand the comment IDs to comment objects and meta before being serialized and sent to the server.
544 + *
545 + * @access public
546 + *
547 + * @param array $args The hook parameters.
548 + * @return array The expanded hook parameters.
549 + */
550 + public function extract_comments_and_meta( $args ) {
551 + list( $filtered_comments, $previous_end ) = $args;
552 + return array(
553 + $filtered_comments['objects'],
554 + $filtered_comments['meta'],
555 + $previous_end,
556 + );
557 + }
558 +
559 + /**
560 + * Given the Module Configuration and Status return the next chunk of items to send.
561 + * This function also expands the posts and metadata and filters them based on the maximum size constraints.
562 + *
563 + * @param array $config This module Full Sync configuration.
564 + * @param array $status This module Full Sync status.
565 + * @param int $chunk_size Chunk size.
566 + *
567 + * @return array
568 + */
569 + public function get_next_chunk( $config, $status, $chunk_size ) {
570 +
571 + $comment_ids = parent::get_next_chunk( $config, $status, $chunk_size );
572 + // If no comment IDs were fetched, return an empty array.
573 + if ( empty( $comment_ids ) ) {
574 + return array();
575 + }
576 + $comments = get_comments(
577 + array(
578 + 'comment__in' => $comment_ids,
579 + 'orderby' => 'comment_ID',
580 + 'order' => 'DESC',
581 + )
582 + );
583 + // If no comments were fetched, make sure to return the expected structure so that status is updated correctly.
584 + if ( empty( $comments ) ) {
585 + return array(
586 + 'object_ids' => $comment_ids,
587 + 'objects' => array(),
588 + 'meta' => array(),
589 + );
590 + }
591 + // Get the comment IDs from the comments that were fetched.
592 + $fetched_comment_ids = wp_list_pluck( $comments, 'comment_ID' );
593 + $metadata = $this->get_metadata( $fetched_comment_ids, 'comment', Settings::get_setting( 'comment_meta_whitelist' ) );
594 +
595 + // Filter the comments and metadata based on the maximum size constraints.
596 + list( $filtered_comment_ids, $filtered_comments, $filtered_comments_metadata ) = $this->filter_objects_and_metadata_by_size(
597 + 'comment',
598 + $comments,
599 + $metadata,
600 + self::MAX_META_LENGTH, // Replace with appropriate comment meta length constant.
601 + self::MAX_SIZE_FULL_SYNC
602 + );
603 +
604 + return array(
605 + 'object_ids' => $filtered_comment_ids,
606 + 'objects' => $filtered_comments,
607 + 'meta' => $filtered_comments_metadata,
608 + );
609 + }
610 +}