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)[^>]*?>.*?\\1>@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 @@
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+