',
- wp_kses(
- wpautop( $data['upgrade_notice '] ),
- array(
- 'p' => array(),
- 'a' => array( 'href', 'title' ),
- 'strong' => array(),
- 'em' => array(),
- )
- )
- );
- }
-
/**
* Register Custom Post Types.
*/
diff --git a/includes/wp-admin/class-admin.php b/includes/wp-admin/class-admin.php
index 77ddfd942..b23844350 100644
--- a/includes/wp-admin/class-admin.php
+++ b/includes/wp-admin/class-admin.php
@@ -55,6 +55,11 @@ public static function init() {
\add_filter( 'dashboard_glance_items', array( self::class, 'dashboard_glance_items' ) );
\add_filter( 'plugin_action_links_' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'add_plugin_settings_link' ) );
+ \add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ), 10, 2 );
+
+ if ( site_supports_blocks() ) {
+ \add_action( 'tool_box', array( self::class, 'tool_box' ) );
+ }
}
/**
@@ -595,4 +600,25 @@ public static function add_plugin_settings_link( $actions ) {
return $actions;
}
+
+ /**
+ * Display plugin upgrade notice to users.
+ *
+ * @param array $data The plugin data.
+ * @param object $update The plugin update data.
+ */
+ public static function plugin_update_message( $data, $update ) {
+ if ( ! isset( $update->upgrade_notice ) ) {
+ return;
+ }
+
+ echo ' ' . wp_strip_all_tags( $update->upgrade_notice ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ }
+
+ /**
+ * Adds metabox on wp-admin/tools.php.
+ */
+ public static function tool_box() {
+ \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/toolbox.php' );
+ }
}
From b3a09d0d4caa4acca28aa9b52213a28659a10ba1 Mon Sep 17 00:00:00 2001
From: Konstantin Obenland
Date: Fri, 28 Feb 2025 09:59:08 -0600
Subject: [PATCH 02/10] Upgrades: Add routine that fixes Follower json (#1383)
* Upgrades: Add routine that fixes Follower json
* Slashed function calls
* Fix merge mishaps.
Props @github.
---
CHANGELOG.md | 4 +++
includes/class-migration.php | 45 +++++++++++++++++++++++++
readme.txt | 1 +
tests/includes/class-test-migration.php | 41 ++++++++++++++++++++++
4 files changed, 91 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 322fb55c4..7af50a343 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+* Upgrade script to fix Follower json representations with unescaped backslashes.
+
### Changed
* Bumped minimum required WordPress version to 6.4.
diff --git a/includes/class-migration.php b/includes/class-migration.php
index 4e859596f..539bb9d76 100644
--- a/includes/class-migration.php
+++ b/includes/class-migration.php
@@ -186,6 +186,7 @@ public static function maybe_migrate() {
add_action( 'init', 'flush_rewrite_rules', 20 );
}
if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) {
+ \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_slashing' ) );
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_comment_author_emails' ) );
}
@@ -635,6 +636,50 @@ public static function create_comment_outbox_items( $batch_size = 50, $offset =
return null;
}
+ /**
+ * Update _activitypub_actor_json meta values to ensure they are properly slashed.
+ *
+ * @param int $batch_size Optional. Number of meta values to process per batch. Default 100.
+ * @param int $offset Optional. Number of meta values to skip. Default 0.
+ * @return array|null Array with batch size and offset if there are more meta values to process, null otherwise.
+ */
+ public static function update_actor_json_slashing( $batch_size = 100, $offset = 0 ) {
+ global $wpdb;
+
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery
+ $meta_values = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_activitypub_actor_json' LIMIT %d OFFSET %d",
+ $batch_size,
+ $offset
+ )
+ );
+
+ foreach ( $meta_values as $meta ) {
+ $json = \json_decode( $meta->meta_value, true );
+
+ // If json_decode fails, try adding slashes.
+ if ( null === $json && \json_last_error() !== JSON_ERROR_NONE ) {
+ $escaped_value = \preg_replace( '#\\\\(?!["\\\\/bfnrtu])#', '\\\\\\\\', $meta->meta_value );
+ $json = \json_decode( $escaped_value, true );
+
+ // Update the meta if json_decode succeeds with slashes.
+ if ( null !== $json && \json_last_error() === JSON_ERROR_NONE ) {
+ \update_post_meta( $meta->post_id, '_activitypub_actor_json', \wp_slash( $escaped_value ) );
+ }
+ }
+ }
+
+ if ( \count( $meta_values ) === $batch_size ) {
+ return array(
+ 'batch_size' => $batch_size,
+ 'offset' => $offset + $batch_size,
+ );
+ }
+
+ return null;
+ }
+
/**
* Update comment author emails with webfinger addresses for ActivityPub comments.
*
diff --git a/readme.txt b/readme.txt
index 9ddd8520c..7f9e1bb7e 100644
--- a/readme.txt
+++ b/readme.txt
@@ -131,6 +131,7 @@ For reasons of data protection, it is not possible to see the followers of other
= Unreleased =
+* Added: Upgrade script to fix Follower json representations with unescaped backslashes.
* Changed: Bumped minimum required WordPress version to 6.4.
* Changed: Use a later hook for Posts to get published to the Outbox, to get sure all `post_meta`s and `taxonomy`s are set stored properly.
* Changed: Use webfinger as author email for comments from the Fediverse.
diff --git a/tests/includes/class-test-migration.php b/tests/includes/class-test-migration.php
index a5c979c7c..c51041f0b 100644
--- a/tests/includes/class-test-migration.php
+++ b/tests/includes/class-test-migration.php
@@ -7,9 +7,11 @@
namespace Activitypub\Tests;
+use Activitypub\Collection\Followers;
use Activitypub\Collection\Outbox;
use Activitypub\Migration;
use Activitypub\Comment;
+use Activitypub\Model\Follower;
/**
* Test class for Activitypub Migrate.
@@ -598,6 +600,45 @@ public function test_create_comment_outbox_items_batching() {
$this->assertNull( $result );
}
+ /**
+ * Test update_actor_json_slashing updates unslashed meta values.
+ *
+ * @covers ::update_actor_json_slashing
+ */
+ public function test_update_actor_json_slashing() {
+ $follower = new Follower();
+ $follower->from_array(
+ array(
+ 'type' => 'Person',
+ 'summary' => '
unescaped backslash 04\2024
',
+ )
+ );
+ $unslashed_json = $follower->to_json();
+
+ $post_id = self::factory()->post->create(
+ array(
+ 'post_type' => Followers::POST_TYPE,
+ 'meta_input' => array( '_activitypub_actor_json' => $unslashed_json ),
+ )
+ );
+
+ $original_meta = \get_post_meta( $post_id, '_activitypub_actor_json', true );
+ $this->assertNull( \json_decode( $original_meta, true ) );
+ $this->assertEquals( JSON_ERROR_SYNTAX, \json_last_error() );
+
+ $result = Migration::update_actor_json_slashing();
+
+ // No additional batch should be scheduled.
+ $this->assertNull( $result );
+
+ $updated_meta = \get_post_meta( $post_id, '_activitypub_actor_json', true );
+
+ // Verify the updated value can be successfully decoded.
+ $decoded = \json_decode( $updated_meta, true );
+ $this->assertNotNull( $decoded, 'Updated meta should be valid JSON' );
+ $this->assertEquals( JSON_ERROR_NONE, \json_last_error() );
+ }
+
/**
* Test update_comment_author_emails updates emails with webfinger addresses.
*
From 339ab1d9be3054e67bc3f69a3484d411b7d800c2 Mon Sep 17 00:00:00 2001
From: Konstantin Obenland
Date: Mon, 3 Mar 2025 07:53:38 -0600
Subject: [PATCH 03/10] Readme: Add upgrade notice (#1399)
---
readme.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/readme.txt b/readme.txt
index 7f9e1bb7e..81cdde857 100644
--- a/readme.txt
+++ b/readme.txt
@@ -219,9 +219,9 @@ See full Changelog on [GitHub](https://github.com/Automattic/wordpress-activityp
== Upgrade Notice ==
-= 1.0.0 =
+= 5.4.0 =
-For version 1.0.0 we have completely rebuilt the followers lists. There is a migration from the old format to the new, but it may take some time until the migration is complete. No data will be lost in the process, please give the migration some time.
+Note: This update requires WordPress 6.4+. Please ensure your site meets this requirement before upgrading.
== Installation ==
From 7f4cbdc2b7f6fb8b3bf0ca9385dcf6c9ea060c9d Mon Sep 17 00:00:00 2001
From: Konstantin Obenland
Date: Mon, 3 Mar 2025 07:56:18 -0600
Subject: [PATCH 04/10] Combine sanitization functions (#1397)
* Combine sanitization functions
* Add host_list sanitization
---
CHANGELOG.md | 1 +
includes/class-sanitize.php | 98 ++++++++++++++
includes/wp-admin/class-settings.php | 46 +------
readme.txt | 1 +
tests/includes/class-test-sanitize.php | 175 +++++++++++++++++++++++++
5 files changed, 278 insertions(+), 43 deletions(-)
create mode 100644 includes/class-sanitize.php
create mode 100644 tests/includes/class-test-sanitize.php
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7af50a343..1c81d1e15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
* Upgrade script to fix Follower json representations with unescaped backslashes.
+* Centralized place for sanitization functions.
### Changed
diff --git a/includes/class-sanitize.php b/includes/class-sanitize.php
new file mode 100644
index 000000000..c669490ee
--- /dev/null
+++ b/includes/class-sanitize.php
@@ -0,0 +1,98 @@
+ $sanitized,
+ 'search_columns' => array( 'user_login', 'user_nicename' ),
+ 'number' => 1,
+ 'hide_empty' => true,
+ 'fields' => 'ID',
+ )
+ );
+
+ if ( $user->get_results() ) {
+ \add_settings_error(
+ 'activitypub_blog_identifier',
+ 'activitypub_blog_identifier',
+ \esc_html__( 'You cannot use an existing author’s name for the blog profile ID.', 'activitypub' )
+ );
+
+ return Blog::get_default_username();
+ }
+
+ return $sanitized;
+ }
+}
diff --git a/includes/wp-admin/class-settings.php b/includes/wp-admin/class-settings.php
index 41be113a6..42805140c 100644
--- a/includes/wp-admin/class-settings.php
+++ b/includes/wp-admin/class-settings.php
@@ -9,6 +9,7 @@
use Activitypub\Collection\Actors;
use Activitypub\Model\Blog;
+use Activitypub\Sanitize;
use function Activitypub\is_user_disabled;
/**
@@ -136,14 +137,7 @@ public static function register_settings() {
'type' => 'string',
'description' => \__( 'Websites allowed to credit you.', 'activitypub' ),
'default' => \Activitypub\home_host(),
- 'sanitize_callback' => function ( $value ) {
- $value = explode( PHP_EOL, $value );
- $value = array_filter( array_map( 'trim', $value ) );
- $value = array_filter( array_map( 'esc_attr', $value ) );
- $value = implode( PHP_EOL, $value );
-
- return $value;
- },
+ 'sanitize_callback' => array( Sanitize::class, 'host_list' ),
)
);
@@ -197,41 +191,7 @@ public static function register_settings() {
'description' => \esc_html__( 'The Identifier of the Blog-User', 'activitypub' ),
'show_in_rest' => true,
'default' => Blog::get_default_username(),
- 'sanitize_callback' => function ( $value ) {
- // Hack to allow dots in the username.
- $parts = explode( '.', $value );
- $sanitized = array();
-
- foreach ( $parts as $part ) {
- $sanitized[] = \sanitize_title( $part );
- }
-
- $sanitized = implode( '.', $sanitized );
-
- // Check for login or nicename.
- $user = new \WP_User_Query(
- array(
- 'search' => $sanitized,
- 'search_columns' => array( 'user_login', 'user_nicename' ),
- 'number' => 1,
- 'hide_empty' => true,
- 'fields' => 'ID',
- )
- );
-
- if ( $user->results ) {
- add_settings_error(
- 'activitypub_blog_identifier',
- 'activitypub_blog_identifier',
- \esc_html__( 'You cannot use an existing author\'s name for the blog profile ID.', 'activitypub' ),
- 'error'
- );
-
- return Blog::get_default_username();
- }
-
- return $sanitized;
- },
+ 'sanitize_callback' => array( Sanitize::class, 'blog_identifier' ),
)
);
diff --git a/readme.txt b/readme.txt
index 81cdde857..fed549800 100644
--- a/readme.txt
+++ b/readme.txt
@@ -132,6 +132,7 @@ For reasons of data protection, it is not possible to see the followers of other
= Unreleased =
* Added: Upgrade script to fix Follower json representations with unescaped backslashes.
+* Added: Centralized place for sanitization functions.
* Changed: Bumped minimum required WordPress version to 6.4.
* Changed: Use a later hook for Posts to get published to the Outbox, to get sure all `post_meta`s and `taxonomy`s are set stored properly.
* Changed: Use webfinger as author email for comments from the Fediverse.
diff --git a/tests/includes/class-test-sanitize.php b/tests/includes/class-test-sanitize.php
new file mode 100644
index 000000000..514cdb5bc
--- /dev/null
+++ b/tests/includes/class-test-sanitize.php
@@ -0,0 +1,175 @@
+ array(
+ array(
+ 'https://example.com',
+ 'https://example.com',
+ 'not-a-url',
+ 'https://wordpress.org',
+ ),
+ array(
+ 'https://example.com',
+ 'http://not-a-url',
+ 'https://wordpress.org',
+ ),
+ ),
+ 'mixed_urls_in_string_whitespace' => array(
+ "https://example.com\nnot-a-url\nhttps://wordpress.org ",
+ array(
+ 'https://example.com',
+ 'http://not-a-url',
+ 'https://wordpress.org',
+ ),
+ ),
+ 'special_characters' => array(
+ array(
+ 'https://example.com/path with spaces ',
+ 'https://example.com/über/path',
+ 'https://example.com/path?param=value¶m2=value2#section',
+ ),
+ array(
+ 'https://example.com/path%20with%20spaces',
+ 'https://example.com/über/path',
+ 'https://example.com/path?param=value¶m2=value2#section',
+ ),
+ ),
+ 'empty_array' => array( array(), array() ),
+ );
+ }
+
+ /**
+ * Test url_list with various inputs.
+ *
+ * @dataProvider url_list_provider
+ * @covers ::url_list
+ *
+ * @param mixed $input Input value.
+ * @param array $expected Expected output.
+ */
+ public function test_url_list( $input, $expected ) {
+ $this->assertEquals( $expected, Sanitize::url_list( $input ) );
+ }
+
+ /**
+ * Data provider for host list tests.
+ *
+ * @return array Test data.
+ */
+ public function host_list_provider() {
+ return array(
+ 'single_valid_host' => array(
+ 'example.com',
+ 'example.com',
+ ),
+ 'multiple_valid_hosts' => array(
+ "ftp://example.com\nhttp://wordpress.org\nhttps://test.example.com",
+ "example.com\nwordpress.org\ntest.example.com",
+ ),
+ 'mixed_case_hosts' => array(
+ "ExAmPlE.cOm\nWoRdPrEsS.oRg",
+ "example.com\nwordpress.org",
+ ),
+ 'invalid_hosts' => array(
+ " not-a-domain\n\nexample.com\n\t@invalid.com",
+ "not-a-domain\nexample.com\ninvalid.com",
+ ),
+ 'empty_string' => array(
+ '',
+ '',
+ ),
+ );
+ }
+
+ /**
+ * Test host_list with various inputs.
+ *
+ * @dataProvider host_list_provider
+ * @covers ::host_list
+ *
+ * @param string $input Input value.
+ * @param string $expected Expected output.
+ */
+ public function test_host_list( $input, $expected ) {
+ $this->assertEquals( $expected, Sanitize::host_list( $input ) );
+ }
+
+ /**
+ * Data provider for blog identifier tests.
+ *
+ * @return array Test data.
+ */
+ public function blog_identifier_provider() {
+ return array(
+ 'simple_string' => array( 'test-Blog', 'test-blog' ),
+ 'with_spaces' => array( 'test blog', 'test-blog' ),
+ 'with_dots' => array( 'test.blog', 'test.blog' ),
+ 'special_chars' => array( 'test@#$%^&*blog', 'testblog' ),
+ 'multiple_dots' => array( 'test.blog.name', 'test.blog.name' ),
+ );
+ }
+
+ /**
+ * Test blog_identifier with various inputs.
+ *
+ * @dataProvider blog_identifier_provider
+ * @covers ::blog_identifier
+ *
+ * @param string $input Input value.
+ * @param string $expected Expected output.
+ */
+ public function test_blog_identifier( $input, $expected ) {
+ $this->assertEquals( $expected, Sanitize::blog_identifier( $input ) );
+ }
+
+ /**
+ * Test blog_identifier with an existing username.
+ *
+ * @covers ::blog_identifier
+ */
+ public function test_blog_identifier_with_existing_user() {
+ $user_id = self::factory()->user->create(
+ array(
+ 'user_login' => 'existing-user',
+ 'user_nicename' => 'test-nicename',
+ )
+ );
+
+ $result = Sanitize::blog_identifier( 'existing-user' );
+
+ $this->assertEquals( \Activitypub\Model\Blog::get_default_username(), $result );
+ $this->assertNotEmpty( get_settings_errors( 'activitypub_blog_identifier' ) );
+
+ // Reset.
+ $GLOBALS['wp_settings_errors'] = array();
+
+ $result = Sanitize::blog_identifier( 'test-nicename' );
+
+ $this->assertEquals( \Activitypub\Model\Blog::get_default_username(), $result );
+ $this->assertNotEmpty( get_settings_errors( 'activitypub_blog_identifier' ) );
+
+ \wp_delete_user( $user_id );
+ }
+}
From b32dd859fc307cca445afb94bdbdac46fa7f8809 Mon Sep 17 00:00:00 2001
From: Matthias Pfefferle
Date: Mon, 3 Mar 2025 15:32:59 +0100
Subject: [PATCH 05/10] Improve `/@username` URLs (#1390)
---
CHANGELOG.md | 4 ++
includes/class-activitypub.php | 60 +++++++++++++++--------
includes/class-migration.php | 4 +-
includes/class-query.php | 4 +-
includes/rest/class-actors-controller.php | 11 +----
readme.txt | 1 +
tests/includes/class-test-query.php | 23 +++++++++
7 files changed, 71 insertions(+), 36 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c81d1e15..0e8b4cd46 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Use a later hook for Posts to get published to the Outbox, to get sure all `post_meta`s and `taxonomy`s are set stored properly.
* Use webfinger as author email for comments from the Fediverse.
+### Fixed
+
+* Do not redirect `/@username` URLs to the API any more, to improve `AUTHORIZED_FETCH` handling.
+
## [5.3.2] - 2025-02-27
### Fixed
diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php
index 6c58d350d..1af393613 100644
--- a/includes/class-activitypub.php
+++ b/includes/class-activitypub.php
@@ -8,6 +8,7 @@
namespace Activitypub;
use Exception;
+use Activitypub\Collection\Actors;
use Activitypub\Collection\Outbox;
use Activitypub\Collection\Followers;
use Activitypub\Collection\Extra_Fields;
@@ -248,29 +249,50 @@ public static function redirect_canonical( $redirect_url, $requested_url ) {
* @return void
*/
public static function template_redirect() {
+ global $wp_query;
+
$comment_id = get_query_var( 'c', null );
// Check if it seems to be a comment.
- if ( ! $comment_id ) {
- return;
- }
+ if ( $comment_id ) {
+ $comment = get_comment( $comment_id );
- $comment = get_comment( $comment_id );
+ // Load a 404-page if `c` is set but not valid.
+ if ( ! $comment ) {
+ $wp_query->set_404();
+ return;
+ }
- // Load a 404-page if `c` is set but not valid.
- if ( ! $comment ) {
- global $wp_query;
- $wp_query->set_404();
- return;
- }
+ // Stop if it's not an ActivityPub comment.
+ if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
+ return;
+ }
- // Stop if it's not an ActivityPub comment.
- if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
- return;
+ wp_safe_redirect( get_comment_link( $comment ) );
+ exit;
}
- wp_safe_redirect( get_comment_link( $comment ) );
- exit;
+ $actor = get_query_var( 'actor', null );
+ if ( $actor ) {
+ $actor = Actors::get_by_username( $actor );
+ if ( ! $actor || \is_wp_error( $actor ) ) {
+ $wp_query->set_404();
+ return;
+ }
+
+ if ( is_activitypub_request() ) {
+ return;
+ }
+
+ if ( $actor->get__id() > 0 ) {
+ $redirect_url = $actor->get_url();
+ } else {
+ $redirect_url = get_bloginfo( 'url' );
+ }
+
+ wp_safe_redirect( $redirect_url, 301 );
+ exit;
+ }
}
/**
@@ -284,6 +306,7 @@ public static function add_query_vars( $vars ) {
$vars[] = 'activitypub';
$vars[] = 'preview';
$vars[] = 'author';
+ $vars[] = 'actor';
$vars[] = 'c';
$vars[] = 'p';
@@ -405,12 +428,7 @@ public static function add_rewrite_rules() {
);
}
- \add_rewrite_rule(
- '^@([\w\-\.]+)$',
- 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/$matches[1]',
- 'top'
- );
-
+ \add_rewrite_rule( '^@([\w\-\.]+)\/?$', 'index.php?actor=$matches[1]', 'top' );
\add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
}
diff --git a/includes/class-migration.php b/includes/class-migration.php
index 539bb9d76..9c3207fec 100644
--- a/includes/class-migration.php
+++ b/includes/class-migration.php
@@ -182,12 +182,10 @@ public static function maybe_migrate() {
if ( \version_compare( $version_from_db, '5.2.0', '<' ) ) {
Scheduler::register_schedules();
}
- if ( \version_compare( $version_from_db, '5.3.0', '<' ) ) {
- add_action( 'init', 'flush_rewrite_rules', 20 );
- }
if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) {
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_slashing' ) );
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_comment_author_emails' ) );
+ \add_action( 'init', 'flush_rewrite_rules', 20 );
}
/*
diff --git a/includes/class-query.php b/includes/class-query.php
index 7d66a048c..d65587348 100644
--- a/includes/class-query.php
+++ b/includes/class-query.php
@@ -205,10 +205,10 @@ protected function maybe_get_virtual_object() {
$author_id = url_to_authorid( $url );
if ( ! is_numeric( $author_id ) ) {
- return null;
+ $author_id = $url;
}
- $user = Actors::get_by_id( $author_id );
+ $user = Actors::get_by_various( $author_id );
if ( \is_wp_error( $user ) || ! $user ) {
return null;
diff --git a/includes/rest/class-actors-controller.php b/includes/rest/class-actors-controller.php
index 230b709fe..15cd0c5c0 100644
--- a/includes/rest/class-actors-controller.php
+++ b/includes/rest/class-actors-controller.php
@@ -101,15 +101,6 @@ public function get_item( $request ) {
return $user;
}
- $link_header = \sprintf( '<%1$s>; rel="alternate"; type="application/activity+json"', $user->get_id() );
-
- // Redirect to canonical URL if it is not an ActivityPub request.
- if ( ! is_activitypub_request() ) {
- \header( 'Link: ' . $link_header );
- \header( 'Location: ' . $user->get_canonical_url(), true, 301 );
- exit;
- }
-
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client.
*/
@@ -119,7 +110,7 @@ public function get_item( $request ) {
$response = \rest_ensure_response( $data );
$response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) );
- $response->header( 'Link', $link_header );
+ $response->header( 'Link', \sprintf( '<%1$s>; rel="alternate"; type="application/activity+json"', $user->get_id() ) );
return $response;
}
diff --git a/readme.txt b/readme.txt
index fed549800..2f3c9bc59 100644
--- a/readme.txt
+++ b/readme.txt
@@ -136,6 +136,7 @@ For reasons of data protection, it is not possible to see the followers of other
* Changed: Bumped minimum required WordPress version to 6.4.
* Changed: Use a later hook for Posts to get published to the Outbox, to get sure all `post_meta`s and `taxonomy`s are set stored properly.
* Changed: Use webfinger as author email for comments from the Fediverse.
+* Fixed: Do not redirect `/@username` URLs to the API any more, to improve `AUTHORIZED_FETCH` handling.
= 5.3.2 =
diff --git a/tests/includes/class-test-query.php b/tests/includes/class-test-query.php
index b096b5078..9e3716cb3 100644
--- a/tests/includes/class-test-query.php
+++ b/tests/includes/class-test-query.php
@@ -265,6 +265,29 @@ public function test_comment_activitypub_object() {
$this->assertNull( Query::get_instance()->get_activitypub_object() );
}
+ /**
+ * Test user at URL activity object.
+ *
+ * @covers ::get_activitypub_object
+ */
+ public function test_user_at_url_activity_object() {
+ $user_id = self::factory()->user->create(
+ array(
+ 'user_login' => 'testuser',
+ 'role' => 'author',
+ )
+ );
+
+ Query::get_instance()->__destruct();
+ $user = get_user_by( 'id', $user_id );
+ $at_url = home_url( '/@' . $user->user_login . '/?activitypub' );
+
+ $this->go_to( $at_url );
+ $this->assertNotNull( Query::get_instance()->get_activitypub_object() );
+
+ \wp_delete_user( $user_id );
+ }
+
/**
* Test user activitypub object.
*
From 4692cf5dad25aa553fb9579dcaf141ff04771dd7 Mon Sep 17 00:00:00 2001
From: Konstantin Obenland
Date: Mon, 3 Mar 2025 10:31:26 -0600
Subject: [PATCH 06/10] Release 5.4.0 (#1403)
---
CHANGELOG.md | 5 +++--
activitypub.php | 4 ++--
includes/class-migration.php | 2 +-
readme.txt | 4 ++--
4 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e8b4cd46..3ce971635 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [Unreleased]
+## [5.4.0] - 2025-03-03
### Added
@@ -1361,8 +1361,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* initial
-[Unreleased]: https://github.com/Automattic/wordpress-activitypub/compare/5.3.2...trunk
+[Unreleased]: https://github.com/Automattic/wordpress-activitypub/compare/5.4.0...trunk
+[5.4.0]: https://github.com/Automattic/wordpress-activitypub/compare/5.3.2...5.4.0
[5.3.2]: https://github.com/Automattic/wordpress-activitypub/compare/5.3.1...5.3.2
[5.3.1]: https://github.com/Automattic/wordpress-activitypub/compare/5.3.0...5.3.1
[5.3.0]: https://github.com/Automattic/wordpress-activitypub/compare/5.2.0...5.3.0
diff --git a/activitypub.php b/activitypub.php
index 23e4dad44..34ea205bb 100644
--- a/activitypub.php
+++ b/activitypub.php
@@ -3,7 +3,7 @@
* Plugin Name: ActivityPub
* Plugin URI: https://github.com/Automattic/wordpress-activitypub
* Description: The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format.
- * Version: 5.3.2
+ * Version: 5.4.0
* Author: Matthias Pfefferle & Automattic
* Author URI: https://automattic.com/
* License: MIT
@@ -19,7 +19,7 @@
use WP_CLI;
-\define( 'ACTIVITYPUB_PLUGIN_VERSION', '5.3.2' );
+\define( 'ACTIVITYPUB_PLUGIN_VERSION', '5.4.0' );
// Plugin related constants.
\define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
diff --git a/includes/class-migration.php b/includes/class-migration.php
index 9c3207fec..a7e1b6536 100644
--- a/includes/class-migration.php
+++ b/includes/class-migration.php
@@ -182,7 +182,7 @@ public static function maybe_migrate() {
if ( \version_compare( $version_from_db, '5.2.0', '<' ) ) {
Scheduler::register_schedules();
}
- if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) {
+ if ( \version_compare( $version_from_db, '5.4.0', '<' ) ) {
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_slashing' ) );
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_comment_author_emails' ) );
\add_action( 'init', 'flush_rewrite_rules', 20 );
diff --git a/readme.txt b/readme.txt
index 2f3c9bc59..775be8379 100644
--- a/readme.txt
+++ b/readme.txt
@@ -3,7 +3,7 @@ Contributors: automattic, pfefferle, mattwiebe, obenland, akirk, jeherve, mediaf
Tags: OStatus, fediverse, activitypub, activitystream
Requires at least: 6.4
Tested up to: 6.7
-Stable tag: 5.3.2
+Stable tag: 5.4.0
Requires PHP: 7.2
License: MIT
License URI: http://opensource.org/licenses/MIT
@@ -129,7 +129,7 @@ For reasons of data protection, it is not possible to see the followers of other
== Changelog ==
-= Unreleased =
+= 5.4.0 =
* Added: Upgrade script to fix Follower json representations with unescaped backslashes.
* Added: Centralized place for sanitization functions.
From 41b189412d9ab276168aa975f05a449f6bfade1f Mon Sep 17 00:00:00 2001
From: Alex Kirk
Date: Mon, 3 Mar 2025 18:23:40 +0100
Subject: [PATCH 07/10] EMA: Remove the special handling of comments (#1401)
* EMA: Remove the special handling of comments
* EMA: Link the mentioned username
* Remove unused methods
* Fix conflict
* Update integration/class-enable-mastodon-apps.php
Co-authored-by: Konstantin Obenland
* Use the right class
---------
Co-authored-by: Konstantin Obenland
Co-authored-by: Matthias Pfefferle
---
CHANGELOG.md | 1 +
integration/class-enable-mastodon-apps.php | 114 +--------------------
readme.txt | 1 +
3 files changed, 4 insertions(+), 112 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3ce971635..0cf2c1e92 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Bumped minimum required WordPress version to 6.4.
* Use a later hook for Posts to get published to the Outbox, to get sure all `post_meta`s and `taxonomy`s are set stored properly.
* Use webfinger as author email for comments from the Fediverse.
+* Remove the special handling of comments from Enable Mastodon Apps.
### Fixed
diff --git a/integration/class-enable-mastodon-apps.php b/integration/class-enable-mastodon-apps.php
index 8ba9d7c3e..c165f7b69 100644
--- a/integration/class-enable-mastodon-apps.php
+++ b/integration/class-enable-mastodon-apps.php
@@ -10,11 +10,11 @@
use DateTime;
use Activitypub\Webfinger as Webfinger_Util;
use Activitypub\Http;
+use Activitypub\Mention;
use Activitypub\Collection\Actors;
use Activitypub\Collection\Followers;
use Activitypub\Collection\Extra_Fields;
use Activitypub\Transformer\Factory;
-use Enable_Mastodon_Apps\Mastodon_API;
use Enable_Mastodon_Apps\Entity\Account;
use Enable_Mastodon_Apps\Entity\Status;
use Enable_Mastodon_Apps\Entity\Media_Attachment;
@@ -44,6 +44,7 @@ public static function init() {
\add_filter( 'mastodon_api_statuses', array( self::class, 'api_statuses_external' ), 10, 2 );
\add_filter( 'mastodon_api_status_context', array( self::class, 'api_get_replies' ), 10, 3 );
\add_action( 'mastodon_api_update_credentials', array( self::class, 'api_update_credentials' ), 10, 2 );
+ \add_action( 'mastodon_api_submit_status_text', array( Mention::class, 'the_content' ), 10, 2 );
}
/**
@@ -340,14 +341,6 @@ public static function api_status( $status, $post_id ) {
return $status;
}
- // EMA makes a `comment` post_type to mirror comments and so that there can be a single get_posts() call for everything.
- if ( get_post_type( $post ) === 'comment' ) {
- $comment_id = get_post_meta( $post->ID, 'comment_id', true );
- if ( $comment_id ) {
- return self::api_comment_status( $comment_id, $post_id );
- }
- }
-
return self::api_post_status( $post_id );
}
@@ -372,109 +365,6 @@ private static function api_post_status( $post_id ) {
return self::activity_to_status( $data, $account, $post_id );
}
- /**
- * Traditional WP commenters may leave a URL, which itself may be a valid actor.
- * If so, we'll use that actor's data to represent the comment.
- *
- * @param string $url The URL.
- * @return Account|false The account or false.
- */
- private static function maybe_get_account_for_actor( $url ) {
- if ( empty( $url ) ) {
- return false;
- }
- $uri = Webfinger_Util::resolve( $url );
- if ( $uri && ! is_wp_error( $uri ) ) {
- return self::get_account_for_actor( $uri );
- }
- // Next, if the URL does not have a path, we'll try to resolve it in the form of domain.com@domain.com.
- $parts = \wp_parse_url( $url );
- if ( ( ! isset( $parts['path'] ) || ! $parts['path'] ) && isset( $parts['host'] ) ) {
- $url = trailingslashit( $url ) . '@' . $parts['host'];
- $acct = Webfinger_Util::uri_to_acct( $url );
- if ( $acct && ! is_wp_error( $acct ) ) {
- return self::get_account_for_actor( $acct );
- }
- }
-
- return false;
- }
-
- /**
- * Convert an local WP comment into a pseudo-account, after first checking if their
- * supplied URL is a valid actor.
- *
- * @param \WP_Comment $comment The comment.
- * @return Account The account.
- */
- private static function get_account_for_local_comment( $comment ) {
- $maybe_actor = self::maybe_get_account_for_actor( $comment->comment_author_url );
- if ( $maybe_actor ) {
- return $maybe_actor;
- }
-
- // We will make a pretend local account for this comment.
- $account = new Account();
- $account->id = 999999; // This is a fake ID.
- $account->username = $comment->comment_author;
- $account->acct = sprintf( 'comments@%s', wp_parse_url( home_url(), PHP_URL_HOST ) );
- $account->display_name = $comment->comment_author;
- $account->url = get_comment_link( $comment );
- $account->avatar = get_avatar_url( $comment->comment_author_email );
- $account->avatar_static = $account->avatar;
- $account->created_at = new DateTime( $comment->comment_date_gmt );
- $account->last_status_at = new DateTime( $comment->comment_date_gmt );
- $account->note = sprintf(
- /* translators: %s: comment author name */
- __( 'This is a local comment by %s, not a fediverse comment. This profile cannot be followed.', 'activitypub' ),
- $comment->comment_author
- );
-
- return $account;
- }
-
- /**
- * Convert a WordPress comment to a Status.
- *
- * @param int $comment_id The comment ID.
- * @param int $post_id The post ID (this is the mirrored `comment` post).
- *
- * @return Status|null The status.
- */
- private static function api_comment_status( $comment_id, $post_id ) {
- $comment = get_comment( $comment_id );
- $post = get_post( $post_id );
- if ( ! $comment || ! $post ) {
- return null;
- }
-
- $is_remote_comment = get_comment_meta( $comment->comment_ID, 'protocol', true ) === 'activitypub';
-
- if ( $is_remote_comment ) {
- $account = self::get_account_for_actor( $comment->comment_author_url );
- // @todo fallback to locally stored data from the time the comment was made,
- // if the remote actor is not found/no longer available.
- } else {
- $account = self::get_account_for_local_comment( $comment );
- }
-
- if ( ! $account ) {
- return null;
- }
-
- $status = new Status();
- $status->id = $comment->comment_ID;
- $status->created_at = new DateTime( $comment->comment_date_gmt );
- $status->content = $comment->comment_content;
- $status->account = $account;
- $status->visibility = 'public';
- $status->uri = get_comment_link( $comment );
- $status->in_reply_to_id = $post->post_parent;
-
- return $status;
- }
-
-
/**
* Get account for actor.
*
diff --git a/readme.txt b/readme.txt
index 775be8379..f99f7c6b5 100644
--- a/readme.txt
+++ b/readme.txt
@@ -136,6 +136,7 @@ For reasons of data protection, it is not possible to see the followers of other
* Changed: Bumped minimum required WordPress version to 6.4.
* Changed: Use a later hook for Posts to get published to the Outbox, to get sure all `post_meta`s and `taxonomy`s are set stored properly.
* Changed: Use webfinger as author email for comments from the Fediverse.
+* Changed: Remove the special handling of comments from Enable Mastodon Apps.
* Fixed: Do not redirect `/@username` URLs to the API any more, to improve `AUTHORIZED_FETCH` handling.
= 5.3.2 =
From f21185613fbcad9cabec1821f3d4cf4d90b8bd06 Mon Sep 17 00:00:00 2001
From: Konstantin Obenland
Date: Mon, 3 Mar 2025 11:38:20 -0600
Subject: [PATCH 08/10] Outbox: Account for user option prefix and added meta
data (#1402)
* Actor: Add failing tests
* Make tests pass.
* Use Actor API to trigger updates
* Add activitypub_icon to allow list.
* Order alphabetically
* Only delete post if there is one.
* Add changelog
* Keep attachment local.
---------
Co-authored-by: Matthias Pfefferle
---
CHANGELOG.md | 6 +++
includes/scheduler/class-actor.php | 10 +++--
readme.txt | 4 ++
tests/includes/scheduler/class-test-actor.php | 45 ++++++++++++++++++-
4 files changed, 60 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0cf2c1e92..5ed13c464 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [Unreleased]
+
+### Fixed
+
+* Updates to certain user meta fields did not trigger an Update activity.
+
## [5.4.0] - 2025-03-03
### Added
diff --git a/includes/scheduler/class-actor.php b/includes/scheduler/class-actor.php
index 3f4d99a58..e7a49f424 100644
--- a/includes/scheduler/class-actor.php
+++ b/includes/scheduler/class-actor.php
@@ -39,6 +39,7 @@ public static function init() {
// Profile updates for user options.
if ( ! is_user_type_disabled( 'user' ) ) {
\add_action( 'profile_update', array( self::class, 'user_update' ) );
+ \add_action( 'added_user_meta', array( self::class, 'user_meta_update' ), 10, 3 );
\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.
}
@@ -62,13 +63,16 @@ public static function user_meta_update( $meta_id, $user_id, $meta_key ) {
return;
}
+ $blog_prefix = $GLOBALS['wpdb']->get_blog_prefix();
+
// The user meta fields that affect a profile.
$fields = array(
- 'activitypub_description',
- 'activitypub_header_image',
+ $blog_prefix . 'activitypub_description',
+ $blog_prefix . 'activitypub_header_image',
+ $blog_prefix . 'activitypub_icon',
'description',
- 'user_url',
'display_name',
+ 'user_url',
);
if ( in_array( $meta_key, $fields, true ) ) {
diff --git a/readme.txt b/readme.txt
index f99f7c6b5..1fa9da937 100644
--- a/readme.txt
+++ b/readme.txt
@@ -129,6 +129,10 @@ For reasons of data protection, it is not possible to see the followers of other
== Changelog ==
+= Unreleased =
+
+* Fixed: Updates to certain user meta fields did not trigger an Update activity.
+
= 5.4.0 =
* Added: Upgrade script to fix Follower json representations with unescaped backslashes.
diff --git a/tests/includes/scheduler/class-test-actor.php b/tests/includes/scheduler/class-test-actor.php
index 25dec9011..0c4478cc9 100644
--- a/tests/includes/scheduler/class-test-actor.php
+++ b/tests/includes/scheduler/class-test-actor.php
@@ -46,8 +46,6 @@ public static function set_up_before_class() {
*/
public function user_meta_provider() {
return array(
- array( 'activitypub_description' ),
- array( 'activitypub_header_image' ),
array( 'description' ),
array( 'user_url' ),
array( 'display_name' ),
@@ -71,6 +69,49 @@ public function test_user_meta_update( $meta_key ) {
$this->assertSame( $activitpub_id, $id );
}
+ /**
+ * Test user option update scheduling.
+ *
+ * @covers ::user_meta_update
+ */
+ public function test_user_option_update() {
+ $actor = Actors::get_by_id( self::$user_id );
+ $post = $this->get_latest_outbox_item( $actor->get_id() );
+ if ( $post ) {
+ \wp_delete_post( $post->ID, true );
+ }
+
+ $attachment_id = self::factory()->attachment->create_upload_object( dirname( __DIR__, 2 ) . '/assets/test.jpg' );
+
+ // Update activitypub_description.
+ $actor->update_summary( 'test summary' );
+
+ $post = $this->get_latest_outbox_item( $actor->get_id() );
+ $id = \get_post_meta( $post->ID, '_activitypub_object_id', true );
+ $this->assertSame( $actor->get_id(), $id );
+
+ \wp_delete_post( $post->ID, true );
+
+ // Update activitypub_icon.
+ $actor->update_icon( $attachment_id );
+
+ $post = $this->get_latest_outbox_item( $actor->get_id() );
+ $id = \get_post_meta( $post->ID, '_activitypub_object_id', true );
+ $this->assertSame( $actor->get_id(), $id );
+
+ \wp_delete_post( $post->ID, true );
+
+ // Update activitypub_header_image.
+ $actor->update_header( $attachment_id );
+
+ $post = $this->get_latest_outbox_item( $actor->get_id() );
+ $id = \get_post_meta( $post->ID, '_activitypub_object_id', true );
+ $this->assertSame( $actor->get_id(), $id );
+
+ \wp_delete_post( $post->ID, true );
+ \wp_delete_attachment( $attachment_id, true );
+ }
+
/**
* Test user update scheduling.
*
From dd73fb02b7ca209bea1a67df43a5a1035b1a2560 Mon Sep 17 00:00:00 2001
From: Matthias Pfefferle
Date: Mon, 3 Mar 2025 18:50:46 +0100
Subject: [PATCH 09/10] Add "Move Account" functionality (#685)
* init
* simplify code to provide a barebone functionality and improve from there
* re-add namespaces
* fix PHPCS
* Move: Create good starting point
* Removes duplicate actor properties.
* Removes unused `Move` methods.
* Adds unit tests for `extend_actor_profiles`
* Move: General method to move accounts (#1362)
* Move: General method to move accounts
First pass
* Check for wp error
* Add option to directly store full activity instead of only the activity-object
* Change to use Activity-Object instead of Activity
* Lint fixes
* Filter outbox activity in type handlers
* use object_to_uri to check if it is an object first
* Update method return type
* Add unit tests
* use "use"
---------
Co-authored-by: Matthias Pfefferle
* Move: Add support for Blog-User (#1389)
* Move: Add support for Blog-User
* add missing extensions to the user
* Add CLI command
* Update includes/class-cli.php
Co-authored-by: Konstantin Obenland
* Remove used options
---------
Co-authored-by: Konstantin Obenland
* Move: Add/Remove/Modify "Account Aliases" (#1396)
* Add the ability to set Account Aliases
* Fix phpcs issues
* add user settings
* fix PHPCS
* Fix phpcs
* Some changes
props @obenland
* simplify code
* Get blog option from options
* remove duplicates
* First pass at registered user meta
* Add general sanitization class
* Move description below textarea
There's no margin between description and textarea otherwise.
* Same for blog user
---------
Co-authored-by: Konstantin Obenland
---------
Co-authored-by: Konstantin Obenland
---
includes/activity/class-actor.php | 8 +-
includes/class-activitypub.php | 20 +++
includes/class-cli.php | 32 ++++
includes/class-move.php | 91 +++++++++++
includes/collection/class-outbox.php | 21 +--
includes/handler/class-delete.php | 38 ++---
includes/handler/class-like.php | 22 ++-
includes/handler/class-move.php | 17 ++
includes/model/class-blog.php | 18 +++
includes/model/class-user.php | 18 +++
includes/wp-admin/class-admin.php | 7 +
.../wp-admin/class-blog-settings-fields.php | 31 ++++
includes/wp-admin/class-settings-fields.php | 8 +-
includes/wp-admin/class-settings.php | 11 ++
templates/user-settings.php | 20 +++
tests/includes/class-test-move.php | 145 ++++++++++++++++++
16 files changed, 468 insertions(+), 39 deletions(-)
create mode 100644 includes/class-move.php
create mode 100644 tests/includes/class-test-move.php
diff --git a/includes/activity/class-actor.php b/includes/activity/class-actor.php
index 92ffb8a9c..2656938be 100644
--- a/includes/activity/class-actor.php
+++ b/includes/activity/class-actor.php
@@ -43,10 +43,6 @@ class Actor extends Base_Object {
'@id' => 'lemmy:moderators',
'@type' => '@id',
),
- 'attributionDomains' => array(
- '@id' => 'toot:attributionDomains',
- '@type' => '@id',
- ),
'alsoKnownAs' => array(
'@id' => 'as:alsoKnownAs',
'@type' => '@id',
@@ -55,6 +51,10 @@ class Actor extends Base_Object {
'@id' => 'as:movedTo',
'@type' => '@id',
),
+ 'attributionDomains' => array(
+ '@id' => 'toot:attributionDomains',
+ '@type' => '@id',
+ ),
'postingRestrictedToMods' => 'lemmy:postingRestrictedToMods',
'discoverable' => 'toot:discoverable',
'indexable' => 'toot:indexable',
diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php
index 1af393613..4e978ea9c 100644
--- a/includes/class-activitypub.php
+++ b/includes/class-activitypub.php
@@ -48,6 +48,7 @@ public static function init() {
\add_action( 'updated_postmeta', array( self::class, 'updated_postmeta' ), 10, 4 );
\add_action( 'added_post_meta', array( self::class, 'updated_postmeta' ), 10, 4 );
+ \add_action( 'init', array( self::class, 'register_user_meta' ), 11 );
// Register several post_types.
self::register_post_types();
@@ -89,6 +90,8 @@ public static function uninstall() {
delete_option( 'activitypub_authorized_fetch' );
delete_option( 'activitypub_application_user_private_key' );
delete_option( 'activitypub_application_user_public_key' );
+ delete_option( 'activitypub_blog_user_also_known_as' );
+ delete_option( 'activitypub_blog_user_moved_to' );
delete_option( 'activitypub_blog_user_private_key' );
delete_option( 'activitypub_blog_user_public_key' );
delete_option( 'activitypub_blog_description' );
@@ -717,4 +720,21 @@ public static function updated_postmeta( $meta_id, $object_id, $meta_key, $meta_
\delete_post_meta( $object_id, 'activitypub_content_visibility' );
}
}
+
+ /**
+ * Register user meta.
+ */
+ public static function register_user_meta() {
+ \register_meta(
+ 'user',
+ 'activitypub_also_known_as',
+ array(
+ 'type' => 'array',
+ 'description' => 'An array of URLs that the user is known by.',
+ 'single' => false,
+ 'default' => array(),
+ 'sanitize_callback' => array( Sanitize::class, 'url_list' ),
+ )
+ );
+ }
}
diff --git a/includes/class-cli.php b/includes/class-cli.php
index 28936400a..4d42693af 100644
--- a/includes/class-cli.php
+++ b/includes/class-cli.php
@@ -199,4 +199,36 @@ public function reschedule( $args ) {
WP_CLI::success( 'Rescheduled activity.' );
}
+
+ /**
+ * Move the blog to a new URL.
+ *
+ * ## OPTIONS
+ *
+ *
+ * The current URL of the blog.
+ *
+ *
+ * The new URL of the blog.
+ *
+ * ## EXAMPLES
+ *
+ * $ wp activitypub move https://example.com/ https://newsite.com/
+ *
+ * @synopsis
+ *
+ * @param array $args The arguments.
+ */
+ public function move( $args ) {
+ $from = $args[0];
+ $to = $args[1];
+
+ $outbox_item_id = Move::account( $from, $to );
+
+ if ( is_wp_error( $outbox_item_id ) ) {
+ WP_CLI::error( $outbox_item_id->get_error_message() );
+ } else {
+ WP_CLI::success( 'Moved Scheduled.' );
+ }
+ }
}
diff --git a/includes/class-move.php b/includes/class-move.php
new file mode 100644
index 000000000..1f1d806da
--- /dev/null
+++ b/includes/class-move.php
@@ -0,0 +1,91 @@
+get__id() > 0 ) {
+ \update_user_option( $user->get__id(), 'activitypub_move_to', $to );
+ } else {
+ \update_option( 'activitypub_blog_user_moved_to', $to );
+ }
+
+ // Add the old account URL to alsoKnownAs.
+ if ( $user->get__id() > 0 ) {
+ self::update_user_also_known_as( $user->get__id(), $from );
+ } else {
+ self::update_blog_also_known_as( $from );
+ }
+
+ $response = Http::get_remote_object( $to );
+
+ if ( \is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $actor = new Actor();
+ $actor->from_array( $response );
+
+ // Check if the `Move` Activity is valid.
+ $also_known_as = $actor->get_also_known_as();
+ if ( ! in_array( $from, $also_known_as, true ) ) {
+ return new \WP_Error( 'invalid_target', __( 'Invalid target', 'activitypub' ) );
+ }
+
+ // Add to outbox.
+ return add_to_outbox( $actor, 'Move', $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC );
+ }
+
+ /**
+ * Update the alsoKnownAs property of a user.
+ *
+ * @param int $user_id The user ID.
+ * @param string $from The current account URL.
+ */
+ private static function update_user_also_known_as( $user_id, $from ) {
+ $also_known_as = (array) \get_user_option( 'activitypub_also_known_as', $user_id );
+ $also_known_as[] = $from;
+
+ \update_user_option( $user_id, 'activitypub_also_known_as', $also_known_as );
+ }
+
+ /**
+ * Update the alsoKnownAs property of the blog.
+ *
+ * @param string $from The current account URL.
+ */
+ private static function update_blog_also_known_as( $from ) {
+ $also_known_as = (array) \get_option( 'activitypub_blog_user_also_known_as' );
+ $also_known_as[] = $from;
+
+ \update_option( 'activitypub_blog_user_also_known_as', $also_known_as );
+ }
+}
diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php
index c89a55619..b6fb855a1 100644
--- a/includes/collection/class-outbox.php
+++ b/includes/collection/class-outbox.php
@@ -32,7 +32,7 @@ class Outbox {
*
* @return false|int|\WP_Error The added item or an error.
*/
- public static function add( $activity_object, $activity_type, $user_id, $content_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { // phpcs:ignore
+ public static function add( $activity_object, $activity_type, $user_id, $content_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) {
switch ( $user_id ) {
case Actors::APPLICATION_USER_ID:
$actor_type = 'application';
@@ -209,20 +209,21 @@ public static function get_activity( $outbox_item ) {
return $actor;
}
- $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true );
$activity = new Activity();
+ $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true );
$activity->set_type( $type );
$activity->set_id( $outbox_item->guid );
+ $activity->set_actor( $actor->get_id() );
// Pre-fill the Activity with data (for example cc and to).
$activity->set_object( \json_decode( $outbox_item->post_content, true ) );
- $activity->set_actor( $actor->get_id() );
-
- // Use simple Object (only ID-URI) for Like and Announce.
- if ( in_array( $type, array( 'Like', 'Delete' ), true ) ) {
- $activity->set_object( $activity->get_object()->get_id() );
- }
- return $activity;
+ /**
+ * Filters the Activity object before it is returned.
+ *
+ * @param Activity $activity The Activity object.
+ * @param \WP_Post $outbox_item The outbox item post object.
+ */
+ return apply_filters( 'activitypub_get_outbox_activity', $activity, $outbox_item );
}
/**
@@ -259,7 +260,7 @@ public static function get_actor( $outbox_item ) {
* @return Activity|\WP_Error The Activity object or WP_Error.
*/
public static function maybe_get_activity( $outbox_item ) {
- if ( ! $outbox_item || ! $outbox_item instanceof \WP_Post ) {
+ if ( ! $outbox_item instanceof \WP_Post ) {
return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' );
}
diff --git a/includes/handler/class-delete.php b/includes/handler/class-delete.php
index 36f7c7851..37163a1f1 100644
--- a/includes/handler/class-delete.php
+++ b/includes/handler/class-delete.php
@@ -12,6 +12,8 @@
use Activitypub\Collection\Followers;
use Activitypub\Collection\Interactions;
+use function Activitypub\object_to_uri;
+
/**
* Handles Delete requests.
*/
@@ -20,24 +22,10 @@ class Delete {
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
- \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
- );
-
- // Side effect.
- \add_action(
- 'activitypub_delete_actor_interactions',
- array( self::class, 'delete_interactions' )
- );
+ \add_action( 'activitypub_inbox_delete', array( self::class, 'handle_delete' ) );
+ \add_filter( 'activitypub_defer_signature_verification', array( self::class, 'defer_signature_verification' ), 10, 2 );
+ \add_action( 'activitypub_delete_actor_interactions', array( self::class, 'delete_interactions' ) );
+ \add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) );
}
/**
@@ -193,4 +181,18 @@ public static function defer_signature_verification( $defer, $request ) {
return false;
}
+
+ /**
+ * Set the object to the object ID.
+ *
+ * @param \Activitypub\Activity\Activity $activity The Activity object.
+ * @return \Activitypub\Activity\Activity The filtered Activity object.
+ */
+ public static function outbox_activity( $activity ) {
+ if ( 'Delete' === $activity->get_type() ) {
+ $activity->set_object( object_to_uri( $activity->get_object() ) );
+ }
+
+ return $activity;
+ }
}
diff --git a/includes/handler/class-like.php b/includes/handler/class-like.php
index 1e9515763..82676c24b 100644
--- a/includes/handler/class-like.php
+++ b/includes/handler/class-like.php
@@ -20,12 +20,8 @@ class Like {
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
- \add_action(
- 'activitypub_inbox_like',
- array( self::class, 'handle_like' ),
- 10,
- 3
- );
+ \add_action( 'activitypub_inbox_like', array( self::class, 'handle_like' ), 10, 3 );
+ \add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) );
}
/**
@@ -67,4 +63,18 @@ public static function handle_like( $like, $user_id ) {
*/
do_action( 'activitypub_handled_like', $like, $user_id, $state, $reaction );
}
+
+ /**
+ * Set the object to the object ID.
+ *
+ * @param \Activitypub\Activity\Activity $activity The Activity object.
+ * @return \Activitypub\Activity\Activity The filtered Activity object.
+ */
+ public static function outbox_activity( $activity ) {
+ if ( 'Like' === $activity->get_type() ) {
+ $activity->set_object( object_to_uri( $activity->get_object() ) );
+ }
+
+ return $activity;
+ }
}
diff --git a/includes/handler/class-move.php b/includes/handler/class-move.php
index 8bbb81ddb..71032a457 100644
--- a/includes/handler/class-move.php
+++ b/includes/handler/class-move.php
@@ -25,6 +25,7 @@ class Move {
*/
public static function init() {
\add_action( 'activitypub_inbox_move', array( self::class, 'handle_move' ) );
+ \add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) );
}
/**
@@ -101,6 +102,22 @@ public static function handle_move( $activity ) {
}
}
+ /**
+ * Convert the object and origin to the correct format.
+ *
+ * @param \Activitypub\Activity\Activity $activity The Activity object.
+ * @return \Activitypub\Activity\Activity The filtered Activity object.
+ */
+ public static function outbox_activity( $activity ) {
+ if ( 'Move' === $activity->get_type() ) {
+ $activity->set_object( object_to_uri( $activity->get_object() ) );
+ $activity->set_origin( $activity->get_actor() );
+ $activity->set_target( $activity->get_object() );
+ }
+
+ return $activity;
+ }
+
/**
* Extract the target from the activity.
*
diff --git a/includes/model/class-blog.php b/includes/model/class-blog.php
index e94c32806..acec8a061 100644
--- a/includes/model/class-blog.php
+++ b/includes/model/class-blog.php
@@ -552,4 +552,22 @@ public function get_attachment() {
public function get_attribution_domains() {
return get_attribution_domains();
}
+
+ /**
+ * Returns the alsoKnownAs.
+ *
+ * @return array The alsoKnownAs.
+ */
+ public function get_also_known_as() {
+ return \get_option( 'activitypub_blog_user_also_known_as' );
+ }
+
+ /**
+ * Returns the movedTo.
+ *
+ * @return string The movedTo.
+ */
+ public function get_moved_to() {
+ return \get_option( 'activitypub_blog_user_moved_to' );
+ }
}
diff --git a/includes/model/class-user.php b/includes/model/class-user.php
index 581e496c7..e48a826fc 100644
--- a/includes/model/class-user.php
+++ b/includes/model/class-user.php
@@ -421,4 +421,22 @@ public function update_header( $value ) {
public function get_attribution_domains() {
return get_attribution_domains();
}
+
+ /**
+ * Returns the alsoKnownAs.
+ *
+ * @return array The alsoKnownAs.
+ */
+ public function get_also_known_as() {
+ return \get_user_option( 'activitypub_also_known_as', $this->_id );
+ }
+
+ /**
+ * Returns the movedTo.
+ *
+ * @return string The movedTo.
+ */
+ public function get_moved_to() {
+ return \get_user_option( 'activitypub_move_to', $this->_id );
+ }
}
diff --git a/includes/wp-admin/class-admin.php b/includes/wp-admin/class-admin.php
index b23844350..2763e2166 100644
--- a/includes/wp-admin/class-admin.php
+++ b/includes/wp-admin/class-admin.php
@@ -177,6 +177,13 @@ public static function save_user_settings( $user_id ) {
} else {
\delete_user_option( $user_id, 'activitypub_header_image' );
}
+
+ $also_known_as = ! empty( $_POST['activitypub_blog_user_also_known_as'] ) ? \sanitize_textarea_field( wp_unslash( $_POST['activitypub_blog_user_also_known_as'] ) ) : false;
+ if ( $also_known_as ) {
+ \update_user_option( $user_id, 'activitypub_also_known_as', $also_known_as );
+ } else {
+ \delete_user_option( $user_id, 'activitypub_also_known_as' );
+ }
}
/**
diff --git a/includes/wp-admin/class-blog-settings-fields.php b/includes/wp-admin/class-blog-settings-fields.php
index 00c20eb36..573dcc145 100644
--- a/includes/wp-admin/class-blog-settings-fields.php
+++ b/includes/wp-admin/class-blog-settings-fields.php
@@ -70,6 +70,14 @@ public static function register_settings() {
'activitypub_blog_settings',
'activitypub_blog_profile'
);
+
+ add_settings_field(
+ 'activitypub_blog_user_also_known_as',
+ __( 'Account Aliases', 'activitypub' ),
+ array( self::class, 'also_known_as_callback' ),
+ 'activitypub_blog_settings',
+ 'activitypub_blog_profile'
+ );
}
/**
@@ -224,4 +232,27 @@ public static function extra_fields_callback() {
+
+
+
+
+
+
+
+
-
+
null,
)
);
+
+ \register_setting(
+ 'activitypub_blog',
+ 'activitypub_blog_user_also_known_as',
+ array(
+ 'type' => 'array',
+ 'description' => 'An array of URLs that the blog user is known by.',
+ 'default' => array(),
+ 'sanitize_callback' => array( Sanitize::class, 'url_list' ),
+ )
+ );
}
/**
diff --git a/templates/user-settings.php b/templates/user-settings.php
index 6d444e15b..b25384f6e 100644
--- a/templates/user-settings.php
+++ b/templates/user-settings.php
@@ -134,6 +134,26 @@ class=""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/includes/class-test-move.php b/tests/includes/class-test-move.php
new file mode 100644
index 000000000..b334ec4cf
--- /dev/null
+++ b/tests/includes/class-test-move.php
@@ -0,0 +1,145 @@
+user->create( array( 'role' => 'author' ) );
+ }
+
+ /**
+ * Clean up after tests.
+ */
+ public static function tear_down_after_class() {
+ wp_delete_user( self::$user_id );
+ }
+
+ /**
+ * Test the account() method with valid input.
+ *
+ * @covers ::account
+ */
+ public function test_account_with_valid_input() {
+ $from = Actors::get_by_id( self::$user_id )->get_id();
+ $to = 'https://newsite.com/user/1';
+
+ \Activitypub\Move::account( $from, $to );
+
+ $moved_to = Actors::get_by_id( self::$user_id )->get_moved_to();
+ $this->assertEquals( $to, $moved_to );
+
+ $also_known_as = Actors::get_by_id( self::$user_id )->get_also_known_as();
+ $this->assertContains( $from, $also_known_as );
+ }
+
+ /**
+ * Test the account() method with invalid user.
+ *
+ * @covers ::account
+ */
+ public function test_account_with_invalid_user() {
+ $result = \Activitypub\Move::account(
+ 'https://example.com/nonexistent/user',
+ 'https://newsite.com/user/999'
+ );
+
+ $this->assertWPError( $result );
+ $this->assertEquals( 'activitypub_no_user_found', $result->get_error_code() );
+ }
+
+ /**
+ * Test the account() method with invalid target URL.
+ *
+ * @covers ::account
+ */
+ public function test_account_with_invalid_target() {
+ $from = Actors::get_by_id( self::$user_id )->get_id();
+ $to = 'https://invalid-url.com/user/1';
+
+ $filter = function () {
+ return new \WP_Error( 'http_request_failed', 'Invalid URL' );
+ };
+ \add_filter( 'pre_http_request', $filter );
+
+ $result = \Activitypub\Move::account( $from, $to );
+
+ $this->assertWPError( $result );
+ $this->assertEquals( 'http_request_failed', $result->get_error_code() );
+
+ \remove_filter( 'pre_http_request', $filter );
+ }
+
+ /**
+ * Test the account() method with duplicate moves.
+ *
+ * @covers ::account
+ */
+ public function test_account_with_duplicate_moves() {
+ $from = Actors::get_by_id( self::$user_id )->get_id();
+ $to = 'https://newsite.com/user/1';
+
+ \update_user_option( self::$user_id, 'activitypub_also_known_as', array( 'https://old.example.com/user/1' ) );
+
+ $filter = function () use ( $from ) {
+ return array(
+ 'body' => wp_json_encode( array( 'also_known_as' => array( $from ) ) ),
+ 'response' => array( 'code' => 200 ),
+ );
+ };
+ \add_filter( 'pre_http_request', $filter );
+
+ \Activitypub\Move::account( $from, $to );
+
+ $also_known_as = Actors::get_by_id( self::$user_id )->get_also_known_as();
+ $this->assertCount( 2, $also_known_as );
+ $this->assertContains( $from, $also_known_as );
+ $this->assertContains( 'https://old.example.com/user/1', $also_known_as );
+
+ \remove_filter( 'pre_http_request', $filter );
+ }
+
+ /**
+ * Test the account() method with duplicate moves.
+ *
+ * @covers ::account
+ */
+ public function test_account_with_blog_author_as_actor() {
+ // Change user mode to blog author.
+ \update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE );
+
+ $from = Actors::get_by_id( Actors::BLOG_USER_ID )->get_id();
+ $to = 'https://newsite.com/user/0';
+
+ \Activitypub\Move::account( $from, $to );
+
+ $also_known_as = Actors::get_by_id( Actors::BLOG_USER_ID )->get_also_known_as();
+ $this->assertCount( 2, $also_known_as );
+ $this->assertContains( $from, $also_known_as );
+
+ \delete_option( 'activitypub_actor_mode' );
+ }
+}
From f61964c06c6b257d0495fdd307bec2b89ee99746 Mon Sep 17 00:00:00 2001
From: Konstantin Obenland
Date: Mon, 3 Mar 2025 12:33:34 -0600
Subject: [PATCH 10/10] Profile Settings: Use Settings API (#1398)
* init
* Move: General method to move accounts (#1362)
* Move: General method to move accounts
First pass
* Check for wp error
* Add option to directly store full activity instead of only the activity-object
* Change to use Activity-Object instead of Activity
* Lint fixes
* Filter outbox activity in type handlers
* use object_to_uri to check if it is an object first
* Update method return type
* Add unit tests
* use "use"
---------
Co-authored-by: Matthias Pfefferle
* Move: Add support for Blog-User (#1389)
* Move: Add support for Blog-User
* add missing extensions to the user
* Add CLI command
* Update includes/class-cli.php
Co-authored-by: Konstantin Obenland
* Remove used options
---------
Co-authored-by: Konstantin Obenland
* Profile Settings: Use Settings API
* Remove unused parameter.
* fix use of options instead of meta and added missing registers
* Add blog-prefix because we use user-options, not user-meta
* Fix phpcs issue
* Add activitypub_icon meta
* be more restrictive
* use `user_description` instead
* Use wp_kses to account for second arg
* Holding my breath
* fix rebase issues
---------
Co-authored-by: Matthias Pfefferle
---
activitypub.php | 1 +
includes/class-activitypub.php | 44 +++-
includes/wp-admin/class-admin.php | 17 +-
.../wp-admin/class-user-settings-fields.php | 229 ++++++++++++++++++
templates/user-settings.php | 160 ------------
5 files changed, 276 insertions(+), 175 deletions(-)
create mode 100644 includes/wp-admin/class-user-settings-fields.php
delete mode 100644 templates/user-settings.php
diff --git a/activitypub.php b/activitypub.php
index 34ea205bb..1e73b7142 100644
--- a/activitypub.php
+++ b/activitypub.php
@@ -83,6 +83,7 @@ function plugin_init() {
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Settings', 'init' ) );
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Settings_Fields', 'init' ) );
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Blog_Settings_Fields', 'init' ) );
+ \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\User_Settings_Fields', 'init' ) );
if ( site_supports_blocks() ) {
\add_action( 'init', array( __NAMESPACE__ . '\Blocks', 'init' ) );
diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php
index 4e978ea9c..0870d7613 100644
--- a/includes/class-activitypub.php
+++ b/includes/class-activitypub.php
@@ -725,16 +725,56 @@ public static function updated_postmeta( $meta_id, $object_id, $meta_key, $meta_
* Register user meta.
*/
public static function register_user_meta() {
+ $blog_prefix = $GLOBALS['wpdb']->get_blog_prefix();
+
\register_meta(
'user',
- 'activitypub_also_known_as',
+ $blog_prefix . 'activitypub_also_known_as',
array(
'type' => 'array',
'description' => 'An array of URLs that the user is known by.',
- 'single' => false,
+ 'single' => true,
'default' => array(),
'sanitize_callback' => array( Sanitize::class, 'url_list' ),
)
);
+
+ \register_meta(
+ 'user',
+ $blog_prefix . 'activitypub_description',
+ array(
+ 'type' => 'string',
+ 'description' => 'The user’s description.',
+ 'single' => true,
+ 'default' => '',
+ 'sanitize_callback' => function ( $value ) {
+ return wp_kses( $value, 'user_description' );
+ },
+ )
+ );
+
+ \register_meta(
+ 'user',
+ $blog_prefix . 'activitypub_icon',
+ array(
+ 'type' => 'integer',
+ 'description' => 'The attachment ID for user’s profile image.',
+ 'single' => true,
+ 'default' => 0,
+ 'sanitize_callback' => 'absint',
+ )
+ );
+
+ \register_meta(
+ 'user',
+ $blog_prefix . 'activitypub_header_image',
+ array(
+ 'type' => 'integer',
+ 'description' => 'The attachment ID for the user’s header image.',
+ 'single' => true,
+ 'default' => 0,
+ 'sanitize_callback' => 'absint',
+ )
+ );
}
}
diff --git a/includes/wp-admin/class-admin.php b/includes/wp-admin/class-admin.php
index 2763e2166..38207095f 100644
--- a/includes/wp-admin/class-admin.php
+++ b/includes/wp-admin/class-admin.php
@@ -125,23 +125,14 @@ public static function add_followers_list_help_tab() {
}
/**
- * Add the profile.
- *
- * @param \WP_User $user The user object.
+ * Render user settings.
*/
- public static function add_profile( $user ) {
- $description = \get_user_option( 'activitypub_description', $user->ID );
-
+ public static function add_profile() {
wp_enqueue_media();
wp_enqueue_script( 'activitypub-header-image' );
- \load_template(
- ACTIVITYPUB_PLUGIN_DIR . 'templates/user-settings.php',
- true,
- array(
- 'description' => $description,
- )
- );
+ wp_nonce_field( 'activitypub-user-settings', '_apnonce' );
+ do_settings_sections( 'activitypub_user_settings' );
}
/**
diff --git a/includes/wp-admin/class-user-settings-fields.php b/includes/wp-admin/class-user-settings-fields.php
new file mode 100644
index 000000000..4b2640433
--- /dev/null
+++ b/includes/wp-admin/class-user-settings-fields.php
@@ -0,0 +1,229 @@
+ 'activitypub_description' )
+ );
+
+ \add_settings_field(
+ 'activitypub_header_image',
+ \esc_html__( 'Header Image', 'activitypub' ),
+ array( self::class, 'header_image_callback' ),
+ 'activitypub_user_settings',
+ 'activitypub_user_profile',
+ array( 'label_for' => 'activitypub_header_image' )
+ );
+
+ \add_settings_field(
+ 'activitypub_extra_fields',
+ \esc_html__( 'Extra Fields', 'activitypub' ),
+ array( self::class, 'extra_fields_callback' ),
+ 'activitypub_user_settings',
+ 'activitypub_user_profile'
+ );
+
+ \add_settings_field(
+ 'activitypub_also_known_as',
+ \esc_html__( 'Account Aliases', 'activitypub' ),
+ array( self::class, 'also_known_as_callback' ),
+ 'activitypub_user_settings',
+ 'activitypub_user_profile',
+ array( 'label_for' => 'activitypub_blog_user_also_known_as' )
+ );
+ }
+
+ /**
+ * Section description callback.
+ */
+ public static function section_description() {
+ echo '
' . \esc_html__( 'Define what others can see on your public Fediverse profile and next to your posts. With a profile picture and a fully completed profile, you are more likely to gain interactions and followers.', 'activitypub' ) . '
';
+ echo '
' . \esc_html__( 'The ActivityPub plugin tries to take as much information as possible from your profile settings. However, the following settings are not supported by WordPress or should be adjusted independently of the WordPress settings.', 'activitypub' ) . '
';
+ }
+
+ /**
+ * Profile URL field callback.
+ */
+ public static function profile_url_callback() {
+ $user = \Activitypub\Collection\Actors::get_by_id( \get_current_user_id() );
+ ?>
+