PluginProbe
ActivityPub / 7.0.0
ActivityPub v7.0.0
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 7.0.0, at includes/collection/class-interactions.php

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