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
activitypub / includes / collection / class-interactions.php

class-interactions.php in ActivityPub 5.3.1, at includes/collection/class-interactions.php

343 lines 9.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Interactions collection file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Collection;
9
10 use WP_Comment_Query;
11 use Activitypub\Comment;
12
13 use function Activitypub\object_to_uri;
14 use function Activitypub\is_post_disabled;
15 use function Activitypub\url_to_commentid;
16 use function Activitypub\object_id_to_comment;
17 use function Activitypub\get_remote_metadata_by_actor;
18
19 /**
20 * ActivityPub Interactions Collection.
21 */
22 class Interactions {
23 const INSERT = 'insert';
24 const UPDATE = 'update';
25
26 /**
27 * Add a comment to a post.
28 *
29 * @param array $activity The activity-object.
30 *
31 * @return array|false The comment data or false on failure.
32 */
33 public static function add_comment( $activity ) {
34 $commentdata = self::activity_to_comment( $activity );
35
36 if ( ! $commentdata || ! isset( $activity['object']['inReplyTo'] ) ) {
37 return false;
38 }
39
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 );
44
45 // Save only replies and reactions.
46 if ( ! $comment_post_id && $parent_comment_id ) {
47 $parent_comment = get_comment( $parent_comment_id );
48 $comment_post_id = $parent_comment->comment_post_ID;
49 }
50
51 if ( is_post_disabled( $comment_post_id ) ) {
52 return false;
53 }
54
55 $commentdata['comment_post_ID'] = $comment_post_id;
56 $commentdata['comment_parent'] = $parent_comment_id ? $parent_comment_id : 0;
57
58 return self::persist( $commentdata, self::INSERT );
59 }
60
61 /**
62 * Update a comment.
63 *
64 * @param array $activity The activity object.
65 *
66 * @return array|string|int|\WP_Error|false The comment data or false on failure.
67 */
68 public static function update_comment( $activity ) {
69 $meta = get_remote_metadata_by_actor( $activity['actor'] );
70
71 // Determine comment_ID.
72 $comment = object_id_to_comment( \esc_url_raw( $activity['object']['id'] ) );
73 $commentdata = \get_comment( $comment, ARRAY_A );
74
75 if ( ! $commentdata ) {
76 return false;
77 }
78
79 // Found a local comment id.
80 $commentdata['comment_author'] = \esc_attr( $meta['name'] ? $meta['name'] : $meta['preferredUsername'] );
81 $commentdata['comment_content'] = \addslashes( $activity['object']['content'] );
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;
98 }
99
100 $url = object_to_uri( $activity['object'] );
101 $comment_post_id = \url_to_postid( $url );
102 $parent_comment_id = url_to_commentid( $url );
103
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 }
108
109 if ( ! $comment_post_id || is_post_disabled( $comment_post_id ) ) {
110 // Not a reply to a post or comment.
111 return false;
112 }
113
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;
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 );
129 }
130
131 /**
132 * Get interaction(s) for a given URL/ID.
133 *
134 * @param string $url The URL/ID to get interactions for.
135 *
136 * @return array The interactions as WP_Comment objects.
137 */
138 public static function get_interaction_by_id( $url ) {
139 $args = array(
140 'nopaging' => true,
141 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
142 'meta_query' => array(
143 'relation' => 'AND',
144 array(
145 'key' => 'protocol',
146 'value' => 'activitypub',
147 ),
148 array(
149 'relation' => 'OR',
150 array(
151 'key' => 'source_url',
152 'value' => $url,
153 ),
154 array(
155 'key' => 'source_id',
156 'value' => $url,
157 ),
158 ),
159 ),
160 );
161
162 $query = new WP_Comment_Query( $args );
163 return $query->comments;
164 }
165
166 /**
167 * Get interaction(s) for a given actor.
168 *
169 * @param string $actor The Actor-URL.
170 *
171 * @return array The interactions as WP_Comment objects.
172 */
173 public static function get_interactions_by_actor( $actor ) {
174 $meta = get_remote_metadata_by_actor( $actor );
175
176 // Get URL, because $actor seems to be the ID.
177 if ( $meta && ! is_wp_error( $meta ) && isset( $meta['url'] ) ) {
178 $actor = object_to_uri( $meta['url'] );
179 }
180
181 $args = array(
182 'nopaging' => true,
183 'author_url' => $actor,
184 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
185 'meta_query' => array(
186 array(
187 'key' => 'protocol',
188 'value' => 'activitypub',
189 'compare' => '=',
190 ),
191 ),
192 );
193 $comment_query = new WP_Comment_Query( $args );
194 return $comment_query->comments;
195 }
196
197 /**
198 * Adds line breaks to the list of allowed comment tags.
199 *
200 * @param array $allowed_tags Allowed HTML tags.
201 * @param string $context Optional. Context. Default empty.
202 *
203 * @return array Filtered tag list.
204 */
205 public static function allowed_comment_html( $allowed_tags, $context = '' ) {
206 if ( 'pre_comment_content' !== $context ) {
207 // Do nothing.
208 return $allowed_tags;
209 }
210
211 // Add `p` and `br` to the list of allowed tags.
212 if ( ! array_key_exists( 'br', $allowed_tags ) ) {
213 $allowed_tags['br'] = array();
214 }
215
216 if ( ! array_key_exists( 'p', $allowed_tags ) ) {
217 $allowed_tags['p'] = array();
218 }
219
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 );
341 }
342 }
343