PluginProbe
ActivityPub / 5.3.1
ActivityPub v5.3.1
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
← All changes | includes/collection/class-interactions.php +201 -102 2.1.05.3.1 View file →
@@ -1,114 +1,75 @@
1 1 <?php
2 +/**
3 + * Interactions collection file.
4 + *
5 + * @package Activitypub
6 + */
7 +
2 8 namespace Activitypub\Collection;
3 9
4 -use WP_Error;
5 10 use WP_Comment_Query;
11 +use Activitypub\Comment;
6 12
13 +use function Activitypub\object_to_uri;
14 +use function Activitypub\is_post_disabled;
7 15 use function Activitypub\url_to_commentid;
8 16 use function Activitypub\object_id_to_comment;
9 17 use function Activitypub\get_remote_metadata_by_actor;
10 18
11 19 /**
12 - * ActivityPub Interactions Collection
20 + * ActivityPub Interactions Collection.
13 21 */
14 22 class Interactions {
23 + const INSERT = 'insert';
24 + const UPDATE = 'update';
25 +
15 26 /**
16 - * Add a comment to a post
27 + * Add a comment to a post.
17 28 *
18 - * @param array $activity The activity-object
29 + * @param array $activity The activity-object.
19 30 *
20 - * @return array|false The commentdata or false on failure
31 + * @return array|false The comment data or false on failure.
21 32 */
22 33 public static function add_comment( $activity ) {
23 - if (
24 - ! isset( $activity['object'] ) ||
25 - ! isset( $activity['object']['id'] )
26 - ) {
27 - return false;
28 - }
34 + $commentdata = self::activity_to_comment( $activity );
29 35
30 - if ( ! isset( $activity['object']['inReplyTo'] ) ) {
36 + if ( ! $commentdata || ! isset( $activity['object']['inReplyTo'] ) ) {
31 37 return false;
32 38 }
33 39
34 - $in_reply_to = \esc_url_raw( $activity['object']['inReplyTo'] );
35 - $comment_post_id = \url_to_postid( $in_reply_to );
36 - $parent_comment_id = url_to_commentid( $in_reply_to );
40 + $in_reply_to = object_to_uri( $activity['object']['inReplyTo'] );
41 + $in_reply_to = \esc_url_raw( $in_reply_to );
42 + $comment_post_id = \url_to_postid( $in_reply_to );
43 + $parent_comment_id = url_to_commentid( $in_reply_to );
37 44
38 - // save only replys and reactions
45 + // Save only replies and reactions.
39 46 if ( ! $comment_post_id && $parent_comment_id ) {
40 47 $parent_comment = get_comment( $parent_comment_id );
41 48 $comment_post_id = $parent_comment->comment_post_ID;
42 49 }
43 50
44 - // not a reply to a post or comment
45 - if ( ! $comment_post_id ) {
51 + if ( is_post_disabled( $comment_post_id ) ) {
46 52 return false;
47 53 }
48 54
49 - $meta = get_remote_metadata_by_actor( $activity['actor'] );
55 + $commentdata['comment_post_ID'] = $comment_post_id;
56 + $commentdata['comment_parent'] = $parent_comment_id ? $parent_comment_id : 0;
50 57
51 - if ( ! $meta || \is_wp_error( $meta ) ) {
52 - return false;
53 - }
54 -
55 - $commentdata = array(
56 - 'comment_post_ID' => $comment_post_id,
57 - 'comment_author' => isset( $meta['name'] ) ? \esc_attr( $meta['name'] ) : \esc_attr( $meta['preferredUsername'] ),
58 - 'comment_author_url' => \esc_url_raw( $meta['url'] ),
59 - 'comment_content' => \addslashes( $activity['object']['content'] ),
60 - 'comment_type' => 'comment',
61 - 'comment_author_email' => '',
62 - 'comment_parent' => $parent_comment_id ? $parent_comment_id : 0,
63 - 'comment_meta' => array(
64 - 'source_id' => \esc_url_raw( $activity['object']['id'] ),
65 - 'protocol' => 'activitypub',
66 - ),
67 - );
68 -
69 - if ( isset( $meta['icon']['url'] ) ) {
70 - $commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $meta['icon']['url'] );
71 - }
72 -
73 - if ( isset( $activity['object']['url'] ) ) {
74 - $commentdata['comment_meta']['source_url'] = \esc_url_raw( $activity['object']['url'] );
75 - }
76 -
77 - // disable flood control
78 - \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
79 - // do not require email for AP entries
80 - \add_filter( 'pre_option_require_name_email', '__return_false' );
81 - // No nonce possible for this submission route
82 - \add_filter(
83 - 'akismet_comment_nonce',
84 - function () {
85 - return 'inactive';
86 - }
87 - );
88 - \add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
89 -
90 - $comment = \wp_new_comment( $commentdata, true );
91 -
92 - \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 );
93 - \remove_filter( 'pre_option_require_name_email', '__return_false' );
94 - // re-add flood control
95 - \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
96 -
97 - return $comment;
58 + return self::persist( $commentdata, self::INSERT );
98 59 }
99 60
100 61 /**
101 - * Update a comment
62 + * Update a comment.
102 63 *
103 - * @param array $activity The activity-object
64 + * @param array $activity The activity object.
104 65 *
105 - * @return array|string|int|\WP_Error|false The commentdata or false on failure
66 + * @return array|string|int|\WP_Error|false The comment data or false on failure.
106 67 */
107 68 public static function update_comment( $activity ) {
108 69 $meta = get_remote_metadata_by_actor( $activity['actor'] );
109 70
110 - //Determine comment_ID
71 + // Determine comment_ID.
111 72 $comment = object_id_to_comment( \esc_url_raw( $activity['object']['id'] ) );
112 73 $commentdata = \get_comment( $comment, ARRAY_A );
113 74
114 75 if ( ! $commentdata ) {
@@ -114,46 +75,64 @@
114 75 if ( ! $commentdata ) {
115 76 return false;
116 77 }
117 78
118 - //found a local comment id
119 - $commentdata['comment_author'] = \esc_attr( $meta['name'] ? $meta['name'] : $meta['preferredUsername'] );
79 + // Found a local comment id.
80 + $commentdata['comment_author'] = \esc_attr( $meta['name'] ? $meta['name'] : $meta['preferredUsername'] );
120 81 $commentdata['comment_content'] = \addslashes( $activity['object']['content'] );
121 - if ( isset( $meta['icon']['url'] ) ) {
122 - $commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $meta['icon']['url'] );
82 +
83 + return self::persist( $commentdata, self::UPDATE );
84 + }
85 +
86 + /**
87 + * Adds an incoming Like, Announce, ... as a comment to a post.
88 + *
89 + * @param array $activity Activity array.
90 + *
91 + * @return array|false Comment data or `false` on failure.
92 + */
93 + public static function add_reaction( $activity ) {
94 + $commentdata = self::activity_to_comment( $activity );
95 +
96 + if ( ! $commentdata ) {
97 + return false;
123 98 }
124 99
125 - // disable flood control
126 - \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
127 - // do not require email for AP entries
128 - \add_filter( 'pre_option_require_name_email', '__return_false' );
129 - // No nonce possible for this submission route
130 - \add_filter(
131 - 'akismet_comment_nonce',
132 - function () {
133 - return 'inactive';
134 - }
135 - );
136 - \add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
100 + $url = object_to_uri( $activity['object'] );
101 + $comment_post_id = \url_to_postid( $url );
102 + $parent_comment_id = url_to_commentid( $url );
137 103
138 - $state = \wp_update_comment( $commentdata, true );
104 + if ( ! $comment_post_id && $parent_comment_id ) {
105 + $parent_comment = \get_comment( $parent_comment_id );
106 + $comment_post_id = $parent_comment->comment_post_ID;
107 + }
139 108
140 - \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 );
141 - \remove_filter( 'pre_option_require_name_email', '__return_false' );
142 - // re-add flood control
143 - \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
109 + if ( ! $comment_post_id || is_post_disabled( $comment_post_id ) ) {
110 + // Not a reply to a post or comment.
111 + return false;
112 + }
144 113
145 - if ( 1 === $state ) {
146 - return $commentdata;
147 - } else {
148 - return $state; // Either `false` or a `WP_Error` instance or `0` or `1`!
114 + $comment_type = Comment::get_comment_type_by_activity_type( $activity['type'] );
115 +
116 + if ( ! $comment_type ) {
117 + // Not a valid comment type.
118 + return false;
149 119 }
120 +
121 + $comment_content = $comment_type['excerpt'];
122 +
123 + $commentdata['comment_post_ID'] = $comment_post_id;
124 + $commentdata['comment_content'] = \esc_html( $comment_content );
125 + $commentdata['comment_type'] = \esc_attr( $comment_type['type'] );
126 + $commentdata['comment_meta']['source_id'] = \esc_url_raw( $activity['id'] );
127 +
128 + return self::persist( $commentdata, self::INSERT );
150 129 }
151 130
152 131 /**
153 132 * Get interaction(s) for a given URL/ID.
154 133 *
155 - * @param strin $url The URL/ID to get interactions for.
134 + * @param string $url The URL/ID to get interactions for.
156 135 *
157 136 * @return array The interactions as WP_Comment objects.
158 137 */
159 138 public static function get_interaction_by_id( $url ) {
@@ -193,21 +172,21 @@
193 172 */
194 173 public static function get_interactions_by_actor( $actor ) {
195 174 $meta = get_remote_metadata_by_actor( $actor );
196 175
197 - // get URL, because $actor seems to be the ID
176 + // Get URL, because $actor seems to be the ID.
198 177 if ( $meta && ! is_wp_error( $meta ) && isset( $meta['url'] ) ) {
199 - $actor = $meta['url'];
178 + $actor = object_to_uri( $meta['url'] );
200 179 }
201 180
202 - $args = array(
181 + $args = array(
203 182 'nopaging' => true,
204 183 'author_url' => $actor,
205 184 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
206 185 'meta_query' => array(
207 186 array(
208 - 'key' => 'protocol',
209 - 'value' => 'activitypub',
187 + 'key' => 'protocol',
188 + 'value' => 'activitypub',
210 189 'compare' => '=',
211 190 ),
212 191 ),
213 192 );
@@ -218,9 +197,9 @@
218 197 /**
219 198 * Adds line breaks to the list of allowed comment tags.
220 199 *
221 200 * @param array $allowed_tags Allowed HTML tags.
222 - * @param string $context Context.
201 + * @param string $context Optional. Context. Default empty.
223 202 *
224 203 * @return array Filtered tag list.
225 204 */
226 205 public static function allowed_comment_html( $allowed_tags, $context = '' ) {
@@ -238,6 +217,126 @@
238 217 $allowed_tags['p'] = array();
239 218 }
240 219
241 220 return $allowed_tags;
221 + }
222 +
223 + /**
224 + * Convert an Activity to a WP_Comment
225 + *
226 + * @param array $activity The Activity array.
227 + *
228 + * @return array|false The comment data or false on failure.
229 + */
230 + public static function activity_to_comment( $activity ) {
231 + $comment_content = null;
232 + $actor = object_to_uri( $activity['actor'] );
233 + $actor = get_remote_metadata_by_actor( $actor );
234 +
235 + // Check Actor-Meta.
236 + if ( ! $actor || is_wp_error( $actor ) ) {
237 + return false;
238 + }
239 +
240 + // Check Actor-Name.
241 + if ( isset( $actor['name'] ) ) {
242 + $comment_author = $actor['name'];
243 + } elseif ( isset( $actor['preferredUsername'] ) ) {
244 + $comment_author = $actor['preferredUsername'];
245 + } else {
246 + return false;
247 + }
248 +
249 + $url = object_to_uri( $actor['url'] ?? $actor['id'] );
250 +
251 + if ( ! $url ) {
252 + $url = object_to_uri( $actor['id'] );
253 + }
254 +
255 + if ( isset( $activity['object']['content'] ) ) {
256 + $comment_content = \addslashes( $activity['object']['content'] );
257 + }
258 +
259 + $commentdata = array(
260 + 'comment_author' => \esc_attr( $comment_author ),
261 + 'comment_author_url' => \esc_url_raw( $url ),
262 + 'comment_content' => $comment_content,
263 + 'comment_type' => 'comment',
264 + 'comment_author_email' => '',
265 + 'comment_meta' => array(
266 + 'source_id' => \esc_url_raw( object_to_uri( $activity['object'] ) ),
267 + 'protocol' => 'activitypub',
268 + ),
269 + );
270 +
271 + if ( isset( $actor['icon']['url'] ) ) {
272 + $commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $actor['icon']['url'] );
273 + }
274 +
275 + if ( isset( $activity['object']['url'] ) ) {
276 + $commentdata['comment_meta']['source_url'] = \esc_url_raw( object_to_uri( $activity['object']['url'] ) );
277 + }
278 +
279 + return $commentdata;
280 + }
281 +
282 + /**
283 + * Persist a comment.
284 + *
285 + * @param array $commentdata The commentdata array.
286 + * @param string $action Optional. Either 'insert' or 'update'. Default 'insert'.
287 + *
288 + * @return array|string|int|\WP_Error|false The comment data or false on failure
289 + */
290 + public static function persist( $commentdata, $action = self::INSERT ) {
291 + // Disable flood control.
292 + \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
293 + // Do not require email for AP entries.
294 + \add_filter( 'pre_option_require_name_email', '__return_false' );
295 + // No nonce possible for this submission route.
296 + \add_filter(
297 + 'akismet_comment_nonce',
298 + function () {
299 + return 'inactive';
300 + }
301 + );
302 + \add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
303 +
304 + if ( self::INSERT === $action ) {
305 + $state = \wp_new_comment( $commentdata, true );
306 + } else {
307 + $state = \wp_update_comment( $commentdata, true );
308 + }
309 +
310 + \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 );
311 + \remove_filter( 'pre_option_require_name_email', '__return_false' );
312 + // Restore flood control.
313 + \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
314 +
315 + if ( 1 === $state ) {
316 + return $commentdata;
317 + } else {
318 + return $state; // Either WP_Comment, false, a WP_Error, 0, or 1!
319 + }
320 + }
321 +
322 + /**
323 + * Get the total number of interactions by type for a given ID.
324 + *
325 + * @param int $post_id The post ID.
326 + * @param string $type The type of interaction to count.
327 + *
328 + * @return int The total number of interactions.
329 + */
330 + public static function count_by_type( $post_id, $type ) {
331 + return \get_comments(
332 + array(
333 + 'post_id' => $post_id,
334 + 'status' => 'approve',
335 + 'type' => $type,
336 + 'count' => true,
337 + 'paging' => false,
338 + 'fields' => 'ids',
339 + )
340 + );
242 341 }
243 342 }