PluginProbe
Friends / 2.9.1
Friends v2.9.1
4.3.2 4.3.1 4.3.0 4.2.2 4.2.1 4.2.0 4.1.0 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.1 All 88 releases
friends / feed-parsers / class-feed-parser-activitypub.php

class-feed-parser-activitypub.php in Friends 2.9.1, at feed-parsers/class-feed-parser-activitypub.php

1,971 lines 60.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Friends ActivityPub Parser
4 *
5 * With this parser, we can talk to ActivityPub.
6 *
7 * @since @2.1.4
8 *
9 * @package Friends
10 * @author Alex Kirk
11 */
12
13 namespace Friends;
14
15 use WP_Error;
16
17 /**
18 * This is the class for integrating ActivityPub into the Friends Plugin.
19 */
20 class Feed_Parser_ActivityPub extends Feed_Parser_V2 {
21 const SLUG = 'activitypub';
22 const NAME = 'ActivityPub';
23 const URL = 'https://www.w3.org/TR/activitypub/';
24 const ACTIVITYPUB_USERNAME_REGEXP = '(?:([A-Za-z0-9_-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))';
25 const EXTERNAL_MENTIONS_USERNAME = 'external-mentions';
26
27 private $friends_feed;
28
29 /**
30 * Constructor.
31 *
32 * @param Feed $friends_feed The friends feed.
33 */
34 public function __construct( Feed $friends_feed ) {
35 $this->friends_feed = $friends_feed;
36
37 \add_action( 'init', array( $this, 'register_post_meta' ) );
38 \add_action( 'admin_menu', array( $this, 'admin_menu' ), 20 );
39 \add_filter( 'feed_item_allow_set_metadata', array( $this, 'feed_item_allow_set_metadata' ), 10, 3 );
40 \add_filter( 'friends_add_friends_input_placeholder', array( $this, 'friends_add_friends_input_placeholder' ) );
41 \add_action( 'friends_add_friend_form_top', array( $this, 'friends_add_friend_form_top' ) );
42
43 \add_action( 'activitypub_inbox', array( $this, 'handle_received_activity' ), 10, 3 );
44 \add_action( 'friends_user_feed_activated', array( $this, 'queue_follow_user' ), 10 );
45 \add_action( 'friends_user_feed_deactivated', array( $this, 'queue_unfollow_user' ), 10 );
46 \add_action( 'friends_feed_parser_activitypub_follow', array( $this, 'activitypub_follow_user' ), 10, 2 );
47 \add_action( 'friends_feed_parser_activitypub_unfollow', array( $this, 'activitypub_unfollow_user' ), 10, 2 );
48 \add_action( 'friends_feed_parser_activitypub_like', array( $this, 'activitypub_like_post' ), 10, 3 );
49 \add_action( 'friends_feed_parser_activitypub_unlike', array( $this, 'activitypub_unlike_post' ), 10, 3 );
50 \add_action( 'friends_feed_parser_activitypub_announce', array( $this, 'activitypub_announce' ), 10, 2 );
51 \add_action( 'friends_feed_parser_activitypub_unannounce', array( $this, 'activitypub_unannounce' ), 10, 2 );
52 \add_filter( 'friends_rewrite_incoming_url', array( $this, 'friends_webfinger_resolve' ), 10, 2 );
53
54 \add_filter( 'friends_edit_feeds_table_end', array( $this, 'activitypub_settings' ), 10 );
55 \add_filter( 'friends_edit_feeds_after_form_submit', array( $this, 'activitypub_save_settings' ), 10 );
56 \add_filter( 'friends_modify_feed_item', array( $this, 'modify_incoming_item' ), 9, 3 );
57 \add_filter( 'friends_potential_avatars', array( $this, 'friends_potential_avatars' ), 10, 2 );
58 \add_filter( 'friends_suggest_user_login', array( $this, 'suggest_user_login' ), 10, 2 );
59
60 \add_filter( 'the_content', array( $this, 'the_content' ), 99, 2 );
61 \add_filter( 'activitypub_extract_mentions', array( $this, 'activitypub_extract_mentions' ), 10, 2 );
62 \add_filter( 'mastodon_api_external_mentions_user', array( $this, 'get_external_mentions_user' ) );
63 \add_filter( 'activitypub_post', array( $this, 'activitypub_post_in_reply_to' ), 10, 2 );
64 \add_filter( 'activitypub_activity_object_array', array( $this, 'activitypub_activity_object_array_in_reply_to' ), 10, 3 );
65
66 \add_action( 'friends_user_post_reaction', array( $this, 'post_reaction' ) );
67 \add_action( 'friends_user_post_undo_reaction', array( $this, 'undo_post_reaction' ) );
68 \add_action( 'mastodon_api_react', array( $this, 'mastodon_api_react' ), 10, 2 );
69 \add_action( 'mastodon_api_unreact', array( $this, 'mastodon_api_unreact' ), 10, 2 );
70 \add_action( 'friends_get_reaction_display_name', array( $this, 'get_reaction_display_name' ), 10, 2 );
71
72 \add_filter( 'pre_comment_approved', array( $this, 'pre_comment_approved' ), 10, 2 );
73 \add_action( 'comment_post', array( $this, 'comment_post' ), 10, 3 );
74 \add_action( 'trashed_comment', array( $this, 'trashed_comment' ), 10, 2 );
75
76 \add_filter( 'friends_reblog_button_label', array( $this, 'friends_reblog_button_label' ), 10, 2 );
77 \add_filter( 'friends_search_autocomplete', array( $this, 'friends_search_autocomplete' ), 10, 2 );
78 \add_action( 'friends_after_header', array( $this, 'frontend_reply_form' ) );
79 \add_action( 'friends_after_header', array( $this, 'frontend_boost_form' ) );
80 \add_action( 'wp_ajax_friends-in-reply-to-preview', array( $this, 'ajax_in_reply_to_preview' ) );
81
82 \add_filter( 'friends_reblog', array( $this, 'unqueue_activitypub_create' ), 9 );
83 \add_action( 'mastodon_api_reblog', array( $this, 'mastodon_api_reblog' ) );
84 \add_action( 'mastodon_api_unreblog', array( $this, 'mastodon_api_unreblog' ) );
85 \add_filter( 'friends_activitypub_announce_any_url', array( $this, 'queue_announce' ) );
86 \add_filter( 'friends_reblog', array( $this, 'reblog' ), 20, 2 );
87 \add_filter( 'friends_unreblog', array( $this, 'unreblog' ), 20, 2 );
88 \add_filter( 'friends_reblog', array( $this, 'maybe_unqueue_friends_reblog_post' ), 9, 2 );
89
90 \add_filter( 'pre_get_remote_metadata_by_actor', array( $this, 'disable_webfinger_for_example_domains' ), 9, 2 );
91
92 add_filter( 'friends_get_feed_metadata', array( $this, 'friends_get_feed_metadata' ), 10, 2 );
93 add_filter( 'friends_get_activitypub_metadata', array( $this, 'friends_activitypub_metadata' ), 10, 2 );
94 }
95
96 /**
97 * Add the admin menu to the sidebar.
98 */
99 public function admin_menu() {
100 $friends = Friends::get_instance();
101 $unread_badge = $friends->admin->get_unread_badge();
102
103 $menu_title = __( 'Friends', 'friends' ) . $unread_badge;
104 $page_type = sanitize_title( $menu_title );
105
106 add_submenu_page(
107 'friends',
108 __( 'ActivityPub', 'friends' ),
109 __( 'ActivityPub', 'friends' ),
110 Friends::required_menu_role(),
111 'friends-activitypub-settings',
112 array( $this, 'settings' )
113 );
114
115 add_action( 'load-' . $page_type . '_page_friends-activitypub-settings', array( $this, 'process_settings' ) );
116 }
117
118 public function settings() {
119 Friends::template_loader()->get_template_part(
120 'admin/settings-header',
121 null,
122 array(
123 'menu' => array(
124 __( 'ActivityPub Settings', 'friends' ) => 'friends-activitypub-settings',
125 ),
126 'active' => 'friends-activitypub-settings',
127 'title' => __( 'Friends', 'friends' ),
128 )
129 );
130
131 if (
132 // This just displays an info text.
133 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
134 isset( $_GET['updated'] )
135 ) {
136 ?>
137 <div id="message" class="updated notice is-dismissible"><p><?php esc_html_e( 'Settings were updated.', 'friends' ); ?></p></div>
138 <?php
139 } elseif (
140 // This just displays an info text.
141 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
142 isset( $_GET['error'] )
143 ) {
144 ?>
145 <div id="message" class="updated error is-dismissible"><p><?php esc_html_e( 'An error occurred.', 'friends' ); ?></p></div>
146 <?php
147 }
148
149 Friends::template_loader()->get_template_part(
150 'admin/activitypub-settings',
151 null,
152 array(
153 'reblog' => ! get_user_option( 'friends_activitypub_dont_reblog' ),
154 )
155 );
156
157 Friends::template_loader()->get_template_part( 'admin/settings-footer' );
158 }
159
160 public function process_settings() {
161 if ( empty( $_POST ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'friends-activitypub-settings' ) ) {
162 return;
163 }
164
165 if ( isset( $_POST['activitypub_reblog'] ) ) {
166 delete_user_option( get_current_user_id(), 'friends_activitypub_dont_reblog' );
167 } else {
168 update_user_option( get_current_user_id(), 'friends_activitypub_dont_reblog', true );
169 }
170 wp_safe_redirect( add_query_arg( 'updated', 'true', admin_url( 'admin.php?page=friends-activitypub-settings' ) ) );
171 exit;
172 }
173
174 public function friends_add_friends_input_placeholder() {
175 return __( 'Enter URL or @activitypub@handle.domain', 'friends' );
176 }
177
178 public function friends_add_friend_form_top() {
179 ?>
180 <p>
181 <?php
182 echo wp_kses(
183 sprintf(
184 // translators: %1$s and %2$s are links to the respective services.
185 __( '<strong>Note:</strong> Because you have the ActivityPub plugin installed, you can also follow people over that protocol, for example <a href=%1$s>Mastodon</a> or <a href=%2$s>Pixelfed</a>.', 'friends' ),
186 '"https://joinmastodon.org/"',
187 '"https://pixelfed.social/"'
188 ),
189 array(
190 'a' => array( 'href' => array() ),
191 'strong' => array(),
192 )
193 );
194 ?>
195 </p>
196 <?php
197 }
198
199 public function friends_get_feed_metadata( $meta, $feed ) {
200 if ( self::SLUG === $feed->get_parser() ) {
201 return $this->friends_activitypub_metadata( $meta, $feed->get_url() );
202 }
203 return $meta;
204 }
205
206 public function friends_activitypub_metadata( $ret, $url ) {
207 $meta = self::get_metadata( $url );
208 if ( is_null( $meta ) ) {
209 return $ret;
210 }
211 if ( is_wp_error( $meta ) ) {
212 if ( ! empty( $ret ) ) {
213 return $ret;
214 }
215 return $meta;
216 }
217 return array_merge( $ret, $meta );
218 }
219
220 public function register_post_meta() {
221 register_post_meta(
222 Friends::CPT,
223 self::SLUG,
224 array(
225 'show_in_rest' => true,
226 'single' => true,
227 'type' => 'object',
228 )
229 );
230 }
231
232 public function feed_item_allow_set_metadata( $verdict, $key, $value ) {
233 if ( self::SLUG === $key && ! empty( $value ) ) {
234 // We don't want to insert empty post meta.
235 return true;
236 }
237 return $verdict;
238 }
239
240 /**
241 * Allow logging a message via an action.
242 *
243 * @param string $message The message to log.
244 * @param array $objects Optional objects as meta data.
245 * @return void
246 */
247 private function log( $message, $objects = array() ) {
248 // Receive debug messages from the ActivityPub feed parser.
249 do_action( 'friends_activitypub_log', $message, $objects );
250 }
251
252 /**
253 * Determines if this is a supported feed and to what degree we feel it's supported.
254 *
255 * @param string $url The url.
256 * @param string $mime_type The mime type.
257 * @param string $title The title.
258 * @param string|null $content The content, it can't be assumed that it's always available.
259 *
260 * @return int Return 0 if unsupported, a positive value representing the confidence for the feed, use 10 if you're reasonably confident.
261 */
262 public function feed_support_confidence( $url, $mime_type, $title, $content = null ) {
263 if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $url ) ) {
264 return 10;
265 }
266
267 if ( 'application/activity+json' === $mime_type ) {
268 return 10;
269 }
270
271 if ( preg_match( '#^https?://[^/]+/@[a-z0-9-]+$#i', $url ) ) {
272 return 9;
273 }
274
275 if ( preg_match( '#^https?://[^/]+/(users|author)/[a-z0-9-]+$#i', $url ) ) {
276 return 8;
277 }
278
279 return 0;
280 }
281
282 /**
283 * Format the feed title and autoselect the posts feed.
284 *
285 * @param array $feed_details The feed details.
286 *
287 * @return array The (potentially) modified feed details.
288 */
289 public function update_feed_details( $feed_details ) {
290 $meta = apply_filters( 'friends_get_activitypub_metadata', array(), $feed_details['url'] );
291 if ( ! $meta || is_wp_error( $meta ) ) {
292 return $feed_details;
293 }
294 if ( isset( $meta['name'] ) ) {
295 $feed_details['title'] = $meta['name'];
296 } elseif ( isset( $meta['preferredUsername'] ) ) {
297 $feed_details['title'] = $meta['preferredUsername'];
298 }
299
300 if ( isset( $meta['id'] ) ) {
301 $feed_details['url'] = $meta['id'];
302 }
303
304 if ( isset( $meta['icon']['type'] ) && 'image' === strtolower( $meta['icon']['type'] ) ) {
305 $feed_details['avatar'] = $meta['icon']['url'];
306 }
307
308 if ( ! empty( $meta['summary'] ) ) {
309 $feed_details['description'] = $meta['summary'];
310 }
311
312 // Disable polling.
313 $feed_details['interval'] = YEAR_IN_SECONDS;
314 $feed_details['next-poll'] = gmdate( 'Y-m-d H:i:s', time() + YEAR_IN_SECONDS );
315 $feed_details['post-format'] = 'status';
316
317 return $feed_details;
318 }
319
320 /**
321 * Rewrite a Mastodon style URL @username@server to a URL via webfinger.
322 *
323 * @param string $url The URL to filter.
324 * @param string $incoming_url Potentially a mastodon identifier.
325 *
326 * @return string The rewritten URL.
327 */
328 public function friends_webfinger_resolve( $url, $incoming_url ) {
329 if ( preg_match( '/^@?' . self::ACTIVITYPUB_USERNAME_REGEXP . '$/i', $incoming_url ) ) {
330 $resolved_url = \Activitypub\Webfinger::resolve( $incoming_url );
331 if ( ! is_wp_error( $resolved_url ) ) {
332 return $resolved_url;
333 }
334 }
335 return $url;
336 }
337
338 /**
339 * Get the inbox URL for an actor
340 *
341 * @param string $url The URL of the actor.
342 * @param string $type The type of the activity.
343 * @return string|WP_Error
344 */
345 public static function get_inbox_by_actor( $url, $type ) {
346 $metadata = self::get_metadata( $url );
347 if ( \is_wp_error( $metadata ) ) {
348 return $metadata;
349 }
350
351 if ( ! in_array( $type, array( 'Follow' ) ) && isset( $metadata['endpoints'] ) && isset( $metadata['endpoints']['sharedInbox'] ) ) {
352 return $metadata['endpoints']['sharedInbox'];
353 }
354
355 if ( \array_key_exists( 'inbox', $metadata ) ) {
356 return $metadata['inbox'];
357 }
358
359 return new WP_Error( 'activitypub_no_inbox', \__( 'No "ActivityPub Inbox" found', 'friends' ), $metadata );
360 }
361
362 public static function get_metadata( $url ) {
363 if ( ! is_string( $url ) ) {
364 return array();
365 }
366
367 if ( false !== strpos( $url, '@' ) ) {
368 if ( false === strpos( $url, '/' ) && preg_match( '#^https?://#', $url, $m ) ) {
369 $url = substr( $url, strlen( $m[0] ) );
370 }
371 return \Activitypub\get_remote_metadata_by_actor( $url );
372 }
373
374 $transient_key = 'friends_activitypub_' . crc32( $url );
375
376 $response = \get_transient( $transient_key );
377 if ( $response ) {
378 return $response;
379 }
380
381 $response = \wp_remote_get(
382 $url,
383 array(
384 'headers' => array( 'Accept' => 'application/activity+json' ),
385 'redirection' => 2,
386 'timeout' => 5,
387 )
388 );
389
390 if ( \is_wp_error( $response ) ) {
391 \set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
392 return $response;
393 }
394
395 if ( \is_wp_error( $response ) ) {
396 \set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
397 return $response;
398 }
399
400 $body = \wp_remote_retrieve_body( $response );
401 $body = \json_decode( $body, true );
402
403 \set_transient( $transient_key, $body, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
404
405 return $body;
406 }
407 /**
408 * Discover the feeds available at the URL specified.
409 *
410 * @param string $content The content for the URL is already provided here.
411 * @param string $url The url to search.
412 *
413 * @return array A list of supported feeds at the URL.
414 */
415 public function discover_available_feeds( $content, $url ) {
416 $discovered_feeds = array();
417
418 $meta = self::get_metadata( $url );
419 if ( $meta && ! is_wp_error( $meta ) && isset( $meta['id'] ) ) {
420 $discovered_feeds[ $meta['id'] ] = array(
421 'type' => 'application/activity+json',
422 'rel' => 'self',
423 'post-format' => 'status',
424 'parser' => self::SLUG,
425 'autoselect' => true,
426 );
427 }
428
429 return $discovered_feeds;
430 }
431
432 /**
433 * Fetches a feed and returns the processed items.
434 *
435 * @param string $url The url.
436 * @param User_Feed $user_feed The user feed.
437 *
438 * @return array An array of feed items.
439 */
440 public function fetch_feed( $url, User_Feed $user_feed = null ) {
441 if ( $user_feed ) {
442 $this->disable_polling( $user_feed );
443 }
444
445 // This is only for previwing.
446 if ( wp_doing_cron() ) {
447 return array();
448 }
449
450 $meta = self::get_metadata( $url );
451 if ( is_wp_error( $meta ) || ! isset( $meta['outbox'] ) ) {
452 return array();
453 }
454
455 $response = \Activitypub\safe_remote_get( $meta['outbox'], Friends::get_main_friend_user_id() );
456 if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
457 return new \WP_Error( 'activitypub_could_not_get_outbox_meta', null, compact( 'meta', 'url' ) );
458 }
459
460 $outbox = json_decode( wp_remote_retrieve_body( $response ), true );
461 if ( ! isset( $outbox['first'] ) ) {
462 return new \WP_Error( 'activitypub_could_not_find_outbox_first_page', null, compact( 'url', 'meta', 'outbox' ) );
463 }
464
465 $response = \Activitypub\safe_remote_get( $outbox['first'], Friends::get_main_friend_user_id() );
466 if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
467 return new \WP_Error(
468 'activitypub_could_not_get_outbox',
469 null,
470 array(
471 'meta' => $outbox,
472 $url => $outbox['first'],
473 )
474 );
475 }
476 $outbox_page = json_decode( wp_remote_retrieve_body( $response ), true );
477 if ( ! isset( $outbox_page['type'] ) || 'OrderedCollectionPage' !== $outbox_page['type'] ) {
478 return new \WP_Error(
479 'activitypub_outbox_page_invalid_type',
480 null,
481 array(
482 'outbox_page' => $outbox_page,
483 $url => $outbox['first'],
484 )
485 );
486 }
487
488 $items = array();
489 foreach ( $outbox_page['orderedItems'] as $object ) {
490 $type = strtolower( $object['type'] );
491 $items[] = $this->process_incoming_activity( $type, $object, get_current_user_id(), $user_feed );
492 }
493
494 return $items;
495 }
496
497 public function suggest_user_login( $login, $url ) {
498 if ( preg_match( '#^https?://([^/]+)/users/([^/]+)#', $url, $m ) ) {
499 return $m[2] . '-' . $m[1];
500 }
501 return $login;
502 }
503 /**
504 * Disable polling for this feed.
505 *
506 * @param User_Feed $user_feed The user feed.
507 * @return void
508 */
509 private function disable_polling( User_Feed $user_feed ) {
510 $user_feed->update_metadata( 'interval', YEAR_IN_SECONDS );
511 $user_feed->update_metadata( 'next-poll', gmdate( 'Y-m-d H:i:s', time() + YEAR_IN_SECONDS ) );
512 }
513
514 /**
515 * Gets the external mentions user.
516 *
517 * @return User The external mentions user.
518 */
519 public function get_external_mentions_user() {
520 $external_mentions_username = apply_filters( 'friends_external_mentions_username', self::EXTERNAL_MENTIONS_USERNAME );
521 $user = User::get_by_username( $external_mentions_username );
522 if ( ! $user || is_wp_error( $user ) ) {
523 $user = Subscription::create( $external_mentions_username, 'subscription', home_url(), __( 'External Mentions', 'friends' ) );
524 }
525
526 if ( $user instanceof \WP_User && ! ( $user instanceof Subscription ) && ! is_user_member_of_blog( $user->ID, get_current_blog_id() ) ) {
527 \add_user_to_blog( get_current_blog_id(), $user->ID, 'subscription' );
528 }
529
530 return $user;
531 }
532
533 private function get_external_mentions_feed() {
534 require_once __DIR__ . '/activitypub/class-virtual-user-feed.php';
535 $user = $this->get_external_mentions_user();
536 return new Virtual_User_Feed( $user, __( 'External Mentions', 'friends' ) );
537 }
538
539 /**
540 * Set the inReplyTo property for the outgoing activity (object).
541 *
542 * @param array $ret The activity from the ActivityPub plugin.
543 * @param WP_Post $post The post object.
544 * @return mixed
545 */
546 public function activitypub_post_in_reply_to( $ret, $post ) {
547 if ( get_post_meta( $post->ID, 'activitypub_in_reply_to', true ) ) {
548 $ret['inReplyTo'] = get_post_meta( $post->ID, 'activitypub_in_reply_to', true );
549 $ret['object']['inReplyTo'] = $ret['inReplyTo'];
550 }
551 return $ret;
552 }
553
554 /**
555 * Set the inReplyTo property for the outgoing activity object.
556 *
557 * @param array $ret The activity object from the ActivityPub plugin.
558 * @param object $c The class.
559 * @param string $url The URL of the post.
560 * @return array The modified activity object.
561 */
562 public function activitypub_activity_object_array_in_reply_to( $ret, $c, $url ) {
563 $post_id = url_to_postid( $url );
564 if ( get_post_meta( $post_id, 'activitypub_in_reply_to', true ) ) {
565 $ret['inReplyTo'] = get_post_meta( $post_id, 'activitypub_in_reply_to', true );
566 }
567 return $ret;
568 }
569
570 /**
571 * Handles "Create" requests
572 *
573 * @param array $activity The activity object.
574 * @param int $user_id The id of the local blog user.
575 * @param string $type The type of the activity.
576 */
577 public function handle_received_activity( $activity, $user_id, $type ) {
578 if ( 'undo' === $type ) {
579 if ( ! isset( $activity['object'] ) ) {
580 return false;
581 }
582 $activity = $activity['object'];
583 switch ( strtolower( $activity['type'] ) ) {
584 case 'like':
585 $type = 'unlike';
586 break;
587 case 'announce':
588 $type = 'unannounce';
589 break;
590 default:
591 return false;
592 }
593 }
594 if ( ! in_array(
595 $type,
596 array(
597 // We don't need to handle 'Accept' types since it's handled by the ActivityPub plugin itself.
598 'create',
599 'delete',
600 'announce',
601 'unannounce',
602 'like',
603 'unlike',
604 ),
605 true
606 ) ) {
607 return false;
608 }
609 $actor_url = $activity['actor'];
610 $user_feed = false;
611 if ( Friends::check_url( $actor_url ) ) {
612 // Let's check if we follow this actor. If not it might be a different URL representation.
613 $user_feed = $this->friends_feed->get_user_feed_by_url( $actor_url );
614 }
615
616 if ( is_wp_error( $user_feed ) || ! Friends::check_url( $actor_url ) ) {
617 $meta = $this->get_metadata( $actor_url );
618 if ( ! $meta || is_wp_error( $meta ) || ! isset( $meta['url'] ) ) {
619 $this->log( 'Received invalid meta for ' . $actor_url );
620 return false;
621 }
622
623 $actor_url = $meta['url'];
624 if ( ! Friends::check_url( $actor_url ) ) {
625 $this->log( 'Received invalid meta url for ' . $actor_url );
626 return false;
627 }
628 }
629
630 $user_feed = $this->friends_feed->get_user_feed_by_url( $actor_url );
631 if ( ! $user_feed || is_wp_error( $user_feed ) ) {
632 if ( isset( $activity['object']['tag'] ) && is_array( $activity['object']['tag'] ) ) {
633 $my_activitypub_id = \get_author_posts_url( $user_id );
634 foreach ( $activity['object']['tag'] as $tag ) {
635 if ( isset( $tag['type'] ) && 'Mention' === $tag['type'] && isset( $tag['href'] ) && $tag['href'] === $my_activitypub_id ) {
636 // It was a mention.
637 $user_feed = $this->get_external_mentions_feed();
638 break;
639 }
640 }
641 }
642 }
643
644 if ( ! $user_feed || is_wp_error( $user_feed ) ) {
645 // We're not following this user.
646 return false;
647 }
648
649 $item = $this->process_incoming_activity( $type, $activity, $user_id, $user_feed );
650
651 if ( $item instanceof Feed_Item ) {
652 $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed );
653 return true;
654 }
655
656 return false;
657 }
658
659 /**
660 * Process an incoming activity.
661 *
662 * @param string $type The type of the activity.
663 * @param array $activity The activity object.
664 * @param int $user_id The id of the local user if any.
665 * @param User_Feed $user_feed The user feed.
666 * @return Feed_Item|null The feed item or null if it's not a valid activity.
667 */
668 private function process_incoming_activity( $type, $activity, $user_id, $user_feed ) {
669 switch ( $type ) {
670 case 'create':
671 return $this->handle_incoming_create( $activity['object'] );
672 case 'delete':
673 return $this->handle_incoming_delete( $activity['object'] );
674 case 'announce':
675 return $this->handle_incoming_announce( $activity['object'], $user_id, $activity['published'] );
676 case 'unannounce':
677 return $this->handle_incoming_unannounce( $activity['object'], $user_feed );
678 case 'like':
679 return $this->handle_incoming_like( $activity, $user_id );
680 case 'unlike':
681 return $this->handle_incoming_unlike( $activity, $user_id );
682 }
683 return null;
684 }
685
686 /**
687 * Map the Activity type to a post fomat.
688 *
689 * @param string $type The type.
690 *
691 * @return string The determined post format.
692 */
693 private function map_type_to_post_format( $type ) {
694 return 'status';
695 }
696
697 /**
698 * We received a post for a feed, handle it.
699 *
700 * @param array $activity The object from ActivityPub.
701 */
702 private function handle_incoming_create( $activity ) {
703 $permalink = $activity['id'];
704 if ( isset( $activity['url'] ) ) {
705 $permalink = $activity['url'];
706 }
707
708 $data = array(
709 'permalink' => $permalink,
710 'content' => $activity['content'],
711 'post_format' => $this->map_type_to_post_format( $activity['type'] ),
712 'date' => $activity['published'],
713 '_external_id' => $activity['id'],
714 self::SLUG => array(),
715 );
716
717 if ( isset( $activity['reblog'] ) && $activity['reblog'] ) {
718 $data[ self::SLUG ]['reblog'] = $activity['reblog'];
719 }
720
721 if ( isset( $activity['attributedTo'] ) ) {
722 $meta = $this->get_metadata( $activity['attributedTo'] );
723 $this->log( 'Attributed to ' . $activity['attributedTo'], compact( 'meta' ) );
724
725 if ( $meta && ! is_wp_error( $meta ) ) {
726 if ( isset( $meta['name'] ) ) {
727 $data['author'] = $meta['name'];
728 } elseif ( isset( $meta['preferredUsername'] ) ) {
729 $data['author'] = $meta['preferredUsername'];
730 }
731
732 $data[ self::SLUG ]['attributedTo'] = array(
733 'id' => $meta['id'],
734 );
735 if ( ! empty( $meta['icon']['url'] ) ) {
736 $data[ self::SLUG ]['attributedTo']['icon'] = $meta['icon']['url'];
737 }
738
739 if ( ! empty( $meta['summary'] ) ) {
740 $data[ self::SLUG ]['attributedTo']['summary'] = $meta['summary'];
741 }
742
743 if ( ! empty( $meta['preferredUsername'] ) ) {
744 $data[ self::SLUG ]['attributedTo']['preferredUsername'] = $meta['preferredUsername'];
745 }
746
747 if ( ! empty( $meta['name'] ) ) {
748 $data[ self::SLUG ]['attributedTo']['name'] = $meta['name'];
749 }
750 }
751 }
752
753 if ( isset( $activity['application'] ) && $activity['application'] ) {
754 $data[ self::SLUG ]['application'] = $activity['application'];
755 }
756
757 if ( ! empty( $activity['attachment'] ) ) {
758 foreach ( $activity['attachment'] as $attachment ) {
759 if ( ! isset( $attachment['type'] ) || ! isset( $attachment['mediaType'] ) ) {
760 continue;
761 }
762 if ( 'Document' !== $attachment['type'] ) {
763 continue;
764 }
765
766 if ( strpos( $attachment['mediaType'], 'image/' ) === 0 ) {
767 $data['content'] .= PHP_EOL;
768 $data['content'] .= '<!-- wp:image -->';
769 $data['content'] .= '<p><img src="' . esc_url( $attachment['url'] ) . '" width="' . esc_attr( $attachment['width'] ) . '" height="' . esc_attr( $attachment['height'] ) . '" class="size-full" /></p>';
770 $data['content'] .= '<!-- /wp:image -->';
771 } elseif ( strpos( $attachment['mediaType'], 'video/' ) === 0 ) {
772 $data['content'] .= PHP_EOL;
773 $data['content'] .= '<!-- wp:video -->';
774 $data['content'] .= '<figure class="wp-block-video"><video controls src="' . esc_url( $attachment['url'] ) . '" />';
775 if ( ! empty( $attachment['name'] ) ) {
776 $data['content'] .= '<figcaption class="wp-element-caption">' . esc_html( $attachment['name'] ) . '</figcaption>';
777 }
778 $data['content'] .= '</figure>';
779 $data['content'] .= '<!-- /wp:video -->';
780 }
781 }
782 }
783
784 $this->log(
785 'Received feed item',
786 array(
787 'url' => $permalink,
788 'data' => $data,
789 )
790 );
791 return new Feed_Item( $data );
792 }
793
794
795 /**
796 * We received a delete of a post, handle it.
797 *
798 * @param array $activity The object from ActivityPub.
799 */
800 private function handle_incoming_delete( $activity ) {
801 $permalink = $activity['id'];
802 if ( isset( $activity['url'] ) ) {
803 $permalink = $activity['url'];
804 }
805
806 $this->log( 'Received delete for ' . $permalink );
807
808 $comment_id = null;
809 if ( preg_match( '/#comment-(\d+)$/i', $permalink, $matches ) ) {
810 $comment_id = $matches[1];
811 $permalink = substr( $permalink, 0, - strlen( $matches[0] ) );
812 }
813
814 $post_id = Feed::url_to_postid( $permalink );
815 if ( $post_id ) {
816 if ( $comment_id ) {
817 $comment = get_comment( $comment_id );
818 if ( $comment ) {
819 wp_delete_comment( $comment_id );
820 return true;
821 }
822 } else {
823 $meta = get_post_meta( $post_id, self::SLUG, true );
824 if ( ! empty( $meta ) ) {
825 wp_trash_post( $post_id );
826 return true;
827 }
828 }
829 }
830 return false;
831 }
832
833 /**
834 * We received an announced URL (boost) for a feed, handle it.
835 *
836 * @param array $url The announced URL.
837 * @param int $user_id The user id (for retrieving the keys).
838 * @param string $published_date The published date.
839 */
840 private function handle_incoming_announce( $url, $user_id, $published_date = null ) {
841 if ( ! Friends::check_url( $url ) ) {
842 $this->log( 'Received invalid announce', compact( 'url' ) );
843 return false;
844 }
845 $this->log( 'Received announce for ' . $url );
846 if ( null === $user_id ) {
847 $user_id = Friends::get_main_friend_user_id();
848 }
849 $response = \Activitypub\safe_remote_get( $url, $user_id );
850 if ( \is_wp_error( $response ) ) {
851 return $response;
852 }
853 $json = \wp_remote_retrieve_body( $response );
854 $object = \json_decode( $json, true );
855 if ( ! $object ) {
856 $this->log( 'Received invalid json', compact( 'json' ) );
857 return false;
858 }
859 $this->log( 'Received response', compact( 'url', 'object' ) );
860 if ( ! $published_date ) {
861 $published_date = 'now';
862 }
863 $object['published'] = gmdate( 'Y-m-d H:i:s', strtotime( $published_date ) );
864 $object['reblog'] = true;
865
866 return $this->handle_incoming_create( $object );
867 }
868
869 /**
870 * We received an announced URL (boost) for a feed, handle it.
871 *
872 * @param array $url The announced URL.
873 * @param User_Feed $user_feed The user feed.
874 */
875 private function handle_incoming_unannounce( $url, $user_feed ) {
876 if ( ! Friends::check_url( $url ) ) {
877 $this->log( 'Received invalid unannounce', compact( 'url' ) );
878 return false;
879 }
880 $this->log( 'Received unannounce for ' . $url );
881
882 $post_id = Feed::url_to_postid( $url, $user_feed->get_friend_user()->ID );
883 if ( $post_id ) {
884 $meta = get_post_meta( $post_id, self::SLUG, true );
885 if ( isset( $meta['reblog'] ) ) {
886 wp_trash_post( $post_id );
887 return true;
888 }
889 }
890 return false;
891 }
892
893 public function get_reaction_display_name( $display_name, $term ) {
894 $url = get_term_meta( $term->term_id, 'url', true );
895 $meta = \Activitypub\get_remote_metadata_by_actor( $url );
896 if ( $meta && ! is_wp_error( $meta ) && ! empty( $meta['preferredUsername'] ) ) {
897 $host = wp_parse_url( $meta['id'], PHP_URL_HOST );
898 return '@' . $meta['preferredUsername'] . '@' . $host;
899 }
900 return $display_name;
901 }
902
903 public function handle_incoming_like( $activity, $user_id ) {
904 $post_id = Feed::url_to_postid( $activity['object'] );
905 if ( ! $post_id ) {
906 return false;
907 }
908 $taxonomy_username = 'ap-' . crc32( $activity['actor'] );
909
910 // Allow setting the term with its meta.
911 $taxonomy = Reactions::register_user_taxonomy( $taxonomy_username );
912
913 register_term_meta(
914 $taxonomy,
915 'url',
916 array(
917 'show_in_rest' => false,
918 'single' => true,
919 'type' => 'string',
920 'sanitize_callback' => 'sanitize_text_field',
921 )
922 );
923
924 // 2b50 = star.
925 // 2764 = heart.
926 $term_id = apply_filters( 'friends_react', null, $post_id, '2b50', $taxonomy_username );
927 if ( ! $term_id || is_wp_error( $term_id ) ) {
928 return false;
929 }
930
931 update_term_meta( $term_id, 'url', $activity['actor'] );
932
933 return $term_id;
934 }
935
936 public function handle_incoming_unlike( $activity, $user_id ) {
937 $post_id = Feed::url_to_postid( $activity['object'] );
938 if ( ! $post_id ) {
939 return false;
940 }
941
942 $taxonomy_username = 'ap-' . crc32( $activity['actor'] );
943
944 // Allow removing the term.
945 Reactions::register_user_taxonomy( $taxonomy_username );
946
947 // 2b50 = star.
948 // 2764 = heart.
949 $ret = apply_filters( 'friends_unreact', null, $post_id, '2b50', $taxonomy_username );
950 if ( ! $ret || is_wp_error( $ret ) ) {
951 return false;
952 }
953
954 return true;
955 }
956
957 /**
958 * Queue a hook to run async.
959 *
960 * @param string $hook The hook name.
961 * @param array $args The arguments to pass to the hook.
962 * @param string $unqueue_hook Optional a hook to unschedule before queuing.
963 * @return void|bool Whether the hook was queued.
964 */
965 public function queue( $hook, $args, $unqueue_hook = null ) {
966 if ( $unqueue_hook ) {
967 $hook_timestamp = wp_next_scheduled( $unqueue_hook, $args );
968 if ( $hook_timestamp ) {
969 wp_unschedule_event( $hook_timestamp, $unqueue_hook, $args );
970 }
971 }
972
973 if ( wp_next_scheduled( $hook, $args ) ) {
974 return;
975 }
976
977 return \wp_schedule_single_event( \time(), $hook, $args );
978 }
979
980 /**
981 * Prepare to follow the user via a scheduled event.
982 *
983 * @param User_Feed $user_feed The user feed.
984 *
985 * @return bool|WP_Error Whether the event was queued.
986 */
987 public function queue_follow_user( User_Feed $user_feed ) {
988 if ( self::SLUG !== $user_feed->get_parser() ) {
989 return;
990 }
991
992 $queued = $this->queue(
993 'friends_feed_parser_activitypub_follow',
994 array( $user_feed->get_url() ),
995 'friends_feed_parser_activitypub_unfollow'
996 );
997
998 if ( $queued ) {
999 $user_feed->update_last_log( __( 'Queued follow request.', 'friends' ) );
1000 }
1001
1002 return $queued;
1003 }
1004
1005 /**
1006 * Follow a user via ActivityPub at a URL.
1007 *
1008 * @param string $url The url.
1009 * @param int $user_id The current user id.
1010 */
1011 public function activitypub_follow_user( $url, $user_id = null ) {
1012 if ( null === $user_id ) {
1013 $user_id = Friends::get_main_friend_user_id();
1014 }
1015
1016 $meta = $this->get_metadata( $url );
1017 $user_feed = User_Feed::get_by_url( $url );
1018 if ( is_wp_error( $meta ) ) {
1019 if ( $user_feed instanceof User_Feed ) {
1020 $user_feed->update_last_log(
1021 sprintf(
1022 // translators: %s an error message.
1023 __( 'Error: %s', 'friends' ),
1024 $meta->get_error_code() . ' ' . $meta->get_error_message()
1025 )
1026 );
1027 }
1028 return $meta;
1029 }
1030 $to = $meta['id'];
1031 $type = 'Follow';
1032 $inbox = self::get_inbox_by_actor( $to, $type );
1033 if ( is_wp_error( $inbox ) ) {
1034 return $inbox;
1035 }
1036 $actor = \get_author_posts_url( $user_id );
1037
1038 $activity = new \Activitypub\Activity\Activity();
1039 $activity->set_type( $type );
1040 $activity->set_to( null );
1041 $activity->set_cc( null );
1042 $activity->set_actor( $actor );
1043 $activity->set_object( $to );
1044 $activity->set_id( $actor . '#follow-' . \preg_replace( '~^https?://~', '', $to ) );
1045 $activity = $activity->to_json();
1046 $response = \Activitypub\safe_remote_post( $inbox, $activity, $user_id );
1047
1048 if ( $user_feed instanceof User_Feed ) {
1049 $user_feed->update_last_log(
1050 sprintf(
1051 // translators: %s is the response from the remote server.
1052 __( 'Sent follow request with response: %s', 'friends' ),
1053 wp_remote_retrieve_response_code( $response ) . ' ' . wp_remote_retrieve_response_message( $response )
1054 )
1055 );
1056 }
1057 }
1058
1059 /**
1060 * Prepare to unfollow the user via a scheduled event.
1061 *
1062 * @param User_Feed $user_feed The user feed.
1063 *
1064 * @return bool|WP_Error Whether the event was queued.
1065 */
1066 public function queue_unfollow_user( User_Feed $user_feed ) {
1067 if ( self::SLUG !== $user_feed->get_parser() ) {
1068 return false;
1069 }
1070
1071 $queued = $this->queue(
1072 'friends_feed_parser_activitypub_unfollow',
1073 array( $user_feed->get_url() ),
1074 'friends_feed_parser_activitypub_follow'
1075 );
1076
1077 if ( $queued ) {
1078 $user_feed->update_last_log( __( 'Queued unfollow request.', 'friends' ) );
1079 }
1080
1081 return $queued;
1082 }
1083
1084 /**
1085 * Unfllow a user via ActivityPub at a URL.
1086 *
1087 * @param string $url The url.
1088 * @param int $user_id The current user id.
1089 */
1090 public function activitypub_unfollow_user( $url, $user_id = null ) {
1091 if ( null === $user_id ) {
1092 $user_id = Friends::get_main_friend_user_id();
1093 }
1094 $meta = $this->get_metadata( $url );
1095 $user_feed = User_Feed::get_by_url( $url );
1096 if ( is_wp_error( $meta ) ) {
1097 if ( $user_feed instanceof User_Feed ) {
1098 $user_feed->update_last_log(
1099 sprintf(
1100 // translators: %s an error message.
1101 __( 'Error: %s', 'friends' ),
1102 $meta->get_error_code() . ' ' . $meta->get_error_message()
1103 )
1104 );
1105 }
1106 return $meta;
1107 }
1108 $to = $meta['id'];
1109 $type = 'Follow';
1110 $inbox = self::get_inbox_by_actor( $to, $type );
1111 if ( is_wp_error( $inbox ) ) {
1112 return $inbox;
1113 }
1114 $actor = \get_author_posts_url( $user_id );
1115
1116 $activity = new \Activitypub\Activity\Activity();
1117 $activity->set_type( 'Undo' );
1118 $activity->set_to( null );
1119 $activity->set_cc( null );
1120 $activity->set_actor( $actor );
1121 $activity->set_object(
1122 array(
1123 'type' => $type,
1124 'actor' => $actor,
1125 'object' => $to,
1126 'id' => $to,
1127 )
1128 );
1129 $activity->set_id( $actor . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) );
1130 $activity = $activity->to_json();
1131 $response = \Activitypub\safe_remote_post( $inbox, $activity, $user_id );
1132
1133 $user_feed = User_Feed::get_by_url( $url );
1134 if ( $user_feed instanceof User_Feed ) {
1135 $user_feed->update_last_log(
1136 sprintf(
1137 // translators: %s is the response from the remote server.
1138 __( 'Sent unfollow request with response: %s', 'friends' ),
1139 wp_remote_retrieve_response_code( $response ) . ' ' . wp_remote_retrieve_response_message( $response )
1140 )
1141 );
1142 }
1143 }
1144
1145 public static function get_possible_mentions() {
1146 static $users = null;
1147 if ( ! method_exists( '\Friends\User_Feed', 'get_by_parser' ) ) {
1148 return array();
1149 }
1150
1151 if ( is_null( $users ) || ! apply_filters( 'activitypub_cache_possible_friend_mentions', true ) ) {
1152 $feeds = User_Feed::get_by_parser( 'activitypub' );
1153 $users = array();
1154 foreach ( $feeds as $feed ) {
1155 $user = $feed->get_friend_user();
1156 $slug = $user->user_nicename;
1157 if ( ! $slug ) {
1158 $slug = $user->user_login;
1159 }
1160 $slug = sanitize_title( $slug );
1161 $users[ '@' . $slug ] = $feed->get_url();
1162 }
1163
1164 $local_users = get_users(
1165 array(
1166 'fields' => array( 'ID', 'user_nicename', 'user_login' ),
1167 )
1168 );
1169 foreach ( $local_users as $local_user ) {
1170 $slug = $local_user->user_nicename;
1171 if ( ! $slug ) {
1172 $slug = $local_user->user_login;
1173 }
1174 $slug = sanitize_title( $slug );
1175 $users[ '@' . $slug ] = get_author_posts_url( $local_user->ID );
1176 }
1177 $users[ '@' . sanitize_title( get_bloginfo( 'name' ) ) ] = get_bloginfo( 'url' );
1178 }
1179 return $users;
1180 }
1181
1182 /**
1183 * Extract the mentions from the post_content.
1184 *
1185 * @param array $mentions The already found mentions.
1186 * @param string $post_content The post content.
1187 * @return mixed The discovered mentions.
1188 */
1189 public function activitypub_extract_mentions( $mentions, $post_content ) {
1190 $users = self::get_possible_mentions();
1191 preg_match_all( '/@(?:[a-zA-Z0-9_-]+)/', $post_content, $matches );
1192 foreach ( $matches[0] as $match ) {
1193 if ( isset( $users[ $match ] ) ) {
1194 $mentions[ $match ] = $users[ $match ];
1195 }
1196 }
1197 return $mentions;
1198 }
1199
1200 public function the_content( $the_content ) {
1201 $protected_tags = array();
1202 $the_content = preg_replace_callback(
1203 '#<a.*?href=[^>]+>.*?</a>#i',
1204 function ( $m ) use ( &$protected_tags ) {
1205 $c = count( $protected_tags );
1206 $protect = '!#!#PROTECT' . $c . '#!#!';
1207 $protected_tags[ $protect ] = $m[0];
1208 return $protect;
1209 },
1210 $the_content
1211 );
1212
1213 $the_content = \preg_replace_callback( '/@(?:[a-zA-Z0-9_-]+)/', array( $this, 'replace_with_links' ), $the_content );
1214
1215 $the_content = str_replace( array_keys( $protected_tags ), array_values( $protected_tags ), $the_content );
1216
1217 return $the_content;
1218 }
1219
1220 /**
1221 * Replace the mention with a link to the user.
1222 *
1223 * @param array $result The matched username.
1224 *
1225 * @return string The replaced username.
1226 */
1227 public function replace_with_links( array $result ) {
1228 $users = self::get_possible_mentions();
1229 if ( ! isset( $users[ $result[0] ] ) ) {
1230 return $result[0];
1231 }
1232
1233 $metadata = $this->get_metadata( $users[ $result[0] ] );
1234 if ( is_wp_error( $metadata ) || empty( $metadata['url'] ) ) {
1235 return $result[0];
1236 }
1237
1238 $username = ltrim( $users[ $result[0] ], '@' );
1239 if ( ! empty( $metadata['name'] ) ) {
1240 $username = $metadata['name'];
1241 }
1242 if ( ! empty( $metadata['preferredUsername'] ) ) {
1243 $username = $metadata['preferredUsername'];
1244 }
1245 $username = '@<span>' . $username . '</span>';
1246 return \sprintf( '<a rel="mention" class="u-url mention" href="%s">%s</a>', $metadata['url'], $username );
1247 }
1248
1249 public function activitypub_save_settings( User $friend ) {
1250 if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'edit-friend-feeds-' . $friend->user_login ) ) {
1251
1252 if ( isset( $_POST['friends_show_replies'] ) && $_POST['friends_show_replies'] ) {
1253 $friend->update_user_option( 'activitypub_friends_show_replies', '1' );
1254 } else {
1255 $friend->delete_user_option( 'activitypub_friends_show_replies' );
1256 }
1257 }
1258 }
1259
1260 public function activitypub_settings( User $friend ) {
1261 $has_activitypub_feed = false;
1262 foreach ( $friend->get_active_feeds() as $feed ) {
1263 if ( 'activitypub' === $feed->get_parser() ) {
1264 $has_activitypub_feed = true;
1265 }
1266 }
1267
1268 if ( ! $has_activitypub_feed ) {
1269 return;
1270 }
1271
1272 ?>
1273 <tr>
1274 <th>ActivityPub</th>
1275 <td>
1276 <fieldset>
1277 <div>
1278 <input type="checkbox" name="friends_show_replies" id="friends_show_replies" value="1" <?php checked( '1', $friend->get_user_option( 'activitypub_friends_show_replies' ) ); ?> />
1279 <label for="friends_show_replies"><?php esc_html_e( "Don't hide @mentions of feeds you don't follow", 'friends' ); ?></label>
1280 </div>
1281 </fieldset>
1282 <p class="description">
1283 <?php
1284 esc_html_e( "Show ActivityPub '@mention' posts even when you do not follow the feed being mentioned.", 'friends' );
1285 ?>
1286 </p>
1287 </td>
1288 </tr>
1289 <?php
1290 }
1291
1292 /**
1293 * Apply the feed rules
1294 *
1295 * @param Feed_Item $item The feed item.
1296 * @param User_Feed $feed The feed object.
1297 * @param User $friend_user The friend user.
1298 * @return Feed_Item The modified feed item.
1299 */
1300 public function modify_incoming_item( Feed_Item $item, User_Feed $feed = null, User $friend_user = null ) {
1301 if ( ! $feed || 'activitypub' !== $feed->get_parser() || ! $friend_user ) {
1302 return $item;
1303 }
1304
1305 $external_mentions_user = $this->get_external_mentions_user();
1306 if (
1307 $external_mentions_user->get_object_id() !== $friend_user->get_object_id() && // Don't hide mentions for the external mentions user.
1308 ! $friend_user->get_user_option( 'activitypub_friends_show_replies' )
1309 ) {
1310 $plain_text_content = \wp_strip_all_tags( $item->post_content );
1311 $possible_mentions = self::get_possible_mentions();
1312
1313 $no_known_user_found = true;
1314 if ( preg_match( '/^@(?:[a-zA-Z0-9_.-]+)/i', $plain_text_content, $m ) ) {
1315 if ( ! isset( $possible_mentions[ $m[0] ] ) ) {
1316 $no_known_user_found = false;
1317 }
1318 }
1319
1320 if ( $no_known_user_found ) {
1321 if ( $friend_user && false !== strpos( $item->post_content, \get_author_posts_url( $friend_user->ID ) ) ) {
1322 $no_known_user_found = false;
1323 }
1324 }
1325
1326 if ( $no_known_user_found ) {
1327 foreach ( $possible_mentions as $username => $mention_url ) {
1328 if ( false !== strpos( $item->post_content, $mention_url ) ) {
1329 $no_known_user_found = false;
1330 break;
1331 }
1332 if ( false !== strpos( $username, $mention_url ) ) {
1333 $no_known_user_found = false;
1334 break;
1335 }
1336 }
1337 }
1338
1339 if ( ! $no_known_user_found ) {
1340 $item->_feed_rule_transform = array(
1341 'post_status' => 'trash',
1342 );
1343 }
1344 }
1345
1346 return $item;
1347 }
1348
1349 public function friends_potential_avatars( $avatars, User $friend_user ) {
1350 foreach ( $friend_user->get_feeds() as $user_feed ) {
1351 if ( 'activitypub' === $user_feed->get_parser() ) {
1352 $details = $this->update_feed_details(
1353 array(
1354 'url' => $user_feed->get_url(),
1355 )
1356 );
1357 if ( isset( $details['avatar'] ) ) {
1358 $avatars[ $details['avatar'] ] = sprintf(
1359 // translators: %s is a username.
1360 __( 'Avatar of %s', 'friends' ),
1361 $details['title']
1362 );
1363 }
1364 }
1365 }
1366 return $avatars;
1367 }
1368
1369 private function get_author_of_post( \WP_Post $post ) {
1370 $friend = User::get_post_author( $post );
1371 if ( ! $friend || is_wp_error( $friend ) ) {
1372 return $friend;
1373 }
1374
1375 $meta = get_post_meta( $post->ID, self::SLUG, true );
1376 if ( isset( $meta['attributedTo']['id'] ) ) {
1377 return $meta['attributedTo']['id'];
1378 }
1379
1380 $host = wp_parse_url( $post->guid, PHP_URL_HOST );
1381
1382 foreach ( $friend->get_active_feeds() as $feed ) {
1383 if ( 'activitypub' !== $feed->get_parser() ) {
1384 continue;
1385 }
1386
1387 $feed_host = wp_parse_url( $feed->get_url(), PHP_URL_HOST );
1388 if ( $feed_host !== $host ) {
1389 continue;
1390 }
1391
1392 return $feed->get_url();
1393 }
1394
1395 return null;
1396 }
1397
1398 /**
1399 * Create a status based on a post reaction.
1400 *
1401 * @param int $post_id The post ID.
1402 */
1403 public function post_reaction( $post_id ) {
1404 $post = get_post( $post_id );
1405 if ( ! $post ) {
1406 return;
1407 }
1408 $author_url = $this->get_author_of_post( $post );
1409 if ( ! $author_url || is_wp_error( $author_url ) ) {
1410 return;
1411 }
1412 return $this->queue_like_post( $post, $author_url );
1413 }
1414
1415 /**
1416 * Prepare to follow the user via a scheduled event.
1417 *
1418 * @param \WP_Post $post The post.
1419 * @param string $author_url The author url.
1420 *
1421 * @return bool|WP_Error Whether the event was queued.
1422 */
1423 public function queue_like_post( \WP_Post $post, $author_url ) {
1424 $external_post_id = get_post_meta( $post->ID, 'external-id', true );
1425 if ( ! $external_post_id ) {
1426 $external_post_id = $post->guid;
1427 }
1428
1429 $queued = $this->queue(
1430 'friends_feed_parser_activitypub_like',
1431 array( $author_url, $external_post_id, get_current_user_id() ),
1432 'friends_feed_parser_activitypub_unlike'
1433 );
1434
1435 return $queued;
1436 }
1437
1438 /**
1439 * Like a post.
1440 *
1441 * @param mixed $url The URL of the user.
1442 * @param mixed $external_post_id The post to like.
1443 * @param mixed $user_id The current user id.
1444 * @return void|WP_Error
1445 */
1446 public function activitypub_like_post( $url, $external_post_id, $user_id ) {
1447 $type = 'Like';
1448 $inbox = self::get_inbox_by_actor( $url, $type );
1449 if ( is_wp_error( $inbox ) ) {
1450 return $inbox;
1451 }
1452 $actor = \get_author_posts_url( $user_id );
1453
1454 $activity = new \Activitypub\Activity\Activity();
1455 $activity->set_type( $type );
1456 $activity->set_to( null );
1457 $activity->set_cc( null );
1458 $activity->set_actor( $actor );
1459 $activity->set_object( $external_post_id );
1460 $activity->set_id( $actor . '#like-' . \preg_replace( '~^https?://~', '', $external_post_id ) );
1461 $activity = $activity->to_json();
1462 $response = \Activitypub\safe_remote_post( $inbox, $activity, $user_id );
1463
1464 $user_feed = User_Feed::get_by_url( $url );
1465 if ( $user_feed instanceof User_Feed ) {
1466 $user_feed->update_last_log(
1467 sprintf(
1468 // translators: %s is the response from the remote server.
1469 __( 'Sent like request with response: %s', 'friends' ),
1470 wp_remote_retrieve_response_code( $response ) . ' ' . wp_remote_retrieve_response_message( $response )
1471 )
1472 );
1473 }
1474 }
1475
1476 /**
1477 * Create a status based on a post reaction.
1478 *
1479 * @param int $post_id The post ID.
1480 */
1481 public function undo_post_reaction( $post_id ) {
1482 $post = get_post( $post_id );
1483 if ( ! $post ) {
1484 return;
1485 }
1486 $author_url = $this->get_author_of_post( $post );
1487 if ( ! $author_url || is_wp_error( $author_url ) ) {
1488 return;
1489 }
1490 return $this->queue_unlike_post( $post, $author_url );
1491 }
1492
1493 /**
1494 * Prepare to follow the user via a scheduled event.
1495 *
1496 * @param \WP_Post $post The post.
1497 * @param string $author_url The author url.
1498 *
1499 * @return bool|WP_Error Whether the event was queued.
1500 */
1501 public function queue_unlike_post( \WP_Post $post, $author_url ) {
1502 $external_post_id = get_post_meta( $post->ID, 'external-id', true );
1503 if ( ! $external_post_id ) {
1504 $external_post_id = $post->guid;
1505 }
1506
1507 $queued = $this->queue(
1508 'friends_feed_parser_activitypub_unlike',
1509 array( $author_url, $external_post_id, get_current_user_id() ),
1510 'friends_feed_parser_activitypub_like'
1511 );
1512
1513 if ( $queued ) {
1514 $user_feed->update_last_log( __( 'Queued unlike request.', 'friends' ) );
1515 }
1516
1517 return $queued;
1518 }
1519
1520 /**
1521 * Unlike a post.
1522 *
1523 * @param mixed $url The URL of the user.
1524 * @param mixed $external_post_id The post to like.
1525 * @param mixed $user_id The current user id.
1526 * @return void|WP_Error
1527 */
1528 public function activitypub_unlike_post( $url, $external_post_id, $user_id ) {
1529 $type = 'Like';
1530 $inbox = self::get_inbox_by_actor( $url, $type );
1531 if ( is_wp_error( $inbox ) ) {
1532 return $inbox;
1533 }
1534 $actor = \get_author_posts_url( $user_id );
1535
1536 $activity = new \Activitypub\Activity\Activity();
1537 $activity->set_type( 'Undo' );
1538 $activity->set_to( null );
1539 $activity->set_cc( null );
1540 $activity->set_actor( $actor );
1541 $activity->set_object(
1542 array(
1543 'type' => $type,
1544 'actor' => $actor,
1545 'object' => $external_post_id,
1546 'id' => $actor . '#like-' . \preg_replace( '~^https?://~', '', $external_post_id ),
1547 )
1548 );
1549 $activity->set_id( $actor . '#unlike-' . \preg_replace( '~^https?://~', '', $external_post_id ) );
1550 $activity = $activity->to_json();
1551 $response = \Activitypub\safe_remote_post( $inbox, $activity, $user_id );
1552
1553 $user_feed = User_Feed::get_by_url( $url );
1554 if ( $user_feed instanceof User_Feed ) {
1555 $user_feed->update_last_log(
1556 sprintf(
1557 // translators: %s is the response from the remote server.
1558 __( 'Sent unlike request with response: %s', 'friends' ),
1559 wp_remote_retrieve_response_code( $response ) . ' ' . wp_remote_retrieve_response_message( $response )
1560 )
1561 );
1562 }
1563 }
1564
1565 public function friends_reblog_button_label( $button_label ) {
1566 if ( User_Feed::get_parser_for_post_id( get_the_ID() ) === 'activitypub' ) {
1567 if ( get_user_option( 'friends_activitypub_dont_reblog' ) ) {
1568 $button_label = _x( 'Boost', 'button', 'friends' );
1569 } else {
1570 $button_label = _x( 'Reblog & Boost', 'button', 'friends' );
1571 }
1572 }
1573 return $button_label;
1574 }
1575
1576
1577 /**
1578 * Get metadata for in_reply_to_preview.
1579 *
1580 * @param string $url The url.
1581 *
1582 * @return array|WP_Error The in reply to metadata.
1583 */
1584 public function get_activitypub_ajax_metadata( $url ) {
1585 $meta = apply_filters( 'friends_get_activitypub_metadata', array(), $url );
1586 if ( is_wp_error( $meta ) ) {
1587 return $meta;
1588 }
1589
1590 if ( ! $meta || ! isset( $meta['attributedTo'] ) ) {
1591 return new \WP_Error( 'no-activitypub', 'No ActivityPub metadata found.' );
1592 }
1593
1594 $html = force_balance_tags( wp_kses_post( $meta['content'] ) );
1595
1596 $webfinger = apply_filters( 'friends_get_activitypub_metadata', array(), $meta['attributedTo'] );
1597 $mention = '';
1598 if ( $webfinger && ! is_wp_error( $webfinger ) ) {
1599 $mention = '@' . $webfinger['preferredUsername'] . '@' . wp_parse_url( $url, PHP_URL_HOST );
1600 }
1601
1602 return array(
1603 'url' => $url,
1604 'html' => $html,
1605 'author' => $meta['attributedTo'],
1606 'mention' => $mention,
1607 );
1608 }
1609
1610 /**
1611 * The Ajax function to fill the in-reply-to-preview.
1612 */
1613 public function ajax_in_reply_to_preview() {
1614 check_ajax_referer( 'friends-in-reply-to-preview' );
1615 $url = wp_unslash( $_POST['url'] );
1616
1617 if ( ! wp_parse_url( $url ) ) {
1618 wp_send_json_error();
1619 exit;
1620 }
1621
1622 $meta = $this->get_activitypub_ajax_metadata( $_POST['url'] );
1623
1624 if ( is_wp_error( $meta ) ) {
1625 wp_send_json_error( $meta->get_error_message() );
1626 exit;
1627 }
1628 wp_send_json_success( $meta );
1629 }
1630
1631 public function frontend_reply_form( $args ) {
1632 if ( isset( $_GET['in_reply_to'] ) && wp_parse_url( $_GET['in_reply_to'] ) ) {
1633 $args['in_reply_to'] = $this->get_activitypub_ajax_metadata( $_GET['in_reply_to'] );
1634 $args['in_reply_to']['html'] = '<figcaption>' . make_clickable( $_GET['in_reply_to'] ) . '</figcaption><blockquote>' . $args['in_reply_to']['html'] . '</blockquote>';
1635 $args['form_class'] = 'open';
1636 Friends::template_loader()->get_template_part( 'frontend/activitypub/reply', true, $args );
1637 }
1638 }
1639
1640 public function frontend_boost_form( $args ) {
1641 if ( isset( $_GET['boost'] ) && wp_parse_url( $_GET['boost'] ) ) {
1642 $args['boost'] = $this->get_activitypub_ajax_metadata( $_GET['boost'] );
1643 $args['boost']['html'] = '<figcaption>' . make_clickable( $_GET['boost'] ) . '</figcaption><blockquote>' . $args['boost']['html'] . '</blockquote>';
1644 $args['form_class'] = 'open';
1645 Friends::template_loader()->get_template_part( 'frontend/activitypub/boost', true, $args );
1646 }
1647 }
1648
1649 public function friends_search_autocomplete( $results, $q ) {
1650 $url = preg_match( '#^(https?:\/\/)?(?:w{3}\.)?[\w-]+(?:\.[\w-]+)+((?:\/[^\s\/]*)*)#i', $q, $m );
1651 $url_with_path = isset( $m[2] ) && $m[2];
1652
1653 $already_added = false;
1654 foreach ( $results as $result ) {
1655 if ( strpos( $result, $q ) !== false ) {
1656 $already_added = true;
1657 }
1658 }
1659
1660 if ( ! $already_added ) {
1661 if ( ( $url && ! $url_with_path ) || preg_match( '/^@?' . self::ACTIVITYPUB_USERNAME_REGEXP . '$/i', $q ) ) {
1662 $result = '<a href="' . esc_url( add_query_arg( 'url', $m[1] ? $q : 'https://' . $q, admin_url( 'admin.php?page=add-friend' ) ) ) . '" class="has-icon-left">';
1663 $result .= '<span class="ab-icon dashicons dashicons-businessperson"><span class="dashicons dashicons-plus"></span></span>';
1664 $result .= 'Follow ';
1665 $result .= ' <small>';
1666 $result .= esc_html( $q );
1667 $result .= '</small></a>';
1668 $results[] = $result;
1669 }
1670 }
1671
1672 if ( $url_with_path ) {
1673 $result = '<a href="' . esc_url( add_query_arg( 'boost', $q, home_url( '/friends/type/status/' ) ) ) . '" class="has-icon-left">';
1674 $result .= '<span class="ab-icon dashicons dashicons-controls-repeat"></span>';
1675 $result .= 'Boost ';
1676 $result .= ' <small>';
1677 $result .= esc_html( Friends::url_truncate( $q ) );
1678 $result .= '</small></a>';
1679 $results[] = $result;
1680 }
1681
1682 if ( $url_with_path ) {
1683 $result = '<a href="' . esc_url( add_query_arg( 'in_reply_to', $q, home_url( '/friends/type/status/' ) ) ) . '" class="has-icon-left">';
1684 $result .= '<span class="ab-icon dashicons dashicons-admin-comments"></span>';
1685 $result .= 'Reply to ';
1686 $result .= ' <small>';
1687 $result .= esc_html( Friends::url_truncate( $q ) );
1688 $result .= '</small></a>';
1689 $results[] = $result;
1690 }
1691
1692 return $results;
1693 }
1694
1695 public function mastodon_api_react( $post_id, $reaction ) {
1696 apply_filters( 'friends_react', null, $post_id, $reaction );
1697 }
1698
1699 public function mastodon_api_unreact( $post_id, $reaction ) {
1700 apply_filters( 'friends_unreact', null, $post_id, $reaction );
1701 }
1702
1703 public function mastodon_api_reblog( $post_id ) {
1704 apply_filters( 'friends_reblog', null, $post_id );
1705 }
1706
1707 public function mastodon_api_unreblog( $post_id ) {
1708 apply_filters( 'friends_unreblog', null, $post_id );
1709 }
1710
1711 /**
1712 * Don't create a note via ActivityPub since we'll use announce there.
1713 *
1714 * @param bool $ret The return value.
1715 *
1716 * @return bool The return value.
1717 */
1718 public function unqueue_activitypub_create( $ret ) {
1719 $hook = 'transition_post_status';
1720 $filter = array( '\Activitypub\Activitypub', 'schedule_post_activity' );
1721 $priority = has_filter( $hook, $filter );
1722 if ( $priority ) {
1723 remove_filter( $hook, $filter, $priority );
1724 }
1725 return $ret;
1726 }
1727
1728 public function maybe_unqueue_friends_reblog_post( $ret, $post ) {
1729 if ( ! get_user_option( 'friends_activitypub_dont_reblog' ) ) {
1730 return $ret;
1731 }
1732
1733 if ( User_Feed::get_parser_for_post_id( $post->ID ) !== 'activitypub' ) {
1734 return $ret;
1735 }
1736
1737 remove_filter( 'friends_reblog', array( 'Friends\Frontend', 'reblog' ), 10, 2 );
1738 return $ret;
1739 }
1740
1741 public function reblog( $ret, $post ) {
1742 $post = get_post( $post );
1743 if ( ! $post ) {
1744 return $ret;
1745 }
1746 if ( User_Feed::get_parser_for_post_id( $post->ID ) === 'activitypub' ) {
1747 $this->queue_announce( $post->guid );
1748 \update_post_meta( $post->ID, 'reblogged', 'activitypub' );
1749 \update_post_meta( $post->ID, 'reblogged_by', get_current_user_id() );
1750 return true;
1751 }
1752 return $ret;
1753 }
1754
1755 public function unreblog( $ret, $post ) {
1756 $post = get_post( $post );
1757 if ( ! $post ) {
1758 return $ret;
1759 }
1760 if ( get_post_meta( $post->ID, 'reblogged', true ) === 'activitypub' ) {
1761 $this->queue_unannounce( $post->guid );
1762 \delete_post_meta( $post->ID, 'reblogged', 'activitypub' );
1763 \delete_post_meta( $post->ID, 'reblogged_by', get_current_user_id() );
1764 return true;
1765 }
1766 return $ret;
1767 }
1768
1769 /**
1770 * Prepare to announce the post via a scheduled event.
1771 *
1772 * @param string $url The url to announce.
1773 *
1774 * @return bool|WP_Error Whether the event was queued.
1775 */
1776 public function queue_announce( $url ) {
1777 $queued = $this->queue(
1778 'friends_feed_parser_activitypub_announce',
1779 array( $url, get_current_user_id() ),
1780 'friends_feed_parser_activitypub_unannounce'
1781 );
1782
1783 return $queued;
1784 }
1785
1786 /**
1787 * Announce the post via ActivityPub.
1788 *
1789 * @param string $url The url.
1790 * @param id $user_id The user id.
1791 */
1792 public function activitypub_announce( $url, $user_id ) {
1793 $actor = \get_author_posts_url( $user_id );
1794
1795 $activity = new \Activitypub\Activity\Activity();
1796 $activity->set_type( 'Announce' );
1797 $activity->set_actor( $actor );
1798 $activity->set_object( $url );
1799 $activity->set_id( $actor . '#activitypub_announce-' . \preg_replace( '~^https?://~', '', $url ) );
1800
1801 $follower_inboxes = \Activitypub\Collection\Followers::get_inboxes( $user_id );
1802 $mentioned_inboxes = \Activitypub\Mention::get_inboxes( $activity->get_cc() );
1803
1804 $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes );
1805 $inboxes = array_unique( $inboxes );
1806
1807 $json = $activity->to_json();
1808
1809 foreach ( $inboxes as $inbox ) {
1810 \Activitypub\safe_remote_post( $inbox, $json, $user_id );
1811 }
1812 }
1813
1814 /**
1815 * Prepare to announce the post via a scheduled event.
1816 *
1817 * @param string $url The url to announce.
1818 *
1819 * @return bool|WP_Error Whether the event was queued.
1820 */
1821 public function queue_unannounce( $url ) {
1822 $queued = $this->queue(
1823 'friends_feed_parser_activitypub_unannounce',
1824 array( $url, get_current_user_id() ),
1825 'friends_feed_parser_activitypub_announce'
1826 );
1827
1828 return $queued;
1829 }
1830
1831 /**
1832 * Unannounce the post via ActivityPub.
1833 *
1834 * @param string $url The url.
1835 * @param id $user_id The user id.
1836 */
1837 public function activitypub_unannounce( $url, $user_id ) {
1838 $actor = \get_author_posts_url( $user_id );
1839
1840 $activity = new \Activitypub\Activity\Activity();
1841 $activity->set_type( 'Undo' );
1842 $activity->set_to( null );
1843 $activity->set_cc( null );
1844 $activity->set_actor( $actor );
1845 $activity->set_id( $actor . '#activitypub_unannounce-' . \preg_replace( '~^https?://~', '', $url ) );
1846 $activity->set_object(
1847 array(
1848 'type' => 'Announce',
1849 'actor' => $actor,
1850 'object' => $url,
1851 'id' => $actor . '#activitypub_announce-' . \preg_replace( '~^https?://~', '', $url ),
1852 )
1853 );
1854
1855 $follower_inboxes = \Activitypub\Collection\Followers::get_inboxes( $user_id );
1856 $mentioned_inboxes = \Activitypub\Mention::get_inboxes( $activity->get_cc() );
1857
1858 $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes );
1859 $inboxes = array_unique( $inboxes );
1860
1861 $json = $activity->to_json();
1862
1863 foreach ( $inboxes as $inbox ) {
1864 \Activitypub\safe_remote_post( $inbox, $json, $user_id );
1865 }
1866 }
1867
1868 /**
1869 * Approve comments that
1870 *
1871 * @param bool $approved Whether the comment is approved.
1872 * @param array $commentdata The commentdata.
1873 *
1874 * @return bool Whether the comment is approved.
1875 */
1876 public function pre_comment_approved( $approved, $commentdata ) {
1877 if ( ! $approved || is_string( $approved ) && 'activitypub' === $commentdata['comment_meta']['protocol'] ) {
1878 $user_feed = User_Feed::get_by_url( $commentdata['comment_author_url'] );
1879 if ( $user_feed instanceof User_Feed ) {
1880 $approved = true;
1881 }
1882 }
1883 return $approved;
1884 }
1885
1886 public function comment_post( $comment_id, $comment_approved, $commentdata ) {
1887 if ( isset( $commentdata['commentmeta']['protocol'] ) && 'activitypub' === $commentdata['commentmeta']['protocol'] ) {
1888 // Don't act upon incoming comments via ActivityPub.
1889 return;
1890 }
1891
1892 if ( empty( $commentdata['user_id'] ) ) {
1893 // Don't act on other people's comments.
1894 return;
1895 }
1896
1897 $user = new \WP_User( $commentdata['user_id'] );
1898 if ( User::is_friends_plugin_user( $user ) ) {
1899 // Don't act on non-local comments.
1900 return;
1901 }
1902
1903 // TODO: in the ActivityPub plugin, we should be able to use the comment_post hook to send out the comment.
1904 }
1905
1906 public function trashed_comment( $comment_id, $comment ) {
1907 if ( get_comment_meta( $comment_id, 'protocol', true ) === 'activitypub' ) {
1908 // Don't act on comments that came via ActivityPub.
1909 return;
1910 }
1911
1912 if ( empty( $comment->user_id ) ) {
1913 // Don't act on other people's comments.
1914 return;
1915 }
1916
1917 $user = new \WP_User( $comment->user_id );
1918 if ( User::is_friends_plugin_user( $user ) ) {
1919 // Don't act on non-local comments.
1920 return;
1921 }
1922
1923 // TODO: in the ActivityPub plugin, we should be able to use the trashed_comment hook to send out the comment deletion.
1924 }
1925
1926 /**
1927 * Disable Webfinger for example domains
1928 *
1929 * @param mixed $metadata Already retrieved metadata.
1930 * @param mixed $actor The actor as URL or username.
1931 * @return mixed The potentially added metadata for example domains.
1932 */
1933 public function disable_webfinger_for_example_domains( $metadata, $actor ) {
1934 if ( ! $metadata ) {
1935 $username = null;
1936 $domain = null;
1937 if ( preg_match( '/^@?' . self::ACTIVITYPUB_USERNAME_REGEXP . '$/i', $actor, $m ) ) {
1938 $username = $m[1];
1939 $domain = $m[2];
1940 } else {
1941 $p = wp_parse_url( $actor );
1942 if ( $p ) {
1943 if ( isset( $p['host'] ) ) {
1944 $domain = $p['host'];
1945 }
1946 if ( isset( $p['path'] ) ) {
1947 $path_parts = explode( '/', trim( $p['path'], '/' ) );
1948 $username = ltrim( array_pop( $path_parts ), '@' );
1949 }
1950 }
1951 }
1952 if ( preg_match( '/[^a-zA-Z0-9.-]/', $domain ) ) { // Has invalid characters.
1953 return $metadata;
1954 }
1955
1956 if (
1957 rtrim( strtok( $domain, '.' ), '0123456789' ) === 'example' // A classic example.org domain.
1958 || preg_match( '/(my|your|our)-(domain)/', $domain )
1959 || preg_match( '/(test)/', $domain )
1960 || in_array( $username, array( 'example' ), true )
1961 ) {
1962 $metadata = array(
1963 'url' => sprintf( 'https://%s/users/%s/', $domain, $username ),
1964 'name' => $username,
1965 );
1966 }
1967 }
1968 return $metadata;
1969 }
1970 }
1971