Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions includes/class-activitypub.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,18 @@ public static function theme_compat() {
/**
* Add the 'activitypub' capability to users who can publish posts.
*
* New users get the capability by default unless the site was previously in
* blog-only mode (indicated by activitypub_disable_users_by_default option).
*
* @param int $user_id User ID.
*/
public static function user_register( $user_id ) {
// Check if site was previously in blog-only mode.
if ( \get_option( 'activitypub_disable_users_by_default' ) ) {
return;
}

// Add capability to users who can publish posts.
if ( \user_can( $user_id, 'publish_posts' ) ) {
$user = \get_user_by( 'id', $user_id );
$user->add_cap( 'activitypub' );
Expand Down
12 changes: 0 additions & 12 deletions includes/class-comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@

namespace Activitypub;

use Activitypub\Collection\Actors;

/**
* ActivityPub Comment Class.
*
Expand Down Expand Up @@ -139,11 +137,6 @@ public static function are_comments_allowed( $comment ) {
return false;
}

if ( is_single_user() && \user_can( $current_user, 'publish_posts' ) ) {
// On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user.
$current_user = Actors::BLOG_USER_ID;
}

return user_can_activitypub( $current_user );
}

Expand Down Expand Up @@ -243,11 +236,6 @@ public static function should_be_federated( $comment ) {
return false;
}

if ( is_single_user() && \user_can( $user_id, 'activitypub' ) ) {
// On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user.
$user_id = Actors::BLOG_USER_ID;
}

// User is not allowed to federate comments.
if ( ! user_can_activitypub( $user_id ) ) {
return false;
Expand Down
36 changes: 36 additions & 0 deletions includes/class-migration.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ public static function maybe_migrate() {
}

if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) {
self::migrate_actor_mode_to_capabilities();
self::clean_up_inbox();
\wp_schedule_single_event( \time(), 'activitypub_migrate_avatar_to_remote_actors' );
}
Expand Down Expand Up @@ -478,6 +479,41 @@ public static function migrate_to_4_7_2() {
}
}

/**
* Migrate from actor mode settings to capability-based system.
*
* User actors are controlled solely via the 'activitypub' capability.
* This migration handles sites that were previously in blog-only mode
* by setting a flag to prevent new users from automatically getting
* the activitypub capability.
*/
public static function migrate_actor_mode_to_capabilities() {
$actor_mode = \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE );

// If site was in blog-only mode, set flag to disable users by default.
if ( ACTIVITYPUB_BLOG_MODE === $actor_mode ) {
\update_option( 'activitypub_disable_users_by_default', true );
Comment on lines +493 to +495
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration only handles ACTIVITYPUB_BLOG_MODE, but doesn't explicitly handle existing users' capabilities. Sites in ACTIVITYPUB_ACTOR_MODE or ACTIVITYPUB_ACTOR_AND_BLOG_MODE may have different user capability states that aren't being preserved. Consider documenting this migration strategy or adding logic to handle these cases.

Suggested change
// If site was in blog-only mode, set flag to disable users by default.
if ( ACTIVITYPUB_BLOG_MODE === $actor_mode ) {
\update_option( 'activitypub_disable_users_by_default', true );
/**
* Migration strategy:
* - ACTIVITYPUB_BLOG_MODE: Disable users by default.
* - ACTIVITYPUB_ACTOR_MODE: Grant 'activitypub' capability to all users who previously had ActivityPub access.
* - ACTIVITYPUB_ACTOR_AND_BLOG_MODE: Grant 'activitypub' capability to all users who previously had ActivityPub access, and keep blog actor enabled.
* For all modes, clean up legacy options.
*/
if ( defined( 'ACTIVITYPUB_BLOG_MODE' ) && ACTIVITYPUB_BLOG_MODE === $actor_mode ) {
// Blog-only mode: disable users by default.
\update_option( 'activitypub_disable_users_by_default', true );
} elseif (
( defined( 'ACTIVITYPUB_ACTOR_MODE' ) && ACTIVITYPUB_ACTOR_MODE === $actor_mode ) ||
( defined( 'ACTIVITYPUB_ACTOR_AND_BLOG_MODE' ) && ACTIVITYPUB_ACTOR_AND_BLOG_MODE === $actor_mode )
) {
// Actor mode or both: ensure users who previously had ActivityPub access retain the capability.
$users = \get_users( array( 'fields' => array( 'ID' ) ) );
foreach ( $users as $user ) {
$user_obj = \get_userdata( $user->ID );
if ( $user_obj && ! $user_obj->has_cap( 'activitypub' ) ) {
$user_obj->add_cap( 'activitypub' );
}
}
// Optionally, you may want to set 'activitypub_disable_users_by_default' to false.
\update_option( 'activitypub_disable_users_by_default', false );
} else {
// Unknown mode: document that no migration was performed.
// You may want to log this event.

Copilot uses AI. Check for mistakes.

// Remove activitypub capability from all existing users.
$users = \get_users(
array(
'capability__in' => array( 'activitypub' ),
)
);

foreach ( $users as $user ) {
$user->remove_cap( 'activitypub' );
}
}

// Clean up old actor mode option.
\delete_option( 'activitypub_actor_mode' );

// Clean up legacy options if they still exist.
\delete_option( 'activitypub_enable_blog_user' );
\delete_option( 'activitypub_enable_users' );
}

/**
* Update comment counts for posts in batches.
*
Expand Down
24 changes: 0 additions & 24 deletions includes/class-options.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ class Options {
* Initialize the options.
*/
public static function init() {
\add_filter( 'pre_option_activitypub_actor_mode', array( self::class, 'pre_option_activitypub_actor_mode' ) );
\add_filter( 'pre_option_activitypub_authorized_fetch', array( self::class, 'pre_option_activitypub_authorized_fetch' ) );
\add_filter( 'pre_option_activitypub_vary_header', array( self::class, 'pre_option_activitypub_vary_header' ) );

Expand All @@ -39,29 +38,6 @@ public static function delete() {
$wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'activitypub_%'" );
}

/**
* Pre-get option filter for the Actor-Mode.
*
* @param string|false $pre The pre-get option value.
*
* @return string|false The actor mode or false if it should not be filtered.
*/
public static function pre_option_activitypub_actor_mode( $pre ) {
if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) && ACTIVITYPUB_SINGLE_USER_MODE ) {
return ACTIVITYPUB_BLOG_MODE;
}

if ( \defined( 'ACTIVITYPUB_DISABLE_USER' ) && ACTIVITYPUB_DISABLE_USER ) {
return ACTIVITYPUB_BLOG_MODE;
}

if ( \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) && ACTIVITYPUB_DISABLE_BLOG_USER ) {
return ACTIVITYPUB_ACTOR_MODE;
}

return $pre;
}

/**
* Pre-get option filter for the Authorized Fetch.
*
Expand Down
5 changes: 0 additions & 5 deletions includes/class-scheduler.php
Original file line number Diff line number Diff line change
Expand Up @@ -550,11 +550,6 @@ public static function is_locked( $key ) {
* @param int $content_visibility The content visibility.
*/
public static function schedule_announce_activity( $outbox_activity_id, $activity, $actor_id, $content_visibility ) {
// Only if we're in both Blog and User modes.
if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
return;
}

// Only if this isn't the Blog Actor.
if ( Actors::BLOG_USER_ID === $actor_id ) {
return;
Expand Down
36 changes: 9 additions & 27 deletions includes/collection/class-actors.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use Activitypub\Model\Blog;
use Activitypub\Model\User;

use function Activitypub\is_user_type_disabled;
use function Activitypub\normalize_host;
use function Activitypub\normalize_url;
use function Activitypub\object_to_uri;
Expand Down Expand Up @@ -109,14 +108,6 @@ public static function get_id_by_username( $username ) {
Blog::get_default_username() === $username ||
\get_option( 'activitypub_blog_identifier' ) === $username
) {
if ( is_user_type_disabled( 'blog' ) ) {
return new \WP_Error(
'activitypub_user_not_found',
\__( 'Actor not found', 'activitypub' ),
array( 'status' => 404 )
);
}

return self::BLOG_USER_ID;
}

Expand Down Expand Up @@ -342,10 +333,6 @@ public static function get_id_by_various( $id ) {
* @return Actor[] Array of User actor objects.
*/
public static function get_collection() {
if ( is_user_type_disabled( 'user' ) ) {
return array();
}

$users = \get_users(
array(
'capability__in' => array( 'activitypub' ),
Expand Down Expand Up @@ -373,21 +360,16 @@ public static function get_collection() {
* @return int[] Array of User and Blog actor IDs.
*/
public static function get_all_ids() {
$user_ids = array();

if ( ! is_user_type_disabled( 'user' ) ) {
$user_ids = \get_users(
array(
'fields' => 'ID',
'capability__in' => array( 'activitypub' ),
)
);
}
// Get all users with activitypub capability.
$user_ids = \get_users(
array(
'fields' => 'ID',
'capability__in' => array( 'activitypub' ),
)
);

// Also include the blog actor if active.
if ( ! is_user_type_disabled( 'blog' ) ) {
$user_ids[] = self::BLOG_USER_ID;
}
// Include the blog actor.
$user_ids[] = self::BLOG_USER_ID;

return array_map( 'intval', $user_ids );
}
Expand Down
31 changes: 0 additions & 31 deletions includes/collection/class-followers.php
Original file line number Diff line number Diff line change
Expand Up @@ -413,37 +413,6 @@ public static function get_inboxes_for_activity( $json, $actor_id, $batch_size =
return \array_slice( $inboxes, $offset, $batch_size );
}

/**
* Maybe add Inboxes of the Blog User.
*
* @deprecated 7.3.0
*
* @param string $json The ActivityPub Activity JSON.
* @param int $actor_id The WordPress Actor ID.
*
* @return bool True if the Inboxes of the Blog User should be added, false otherwise.
*/
public static function maybe_add_inboxes_of_blog_user( $json, $actor_id ) {
\_deprecated_function( __METHOD__, '7.3.0' );

// Only if we're in both Blog and User modes.
if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
return false;
}
// Only if this isn't the Blog Actor.
if ( Actors::BLOG_USER_ID === $actor_id ) {
return false;
}

$activity = \json_decode( $json, true );
// Only if this is an Update or Delete. Create handles its own "Announce" in dual user mode.
if ( ! \in_array( $activity['type'] ?? null, array( 'Update', 'Delete' ), true ) ) {
return false;
}

return true;
}

/**
* Get all Followers.
*
Expand Down
6 changes: 1 addition & 5 deletions includes/collection/class-replies.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use function Activitypub\get_rest_url_by_path;
use function Activitypub\is_local_comment;
use function Activitypub\is_post_disabled;
use function Activitypub\is_user_type_disabled;

/**
* Class containing code for getting replies Collections and CollectionPages of posts and comments.
Expand Down Expand Up @@ -171,10 +170,7 @@ public static function get_context_collection( $post_id ) {

$author = Actors::get_by_id( $post->post_author );
if ( is_wp_error( $author ) ) {
if ( is_user_type_disabled( 'blog' ) ) {
return false;
}

// Fallback to blog actor.
$author = new Blog();
}

Expand Down
14 changes: 9 additions & 5 deletions includes/constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,6 @@

define( 'ACTIVITYPUB_DATE_TIME_RFC3339', 'Y-m-d\TH:i:s\Z' );

// Define Actor-Modes for the plugin.
define( 'ACTIVITYPUB_ACTOR_MODE', 'actor' );
define( 'ACTIVITYPUB_BLOG_MODE', 'blog' );
define( 'ACTIVITYPUB_ACTOR_AND_BLOG_MODE', 'actor_blog' );

// Post visibility constants.
define( 'ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC', '' );
define( 'ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC', 'quiet_public' );
Expand All @@ -79,6 +74,15 @@
define( 'ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS', 'followers' );
define( 'ACTIVITYPUB_INTERACTION_POLICY_ME', 'me' );

/*
* Actor mode constants.
*
* @deprecated unreleased The Actor Mode is no longer supported.
*/
define( 'ACTIVITYPUB_ACTOR_MODE', 'actor' );
define( 'ACTIVITYPUB_BLOG_MODE', 'blog' );
define( 'ACTIVITYPUB_ACTOR_AND_BLOG_MODE', 'actor_blog' );

// Identifiers that mark an Activity as Public.
define(
'ACTIVITYPUB_PUBLIC_AUDIENCE_IDENTIFIERS',
Expand Down
Loading
Loading