Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 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
25 changes: 25 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,30 @@ 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.
}

// 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
85 changes: 20 additions & 65 deletions includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -364,11 +364,8 @@ function user_can_activitypub( $user_id ) {

switch ( $user_id ) {
case Actors::APPLICATION_USER_ID:
$enabled = true; // Application user is always enabled.
break;

case Actors::BLOG_USER_ID:
$enabled = ! is_user_type_disabled( 'blog' );
$enabled = true; // Application and Blog user is always enabled.
break;

default:
Expand All @@ -377,11 +374,7 @@ function user_can_activitypub( $user_id ) {
break;
}

if ( is_user_type_disabled( 'user' ) ) {
$enabled = false;
break;
}

// Check only the capability.
$enabled = \user_can( $user_id, 'activitypub' );
}

Expand All @@ -400,64 +393,30 @@ function user_can_activitypub( $user_id ) {
* This function is used to check if the 'blog' or 'user'
* type is disabled for ActivityPub.
*
* @since unreleased User actors are controlled via the 'activitypub'
* capability. This function now always returns false but is maintained for
* backward compatibility and filter support.
*
* @param string $type User type. 'blog' or 'user'.
*
* @return boolean True if the user type is disabled, false otherwise.
* @return boolean Always returns false (no types are globally disabled).
*/
function is_user_type_disabled( $type ) {
switch ( $type ) {
case 'blog':
if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) {
if ( ACTIVITYPUB_SINGLE_USER_MODE ) {
$disabled = false;
break;
}
}

if ( \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) ) {
$disabled = ACTIVITYPUB_DISABLE_BLOG_USER;
break;
}

if ( ACTIVITYPUB_ACTOR_MODE === \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
$disabled = true;
break;
}

$disabled = false;
break;
case 'user':
if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) {
if ( ACTIVITYPUB_SINGLE_USER_MODE ) {
$disabled = true;
break;
}
}

if ( \defined( 'ACTIVITYPUB_DISABLE_USER' ) ) {
$disabled = ACTIVITYPUB_DISABLE_USER;
break;
}

if ( ACTIVITYPUB_BLOG_MODE === \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
$disabled = true;
break;
}
$disabled = false;

$disabled = false;
break;
default:
$disabled = new \WP_Error(
'activitypub_wrong_user_type',
__( 'Wrong user type', 'activitypub' ),
array( 'status' => 400 )
);
break;
if ( ! in_array( $type, array( 'blog', 'user' ), true ) ) {
$disabled = new \WP_Error(
'activitypub_wrong_user_type',
__( 'Wrong user type', 'activitypub' ),
array( 'status' => 400 )
);
}

/**
* Allow plugins to disable user types for ActivityPub.
*
* Note: This filter is deprecated. Use capability management instead.
*
* @param boolean $disabled True if the user type is disabled, false otherwise.
* @param string $type The User-Type.
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 filter documentation indicates deprecation, but there's no @deprecated tag. Consider adding a proper @deprecated tag with version number to follow WordPress documentation standards and make it clear to developers when this was deprecated (e.g., '@deprecated unreleased').

Suggested change
* @param string $type The User-Type.
* @param string $type The User-Type.
* @deprecated unreleased

Copilot uses AI. Check for mistakes.
*/
Expand Down Expand Up @@ -1311,16 +1270,12 @@ function get_content_warning( $post_id ) {
* @return string|false The ActivityPub ID (a URL) of the User or false if not found.
*/
function get_user_id( $id ) {
$mode = \get_option( 'activitypub_actor_mode', 'default' );
// Try to get the user actor first.
$user = Actors::get_by_id( $id );

if ( ACTIVITYPUB_BLOG_MODE === $mode ) {
// Fallback to blog actor if user not found.
if ( \is_wp_error( $user ) ) {
$user = Actors::get_by_id( Actors::BLOG_USER_ID );
} else {
$user = Actors::get_by_id( $id );

if ( \is_wp_error( $user ) ) {
$user = Actors::get_by_id( Actors::BLOG_USER_ID );
}
}

if ( \is_wp_error( $user ) ) {
Expand Down
Loading