PluginProbe
ActivityPub / 4.0.2
ActivityPub v4.0.2
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 4.0.2, at includes/collection/class-interactions.php

322 lines 8.7 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\url_to_commentid;
15 use function Activitypub\object_id_to_comment;
16 use function Activitypub\get_remote_metadata_by_actor;
17
18 /**
19 * ActivityPub Interactions Collection.
20 */
21 class Interactions {
22 const INSERT = 'insert';
23 const UPDATE = 'update';
24
25 /**
26 * Add a comment to a post.
27 *
28 * @param array $activity The activity-object.
29 *
30 * @return array|false The comment data or false on failure.
31 */
32 public static function add_comment( $activity ) {
33 $commentdata = self::activity_to_comment( $activity );
34
35 if ( ! $commentdata || ! isset( $activity['object']['inReplyTo'] ) ) {
36 return false;
37 }
38
39 $in_reply_to = \esc_url_raw( $activity['object']['inReplyTo'] );
40 $comment_post_id = \url_to_postid( $in_reply_to );
41 $parent_comment_id = url_to_commentid( $in_reply_to );
42
43 // Save only replies and reactions.
44 if ( ! $comment_post_id && $parent_comment_id ) {
45 $parent_comment = get_comment( $parent_comment_id );
46 $comment_post_id = $parent_comment->comment_post_ID;
47 }
48
49 // Not a reply to a post or comment.
50 if ( ! $comment_post_id ) {
51 return false;
52 }
53
54 $commentdata['comment_post_ID'] = $comment_post_id;
55 $commentdata['comment_parent'] = $parent_comment_id ? $parent_comment_id : 0;
56
57 return self::persist( $commentdata, self::INSERT );
58 }
59
60 /**
61 * Update a comment.
62 *
63 * @param array $activity The activity object.
64 *
65 * @return array|string|int|\WP_Error|false The comment data or false on failure.
66 */
67 public static function update_comment( $activity ) {
68 $meta = get_remote_metadata_by_actor( $activity['actor'] );
69
70 // Determine comment_ID.
71 $comment = object_id_to_comment( \esc_url_raw( $activity['object']['id'] ) );
72 $commentdata = \get_comment( $comment, ARRAY_A );
73
74 if ( ! $commentdata ) {
75 return false;
76 }
77
78 // Found a local comment id.
79 $commentdata['comment_author'] = \esc_attr( $meta['name'] ? $meta['name'] : $meta['preferredUsername'] );
80 $commentdata['comment_content'] = \addslashes( $activity['object']['content'] );
81
82 return self::persist( $commentdata, self::UPDATE );
83 }
84
85 /**
86 * Adds an incoming Like, Announce, ... as a comment to a post.
87 *
88 * @param array $activity Activity array.
89 *
90 * @return array|false Comment data or `false` on failure.
91 */
92 public static function add_reaction( $activity ) {
93 $commentdata = self::activity_to_comment( $activity );
94
95 if ( ! $commentdata ) {
96 return false;
97 }
98
99 $url = object_to_uri( $activity['object'] );
100 $comment_post_id = url_to_postid( $url );
101 $parent_comment_id = url_to_commentid( $url );
102
103 if ( ! $comment_post_id && $parent_comment_id ) {
104 $parent_comment = get_comment( $parent_comment_id );
105 $comment_post_id = $parent_comment->comment_post_ID;
106 }
107
108 if ( ! $comment_post_id ) {
109 // Not a reply to a post or comment.
110 return false;
111 }
112
113 $type = $activity['type'];
114
115 if ( ! Comment::is_registered_comment_type( $type ) ) {
116 // Not a valid comment type.
117 return false;
118 }
119
120 $comment_type = Comment::get_comment_type( $type );
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'] );
250
251 if ( ! $url ) {
252 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