PluginProbe
Friends / 2.8.9
Friends v2.8.9
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.8.9, at feed-parsers/class-feed-parser-activitypub.php

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