From f2b231ccac85d312897f6ea696bd315627ccb8a6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 22 Dec 2023 11:01:23 +0100 Subject: [PATCH 1/4] fix some small issues --- includes/class-activity-dispatcher.php | 2 +- includes/transformer/class-post.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index c9a1851..1d6f353 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -74,7 +74,7 @@ class Activity_Dispatcher { return; } - $activity = $transformer->to_activity( 'Create' ); + $activity = $transformer->to_activity( $type ); $follower_inboxes = Followers::get_inboxes( $user_id ); $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 7e631dd..e5f0054 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -377,7 +377,7 @@ class Post extends Base { * * @return string The Object-Type. */ - protected function get_object_type() { + protected function get_type() { if ( 'wordpress-post-format' !== \get_option( 'activitypub_object_type', 'note' ) ) { return \ucfirst( \get_option( 'activitypub_object_type', 'note' ) ); } From 3cda64a255f87a4a07de3e9c47ff2b3d7c091222 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Fri, 22 Dec 2023 04:33:25 -0600 Subject: [PATCH 2/4] Profiles: `Update` followers when profile fields change (#542) * Profiles: update followers when profile fields change * use static * only try to merge mention inboxes when valid * cleanups * add hook to wp_update_user * update readme --------- Co-authored-by: Matthias Pfefferle --- README.md | 1 + includes/class-activity-dispatcher.php | 65 ++++++++++++++++---- includes/class-scheduler.php | 83 ++++++++++++++++++++++++++ readme.txt | 1 + 4 files changed, 137 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 656b282..da17996 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu * Added: CSS class for ActivityPub comments to allow custom designs * Added: FEP-2677: Identifying the Application Actor * Added: Basic Comment Federation +* Added: Profile Update Activities * Improved: WebFinger endpoints ### 1.3.0 ### diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 1d6f353..99aa960 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -28,6 +28,7 @@ class Activity_Dispatcher { public static function init() { \add_action( 'activitypub_send_activity', array( self::class, 'send_activity' ), 10, 2 ); \add_action( 'activitypub_send_activity', array( self::class, 'send_activity_or_announce' ), 10, 2 ); + \add_action( 'activitypub_send_update_profile_activity', array( self::class, 'send_profile_update' ), 10, 1 ); } /** @@ -76,17 +77,7 @@ class Activity_Dispatcher { $activity = $transformer->to_activity( $type ); - $follower_inboxes = Followers::get_inboxes( $user_id ); - $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); - - $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); - $inboxes = array_unique( $inboxes ); - - $json = $activity->to_json(); - - foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $json, $user_id ); - } + self::send_activity_to_inboxes( $activity, $user_id ); } /** @@ -112,12 +103,60 @@ class Activity_Dispatcher { $user_id = $transformer->get_wp_user_id(); $activity = $transformer->to_activity( 'Announce' ); - $follower_inboxes = Followers::get_inboxes( $user_id ); - $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); + self::send_activity_to_inboxes( $activity, $user_id ); + } + + /** + * Send a "Update" Activity when a user updates their profile. + * + * @param int $user_id The user ID to send an update for. + * + * @return void + */ + public static function send_profile_update( $user_id ) { + $user = Users::get_by_various( $user_id ); + + // bail if that's not a good user + if ( is_wp_error( $user ) ) { + return; + } + + // build the update + $activity = new Activity(); + $activity->set_id( $user->get_url() . '#update' ); + $activity->set_type( 'Update' ); + $activity->set_actor( $user->get_url() ); + $activity->set_object( $user->get_url() ); + $activity->set_to( 'https://www.w3.org/ns/activitystreams#Public' ); + + // send the update + self::send_activity_to_inboxes( $activity, $user_id ); + } + + /** + * Send an Activity to all followers and mentioned users. + * + * @param Activity $activity The ActivityPub Activity. + * @param int $user_id The user ID. + * + * @return void + */ + private static function send_activity_to_inboxes( $activity, $user_id ) { + $follower_inboxes = Followers::get_inboxes( $user_id ); + + $mentioned_inboxes = array(); + $cc = $activity->get_cc(); + if ( $cc ) { + $mentioned_inboxes = Mention::get_inboxes( $cc ); + } $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); $inboxes = array_unique( $inboxes ); + if ( empty( $inboxes ) ) { + return; + } + $json = $activity->to_json(); foreach ( $inboxes as $inbox ) { diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 61deab7..60e3f00 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -6,12 +6,15 @@ use Activitypub\Collection\Users; use Activitypub\Collection\Followers; use Activitypub\Transformer\Post; +use function Activitypub\is_user_type_disabled; + /** * ActivityPub Scheduler Class * * @author Matthias Pfefferle */ class Scheduler { + /** * Initialize the class, registering WordPress hooks */ @@ -58,6 +61,22 @@ class Scheduler { // Migration \add_action( 'admin_init', array( self::class, 'schedule_migration' ) ); + + // profile updates for blog options + if ( ! is_user_type_disabled( 'blog' ) ) { + \add_action( 'update_option_site_icon', array( self::class, 'blog_user_update' ) ); + \add_action( 'update_option_blogdescription', array( self::class, 'blog_user_update' ) ); + \add_action( 'update_option_blogname', array( self::class, 'blog_user_update' ) ); + \add_filter( 'pre_set_theme_mod_custom_logo', array( self::class, 'blog_user_update' ) ); + \add_filter( 'pre_set_theme_mod_header_image', array( self::class, 'blog_user_update' ) ); + } + + // profile updates for user options + if ( ! is_user_type_disabled( 'user' ) ) { + \add_action( 'wp_update_user', array( self::class, 'user_update' ) ); + \add_action( 'updated_user_meta', array( self::class, 'user_meta_update' ), 10, 3 ); + // @todo figure out a feasible way of updating the header image since it's not unique to any user. + } } /** @@ -255,4 +274,68 @@ class Scheduler { \wp_schedule_single_event( \time(), 'activitypub_schedule_migration' ); } } + + /** + * Send a profile update when relevant user meta is updated. + * + * @param int $meta_id Meta ID being updated. + * @param int $user_id User ID being updated. + * @param string $meta_key Meta key being updated. + * + * @return void + */ + public static function user_meta_update( $meta_id, $user_id, $meta_key ) { + // don't bother if the user can't publish + if ( ! \user_can( $user_id, 'publish_posts' ) ) { + return; + } + // the user meta fields that affect a profile. + $fields = array( + 'activitypub_user_description', + 'description', + 'user_url', + 'display_name', + ); + if ( in_array( $meta_key, $fields, true ) ) { + self::schedule_profile_update( $user_id ); + } + } + + /** + * Send a profile update when a user is updated. + * + * @param int $user_id User ID being updated. + * + * @return void + */ + public static function user_update( $user_id ) { + // don't bother if the user can't publish + if ( ! \user_can( $user_id, 'publish_posts' ) ) { + return; + } + + self::schedule_profile_update( $user_id ); + } + + /** + * Theme mods only have a dynamic filter so we fudge it like this. + * @param mixed $value + * @return mixed + */ + public static function blog_user_update( $value = null ) { + self::schedule_profile_update( 0 ); + return $value; + } + + /** + * Send a profile update to all followers. Gets hooked into all relevant options/meta etc. + * @param int $user_id The user ID to update (Could be 0 for Blog-User). + */ + public static function schedule_profile_update( $user_id ) { + \wp_schedule_single_event( + \time(), + 'activitypub_send_update_profile_activity', + array( $user_id ) + ); + } } diff --git a/readme.txt b/readme.txt index 510a3ca..f31f3ab 100644 --- a/readme.txt +++ b/readme.txt @@ -114,6 +114,7 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu * Added: CSS class for ActivityPub comments to allow custom designs * Added: FEP-2677: Identifying the Application Actor * Added: Basic Comment Federation +* Added: Profile Update Activities * Improved: WebFinger endpoints = 1.3.0 = From a9c65f55d64d38c622a9fec43b9e66661e00fd00 Mon Sep 17 00:00:00 2001 From: Matthew Exon Date: Fri, 22 Dec 2023 11:42:20 +0100 Subject: [PATCH 3/4] show admin notice when permalink structure is plain (#615) * show admin notice when permalink structure is plain fix #609 * fix phpcs issues * remove calculation of static array of issues * small changes * removed unused attribute --------- Co-authored-by: Matthew Exon Co-authored-by: Matthias Pfefferle --- includes/class-admin.php | 32 ++++++++++++++++++++++++++++++++ tests/test-class-admin.php | 18 ++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 tests/test-class-admin.php diff --git a/includes/class-admin.php b/includes/class-admin.php index 7acfff9..dca3aee 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -18,6 +18,7 @@ class Admin { \add_action( 'admin_init', array( self::class, 'register_settings' ) ); \add_action( 'personal_options_update', array( self::class, 'save_user_description' ) ); \add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) ); + \add_action( 'admin_notices', array( self::class, 'admin_notices' ) ); if ( ! is_user_disabled( get_current_user_id() ) ) { \add_action( 'show_user_profile', array( self::class, 'add_profile' ) ); @@ -46,6 +47,37 @@ class Admin { } } + /** + * Display admin menu notices about configuration problems or conflicts. + * + * @return void + */ + public static function admin_notices() { + $permalink_structure = \get_option( 'permalink_structure' ); + if ( empty( $permalink_structure ) ) { + $admin_notice = \__( 'You are using the ActivityPub plugin without setting a permalink structure. This will prevent ActivityPub from working. Please set a permalink structure.', 'activitypub' ); + self::show_admin_notice( $admin_notice, 'error' ); + } + } + + /** + * Display one admin menu notice about configuration problems or conflicts. + * + * @param string $admin_notice The notice to display. + * @param string $level The level of the notice (error, warning, success, info). + * + * @return void + */ + private static function show_admin_notice( $admin_notice, $level ) { + ?> + +
+

+
+ + expectOutputRegex( "/notice-error/" ); + + \delete_option( 'permalink_structure' ); + } + + public function test_has_permalink_structure_no_errors() { + \add_option( 'permalink_structure', '/archives/%post_id%' ); + \do_action( 'admin_notices' ); + $this->expectOutputRegex( "/^((?!notice-error).)*$/s" ); + + \delete_option( 'permalink_structure' ); + } +} From a8078ce72bc93a7d095f19b3bdde055e68939fe8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 22 Dec 2023 17:45:27 +0100 Subject: [PATCH 4/4] Shared Inbox (#617) * init shared inbox * try to get user id from activity * some code formatting * disable ACTIVITYPUB_SHARED_INBOX_FEATURE * done! * do not use the inbox-user at all * fix user check! * fix user check! --- activitypub.php | 1 + includes/handler/class-create.php | 7 +++++- includes/handler/class-delete.php | 23 ++++++++++++++---- includes/handler/class-follow.php | 40 ++++++++++++++++++++++++++----- includes/handler/class-undo.php | 22 ++++++++++++++--- includes/handler/class-update.php | 13 ++++++---- includes/model/class-user.php | 12 ++++++++++ includes/rest/class-inbox.php | 36 +++------------------------- 8 files changed, 101 insertions(+), 53 deletions(-) diff --git a/activitypub.php b/activitypub.php index 55d040b..3ec6cb5 100644 --- a/activitypub.php +++ b/activitypub.php @@ -33,6 +33,7 @@ require_once __DIR__ . '/includes/functions.php'; \defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" ); \defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) || \define( 'ACTIVITYPUB_AUTHORIZED_FETCH', false ); \defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false ); +\defined( 'ACTIVITYPUB_SHARED_INBOX_FEATURE' ) || \define( 'ACTIVITYPUB_SHARED_INBOX_FEATURE', false ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); diff --git a/includes/handler/class-create.php b/includes/handler/class-create.php index 2e5d76a..64788da 100644 --- a/includes/handler/class-create.php +++ b/includes/handler/class-create.php @@ -15,7 +15,12 @@ class Create { * Initialize the class, registering WordPress hooks */ public static function init() { - \add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 3 ); + \add_action( + 'activitypub_inbox_create', + array( self::class, 'handle_create' ), + 10, + 3 + ); } /** diff --git a/includes/handler/class-delete.php b/includes/handler/class-delete.php index a8611d4..f2571c0 100644 --- a/includes/handler/class-delete.php +++ b/includes/handler/class-delete.php @@ -15,11 +15,24 @@ class Delete { * Initialize the class, registering WordPress hooks */ public static function init() { - \add_action( 'activitypub_inbox_delete', array( self::class, 'handle_delete' ), 10, 2 ); + \add_action( + 'activitypub_inbox_delete', + array( self::class, 'handle_delete' ) + ); + // defer signature verification for `Delete` requests. - \add_filter( 'activitypub_defer_signature_verification', array( self::class, 'defer_signature_verification' ), 10, 2 ); + \add_filter( + 'activitypub_defer_signature_verification', + array( self::class, 'defer_signature_verification' ), + 10, + 2 + ); + // side effect - \add_action( 'activitypub_delete_actor_interactions', array( self::class, 'delete_interactions' ), 10, 1 ); + \add_action( + 'activitypub_delete_actor_interactions', + array( self::class, 'delete_interactions' ) + ); } /** @@ -28,7 +41,7 @@ class Delete { * @param array $activity The delete activity. * @param int $user_id The ID of the user performing the delete activity. */ - public static function handle_delete( $activity, $user_id ) { + public static function handle_delete( $activity ) { $object_type = isset( $activity['object']['type'] ) ? $activity['object']['type'] : ''; switch ( $object_type ) { @@ -39,7 +52,7 @@ class Delete { case 'Organization': case 'Service': case 'Application': - self::maybe_delete_follower( $user_id, $activity ); + self::maybe_delete_follower( $activity ); break; // Object and Link Types // @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types diff --git a/includes/handler/class-follow.php b/includes/handler/class-follow.php index 6855dbd..810680b 100644 --- a/includes/handler/class-follow.php +++ b/includes/handler/class-follow.php @@ -14,8 +14,17 @@ class Follow { * Initialize the class, registering WordPress hooks */ public static function init() { - \add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow' ), 10, 2 ); - \add_action( 'activitypub_followers_post_follow', array( self::class, 'send_follow_response' ), 10, 4 ); + \add_action( + 'activitypub_inbox_follow', + array( self::class, 'handle_follow' ) + ); + + \add_action( + 'activitypub_followers_post_follow', + array( self::class, 'send_follow_response' ), + 10, + 4 + ); } /** @@ -24,11 +33,30 @@ class Follow { * @param array $activity The activity object * @param int $user_id The user ID */ - public static function handle_follow( $activity, $user_id ) { - // save follower - $follower = Followers::add_follower( $user_id, $activity['actor'] ); + public static function handle_follow( $activity ) { + $user = Users::get_by_resource( $activity['object'] ); - do_action( 'activitypub_followers_post_follow', $activity['actor'], $activity, $user_id, $follower ); + if ( ! $user || is_wp_error( $user ) ) { + // If we can not find a user, + // we can not initiate a follow process + return; + } + + $user_id = $user->get__id(); + + // save follower + $follower = Followers::add_follower( + $user_id, + $activity['actor'] + ); + + do_action( + 'activitypub_followers_post_follow', + $activity['actor'], + $activity, + $user_id, + $follower + ); } /** diff --git a/includes/handler/class-undo.php b/includes/handler/class-undo.php index 13c06f3..74d3dca 100644 --- a/includes/handler/class-undo.php +++ b/includes/handler/class-undo.php @@ -1,6 +1,7 @@ get__id(); + Followers::remove_follower( $user_id, $activity['actor'] ); } } diff --git a/includes/handler/class-update.php b/includes/handler/class-update.php index 00e0430..002c3d5 100644 --- a/includes/handler/class-update.php +++ b/includes/handler/class-update.php @@ -14,7 +14,10 @@ class Update { * Initialize the class, registering WordPress hooks */ public static function init() { - \add_action( 'activitypub_inbox_update', array( self::class, 'handle_update' ), 10, 2 ); + \add_action( + 'activitypub_inbox_update', + array( self::class, 'handle_update' ) + ); } /** @@ -23,7 +26,7 @@ class Update { * @param array $array The activity-object * @param int $user_id The id of the local blog-user */ - public static function handle_update( $array, $user_id ) { + public static function handle_update( $array ) { $object_type = isset( $array['object']['type'] ) ? $array['object']['type'] : ''; switch ( $object_type ) { @@ -45,7 +48,7 @@ class Update { case 'Video': case 'Event': case 'Document': - self::update_interaction( $array, $user_id ); + self::update_interaction( $array ); break; // Minimal Activity // @see https://www.w3.org/TR/activitystreams-core/#example-1 @@ -62,7 +65,7 @@ class Update { * * @return void */ - public static function update_interaction( $activity, $user_id ) { + public static function update_interaction( $activity ) { $state = Interactions::update_comment( $activity ); $reaction = null; @@ -70,7 +73,7 @@ class Update { $reaction = \get_comment( $state ); } - \do_action( 'activitypub_handled_update', $activity, $user_id, $state, $reaction ); + \do_action( 'activitypub_handled_update', $activity, null, $state, $reaction ); } /** diff --git a/includes/model/class-user.php b/includes/model/class-user.php index c713434..c22f83c 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -226,6 +226,18 @@ class User extends Actor { return get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $this->get__id() ) ); } + public function get_endpoints() { + $endpoints = null; + + if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) { + $endpoints = array( + 'sharedInbox' => get_rest_url_by_path( 'inbox' ), + ); + } + + return $endpoints; + } + /** * Extend the User-Output with Attachments. * diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 938ca90..c527040 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -94,11 +94,8 @@ class Inbox { $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->type = 'OrderedCollectionPage'; $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get__id() ) ); // phpcs:ignore - $json->totalItems = 0; // phpcs:ignore - $json->orderedItems = array(); // phpcs:ignore - $json->first = $json->partOf; // phpcs:ignore // filter output @@ -155,37 +152,10 @@ class Inbox { $data = $request->get_json_params(); $activity = Activity::init_from_array( $data ); $type = $request->get_param( 'type' ); - $users = self::get_recipients( $data ); + $type = \strtolower( $type ); - if ( ! $users ) { - return new WP_Error( - 'rest_invalid_param', - \__( 'No recipients found', 'activitypub' ), - array( - 'status' => 400, - 'params' => array( - 'to' => \__( 'Please check/validate "to" field', 'activitypub' ), - 'bto' => \__( 'Please check/validate "bto" field', 'activitypub' ), - 'cc' => \__( 'Please check/validate "cc" field', 'activitypub' ), - 'bcc' => \__( 'Please check/validate "bcc" field', 'activitypub' ), - 'audience' => \__( 'Please check/validate "audience" field', 'activitypub' ), - ), - ) - ); - } - - foreach ( $users as $user ) { - $user = User_Collection::get_by_various( $user ); - - if ( is_wp_error( $user ) ) { - continue; - } - - $type = \strtolower( $type ); - - \do_action( 'activitypub_inbox', $data, $user->ID, $type, $activity ); - \do_action( "activitypub_inbox_{$type}", $data, $user->ID, $activity ); - } + \do_action( 'activitypub_inbox', $data, null, $type, $activity ); + \do_action( "activitypub_inbox_{$type}", $data, null, $activity ); $rest_response = new WP_REST_Response( array(), 202 ); $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );