diff --git a/FEDERATION.md b/FEDERATION.md index 77f84b90b..b1dabe415 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -16,6 +16,7 @@ The WordPress plugin largely follows ActivityPub's server-to-server specificatio - [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md) - [FEP-2677: Identifying the Application Actor](https://codeberg.org/fediverse/fep/src/branch/main/fep/2677/fep-2677.md) - [FEP-2c59: Discovery of a Webfinger address from an ActivityPub actor](https://codeberg.org/fediverse/fep/src/branch/main/fep/2c59/fep-2c59.md) +- [FEP-fb2a: Actor metadata](https://codeberg.org/fediverse/fep/src/branch/main/fep/fb2a/fep-fb2a.md) Partially supported FEPs diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index afef5b712..7e31f344a 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -45,6 +45,8 @@ public static function init() { \add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ) ); + \add_filter( 'activitypub_get_actor_extra_fields', array( self::class, 'default_actor_extra_fields' ), 10, 2 ); + // register several post_types self::register_post_types(); } @@ -458,13 +460,43 @@ private static function register_post_types() { ) ); + \register_post_type( + 'ap_extrafield', + array( + 'labels' => array( + 'name' => _x( 'Extra fields', 'post_type plural name', 'activitypub' ), + 'singular_name' => _x( 'Extra field', 'post_type single name', 'activitypub' ), + 'add_new' => __( 'Add new', 'activitypub' ), + 'add_new_item' => __( 'Add new extra field', 'activitypub' ), + 'new_item' => __( 'New extra field', 'activitypub' ), + 'edit_item' => __( 'Edit extra field', 'activitypub' ), + 'view_item' => __( 'View extra field', 'activitypub' ), + 'all_items' => __( 'All extra fields', 'activitypub' ), + ), + 'public' => false, + 'hierarchical' => false, + 'query_var' => false, + 'has_archive' => false, + 'publicly_queryable' => false, + 'show_in_menu' => false, + 'delete_with_user' => true, + 'can_export' => true, + 'exclude_from_search' => true, + 'show_in_rest' => true, + 'map_meta_cap' => true, + 'show_ui' => true, + 'supports' => array( 'title', 'editor' ), + ) + ); + \do_action( 'activitypub_after_register_post_type' ); } /** - * Add the 'activitypub' query variable so WordPress won't mangle it. + * Add the 'activitypub' capability to users who can publish posts. * * @param int $user_id User ID. + * * @param array $userdata The raw array of data passed to wp_insert_user(). */ public static function user_register( $user_id ) { @@ -473,4 +505,57 @@ public static function user_register( $user_id ) { $user->add_cap( 'activitypub' ); } } + + /** + * Add default extra fields to an actor. + * + * @param array $extra_fields The extra fields. + * @param int $user_id The User-ID. + * + * @return array The extra fields. + */ + public static function default_actor_extra_fields( $extra_fields, $user_id ) { + if ( $extra_fields ) { + return $extra_fields; + } + + $already_migrated = \get_user_meta( $user_id, 'activitypub_default_extra_fields', true ); + + if ( $already_migrated ) { + return $extra_fields; + } + + $defaults = array( + \__( 'Blog', 'activitypub' ) => \home_url( '/' ), + \__( 'Profile', 'activitypub' ) => \get_author_posts_url( $user_id ), + \__( 'Homepage', 'activitypub' ) => \get_the_author_meta( 'user_url', $user_id ), + ); + + foreach ( $defaults as $title => $url ) { + if ( ! $url ) { + continue; + } + + $extra_field = array( + 'post_type' => 'ap_extrafield', + 'post_title' => $title, + 'post_status' => 'publish', + 'post_author' => $user_id, + 'post_content' => sprintf( + '

%s

', + \esc_attr( $url ), + $url, + \wp_parse_url( $url, \PHP_URL_HOST ) + ), + 'comment_status' => 'closed', + ); + + $extra_field_id = wp_insert_post( $extra_field ); + $extra_fields[] = get_post( $extra_field_id ); + } + + \update_user_meta( $user_id, 'activitypub_default_extra_fields', true ); + + return $extra_fields; + } } diff --git a/includes/class-admin.php b/includes/class-admin.php index 33ca33b69..de24daf36 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -3,12 +3,14 @@ use WP_User_Query; use Activitypub\Model\Blog; +use Activitypub\Activitypub; use Activitypub\Collection\Users; use function Activitypub\count_followers; use function Activitypub\is_user_disabled; use function Activitypub\was_comment_received; use function Activitypub\is_comment_federatable; +use function Activitypub\add_default_actor_extra_fields; /** * ActivityPub Admin Class @@ -23,16 +25,21 @@ public static function init() { \add_action( 'admin_menu', array( self::class, 'admin_menu' ) ); \add_action( 'admin_init', array( self::class, 'register_settings' ) ); \add_action( 'load-comment.php', array( self::class, 'edit_comment' ) ); + \add_action( 'load-post.php', array( self::class, 'edit_post' ) ); + \add_action( 'load-edit.php', array( self::class, 'list_posts' ) ); \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' ) ); \add_filter( 'comment_row_actions', array( self::class, 'comment_row_actions' ), 10, 2 ); \add_filter( 'manage_edit-comments_columns', array( static::class, 'manage_comment_columns' ) ); - \add_filter( 'manage_comments_custom_column', array( static::class, 'manage_comments_custom_column' ), 9, 2 ); + \add_action( 'manage_comments_custom_column', array( static::class, 'manage_comments_custom_column' ), 9, 2 ); + + \add_filter( 'manage_posts_columns', array( static::class, 'manage_post_columns' ), 10, 2 ); + \add_action( 'manage_posts_custom_column', array( self::class, 'manage_posts_custom_column' ), 10, 2 ); \add_filter( 'manage_users_columns', array( self::class, 'manage_users_columns' ), 10, 1 ); - \add_filter( 'manage_users_custom_column', array( self::class, 'manage_users_custom_column' ), 10, 3 ); + \add_action( 'manage_users_custom_column', array( self::class, 'manage_users_custom_column' ), 10, 3 ); \add_filter( 'bulk_actions-users', array( self::class, 'user_bulk_options' ) ); \add_filter( 'handle_bulk_actions-users', array( self::class, 'handle_bulk_request' ), 10, 3 ); @@ -62,6 +69,8 @@ public static function admin_menu() { $followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers', 'activitypub' ), 'read', 'activitypub-followers-list', array( self::class, 'followers_list_page' ) ); \add_action( 'load-' . $followers_list_page, array( self::class, 'add_followers_list_help_tab' ) ); + + \add_users_page( \__( 'Extra Fields', 'activitypub' ), \__( 'Extra Fields', 'activitypub' ), 'read', esc_url( admin_url( '/edit.php?post_type=ap_extrafield' ) ) ); } } @@ -76,6 +85,16 @@ public static function admin_notices() { $admin_notice = \__( 'You are using the ActivityPub plugin with a permalink structure of "plain". This will prevent ActivityPub from working. Please go to "Settings" / "Permalinks" and choose a permalink structure other than "plain".', 'activitypub' ); self::show_admin_notice( $admin_notice, 'error' ); } + + $current_screen = get_current_screen(); + + if ( isset( $current_screen->id ) && 'edit-ap_extrafield' === $current_screen->id ) { + ?> +
+ +
+ post_type ) { + return $allcaps; + } + + if ( (int) get_current_user_id() !== (int) $post->post_author ) { + return false; + } + + return $allcaps; + }, + 1, + 3 + ); + } + + /** + * Add ActivityPub specific actions/filters to the post list view + * + * @return void + */ + public static function list_posts() { + // Show only the user's extra fields. + \add_action( + 'pre_get_posts', + function ( $query ) { + if ( $query->get( 'post_type' ) === 'ap_extrafield' ) { + $query->set( 'author', get_current_user_id() ); + } + } + ); + + // Remove all views for the extra fields. + $screen_id = get_current_screen()->id; + + add_filter( + "views_{$screen_id}", + function ( $views ) { + if ( 'ap_extrafield' === get_post_type() ) { + return array(); + } + + return $views; + } + ); + + // Set defaults for new extra fields. + if ( 'edit-ap_extrafield' === $screen_id ) { + Activitypub::default_actor_extra_fields( array(), get_current_user_id() ); + } + } + public static function comment_row_actions( $actions, $comment ) { if ( was_comment_received( $comment ) ) { unset( $actions['edit'] ); @@ -382,12 +463,28 @@ public static function manage_users_columns( $columns ) { * @param array $columns the list of column names */ public static function manage_comment_columns( $columns ) { - $columns['comment_type'] = esc_attr__( 'Comment-Type', 'activitypub' ); + $columns['comment_type'] = esc_attr__( 'Comment-Type', 'activitypub' ); $columns['comment_protocol'] = esc_attr__( 'Protocol', 'activitypub' ); return $columns; } + /** + * Add "post_content" as column for Extra-Fields in WP-Admin + * + * @param array $columns Tthe list of column names. + * @param string $post_type The post type. + */ + public static function manage_post_columns( $columns, $post_type ) { + if ( 'ap_extrafield' === $post_type ) { + $after_key = 'title'; + $index = array_search( $after_key, array_keys( $columns ), true ); + $columns = array_slice( $columns, 0, $index + 1 ) + array( 'extra_field_content' => esc_attr__( 'Content', 'activitypub' ) ) + $columns; + } + + return $columns; + } + /** * Add "comment-type" and "protocol" as column in WP-Admin * @@ -429,6 +526,25 @@ public static function manage_users_custom_column( $output, $column_name, $user_ } } + /** + * Add a column "extra_field_content" to the post list view + * + * @param string $column_name The column name. + * @param int $post_id The post ID. + * + * @return void + */ + public static function manage_posts_custom_column( $column_name, $post_id ) { + $post = get_post( $post_id ); + + if ( 'extra_field_content' === $column_name ) { + $post = get_post( $post_id ); + if ( 'ap_extrafield' === $post->post_type ) { + echo esc_attr( wp_strip_all_tags( $post->post_content ) ); + } + } + } + /** * Add options to the Bulk dropdown on the users page * diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 07eb1c3b7..4faf51a74 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -117,6 +117,11 @@ public static function deregister_schedules() { public static function schedule_post_activity( $new_status, $old_status, $post ) { $post = get_post( $post ); + if ( 'ap_extrafield' === $post->post_type ) { + self::schedule_profile_update( $post->post_author ); + return; + } + // Do not send activities if post is password protected. if ( \post_password_required( $post ) ) { return; @@ -318,7 +323,9 @@ public static function user_update( $user_id ) { /** * Theme mods only have a dynamic filter so we fudge it like this. - * @param mixed $value + * + * @param mixed $value + * * @return mixed */ public static function blog_user_update( $value = null ) { @@ -328,6 +335,7 @@ public static function blog_user_update( $value = null ) { /** * 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 ) { diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index 6fe534cf1..81702d7f4 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -190,7 +190,7 @@ public static function get_data( $uri ) { return $data; } - $webfinger_url = 'https://' . $host . '/.well-known/webfinger?resource=' . rawurlencode( $identifier ); + $webfinger_url = sprintf( 'https://%s/.well-known/webfinger?resource=%s', $host, rawurlencode( $identifier ) ); $response = wp_safe_remote_get( $webfinger_url, diff --git a/includes/collection/class-users.php b/includes/collection/class-users.php index b210504e9..e4596b878 100644 --- a/includes/collection/class-users.php +++ b/includes/collection/class-users.php @@ -8,6 +8,8 @@ use Activitypub\Model\Application; use function Activitypub\object_to_uri; +use function Activitypub\normalize_url; +use function Activitypub\normalize_host; use function Activitypub\url_to_authorid; use function Activitypub\is_user_disabled; @@ -182,8 +184,8 @@ public static function get_by_resource( $resource ) { // check for http(s)://blog.example.com/ if ( - self::normalize_url( site_url() ) === self::normalize_url( $resource ) || - self::normalize_url( home_url() ) === self::normalize_url( $resource ) + normalize_url( site_url() ) === normalize_url( $resource ) || + normalize_url( home_url() ) === normalize_url( $resource ) ) { return self::get_by_id( self::BLOG_USER_ID ); } @@ -197,8 +199,8 @@ public static function get_by_resource( $resource ) { case 'acct': $resource = \str_replace( 'acct:', '', $resource ); $identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) ); - $host = self::normalize_host( \substr( \strrchr( $resource, '@' ), 1 ) ); - $blog_host = self::normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); + $host = normalize_host( \substr( \strrchr( $resource, '@' ), 1 ) ); + $blog_host = normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); if ( $blog_host !== $host ) { return new WP_Error( @@ -253,33 +255,6 @@ public static function get_by_various( $id ) { return self::get_by_username( $id ); } - /** - * Normalize a host. - * - * @param string $host The host. - * - * @return string The normalized host. - */ - public static function normalize_host( $host ) { - return \str_replace( 'www.', '', $host ); - } - - /** - * Normalize a URL. - * - * @param string $url The URL. - * - * @return string The normalized URL. - */ - public static function normalize_url( $url ) { - $url = \untrailingslashit( $url ); - $url = \str_replace( 'https://', '', $url ); - $url = \str_replace( 'http://', '', $url ); - $url = \str_replace( 'www.', '', $url ); - - return $url; - } - /** * Get the User collection. * diff --git a/includes/functions.php b/includes/functions.php index bf923a159..2fa038982 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1,6 +1,7 @@ 'ap_extrafield', + 'nopaging' => true, + 'status' => 'publish', + 'author' => $user_id, + ) + ); + + if ( $extra_fields->have_posts() ) { + $extra_fields = $extra_fields->posts; + } else { + $extra_fields = array(); + } + + return apply_filters( 'activitypub_get_actor_extra_fields', $extra_fields, $user_id ); +} diff --git a/includes/model/class-blog.php b/includes/model/class-blog.php index cc515385c..8dbde45c0 100644 --- a/includes/model/class-blog.php +++ b/includes/model/class-blog.php @@ -368,4 +368,38 @@ public function get_indexable() { return false; } } + + /** + * Extend the User-Output with Attachments. + * + * @return array The extended User-Output. + */ + public function get_attachment() { + $array = array(); + + $array[] = array( + 'type' => 'PropertyValue', + 'name' => \__( 'Blog', 'activitypub' ), + 'value' => \html_entity_decode( + sprintf( + '%s', + \esc_attr( \home_url( '/' ) ), + \esc_url( \home_url( '/' ) ), + \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) + ), + \ENT_QUOTES, + 'UTF-8' + ), + ); + + // Add support for FEP-fb2a, for more information see FEDERATION.md + $array[] = array( + 'type' => 'Link', + 'name' => \__( 'Blog', 'activitypub' ), + 'href' => \esc_url( \home_url( '/' ) ), + 'rel' => array( 'me' ), + ); + + return $array; + } } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index c5f91a318..64233eee8 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -3,6 +3,7 @@ use WP_Query; use WP_Error; +use Activitypub\Migration; use Activitypub\Signature; use Activitypub\Model\Blog; use Activitypub\Activity\Actor; @@ -10,6 +11,7 @@ use function Activitypub\is_user_disabled; use function Activitypub\get_rest_url_by_path; +use function Activitypub\get_actor_extra_fields; class User extends Actor { /** @@ -233,41 +235,74 @@ public function get_endpoints() { * @return array The extended User-Output. */ public function get_attachment() { - $array = array(); - - $array[] = array( - 'type' => 'PropertyValue', - 'name' => \__( 'Blog', 'activitypub' ), - 'value' => \html_entity_decode( - '' . \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) . '', - \ENT_QUOTES, - 'UTF-8' - ), - ); - - $array[] = array( - 'type' => 'PropertyValue', - 'name' => \__( 'Profile', 'activitypub' ), - 'value' => \html_entity_decode( - '' . \wp_parse_url( \get_author_posts_url( $this->get__id() ), \PHP_URL_HOST ) . '', - \ENT_QUOTES, - 'UTF-8' - ), - ); - - if ( \get_the_author_meta( 'user_url', $this->get__id() ) ) { - $array[] = array( + $extra_fields = get_actor_extra_fields( \get_current_user_id() ); + + $attachments = array(); + + foreach ( $extra_fields as $post ) { + $content = \get_the_content( null, false, $post ); + $content = \make_clickable( $content ); + $content = \do_blocks( $content ); + $content = \wptexturize( $content ); + $content = \wp_filter_content_tags( $content ); + // replace script and style elements + $content = \preg_replace( '@<(script|style)[^>]*?>.*?@si', '', $content ); + $content = \strip_shortcodes( $content ); + $content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) ); + + $attachments[] = array( 'type' => 'PropertyValue', - 'name' => \__( 'Website', 'activitypub' ), + 'name' => \get_the_title( $post ), 'value' => \html_entity_decode( - '' . \wp_parse_url( \get_the_author_meta( 'user_url', $this->get__id() ), \PHP_URL_HOST ) . '', + $content, \ENT_QUOTES, 'UTF-8' ), ); + + $link_added = false; + + // Add support for FEP-fb2a, for more information see FEDERATION.md + if ( \class_exists( '\WP_HTML_Tag_Processor' ) ) { + $tags = new \WP_HTML_Tag_Processor( $content ); + $tags->next_tag(); + + if ( 'P' === $tags->get_tag() ) { + $tags->next_tag(); + } + + if ( 'A' === $tags->get_tag() ) { + $tags->set_bookmark( 'link' ); + if ( ! $tags->next_tag() ) { + $tags->seek( 'link' ); + $attachment = array( + 'type' => 'Link', + 'name' => \get_the_title( $post ), + 'href' => \esc_url( $tags->get_attribute( 'href' ) ), + 'rel' => explode( ' ', $tags->get_attribute( 'rel' ) ), + ); + + $link_added = true; + } + } + } + + if ( ! $link_added ) { + $attachment = array( + 'type' => 'Note', + 'name' => \get_the_title( $post ), + 'content' => \html_entity_decode( + $content, + \ENT_QUOTES, + 'UTF-8' + ), + ); + } + + $attachments[] = $attachment; } - return $array; + return $attachments; } /** diff --git a/integration/class-buddypress.php b/integration/class-buddypress.php index 45cfc0d63..8d71c1d23 100644 --- a/integration/class-buddypress.php +++ b/integration/class-buddypress.php @@ -32,7 +32,12 @@ public static function add_user_metadata( $object, $author_id ) { 'type' => 'PropertyValue', 'name' => \__( 'Profile', 'activitypub' ), 'value' => \html_entity_decode( - '' . \wp_parse_url( \bp_core_get_user_domain( $author_id ), \PHP_URL_HOST ) . '', + sprintf( + '%s', + \esc_attr( bp_core_get_user_domain( $author_id ) ), + \bp_core_get_user_domain( $author_id ), + \wp_parse_url( \bp_core_get_user_domain( $author_id ), \PHP_URL_HOST ) + ), \ENT_QUOTES, 'UTF-8' ), @@ -51,7 +56,12 @@ public static function add_user_metadata( $object, $author_id ) { 'type' => 'PropertyValue', 'name' => $blog->blogname, 'value' => \html_entity_decode( - '' . \wp_parse_url( $blog->siteurl, \PHP_URL_HOST ) . '', + sprintf( + '%s', + \esc_attr( $blog->siteurl ), + $blog->siteurl, + \wp_parse_url( $blog->siteurl, \PHP_URL_HOST ) + ), \ENT_QUOTES, 'UTF-8' ), diff --git a/templates/user-settings.php b/templates/user-settings.php index 0d7b3f6eb..9489acbd8 100644 --- a/templates/user-settings.php +++ b/templates/user-settings.php @@ -28,5 +28,40 @@ + + + + + +

+ + + + + + + + + + + +

+ + + + + + +

+ +