From d5821d849c73f6f948d9e6b05cababef4f042db7 Mon Sep 17 00:00:00 2001
From: Matthias Pfefferle
Date: Tue, 17 Mar 2026 10:55:45 +0100
Subject: [PATCH 1/7] Add configurable distribution modes for federation
delivery
Adds a "Distribution Mode" setting to the Advanced Settings page with
four presets (Default, Balanced, Eco, Custom) that control batch size
and pause between batches for federation delivery. Includes a constant
override via ACTIVITYPUB_DISTRIBUTION_MODE in wp-config.php.
Fixes #2672
---
includes/class-options.php | 121 ++++++++++++++++++
includes/constants.php | 1 +
.../class-advanced-settings-fields.php | 92 +++++++++++++
3 files changed, 214 insertions(+)
diff --git a/includes/class-options.php b/includes/class-options.php
index 1b63697673..4f988c6363 100644
--- a/includes/class-options.php
+++ b/includes/class-options.php
@@ -27,6 +27,10 @@ public static function init() {
\add_filter( 'pre_option_activitypub_following_ui', array( self::class, 'pre_option_activitypub_following_ui' ) );
\add_filter( 'pre_option_activitypub_create_posts', array( self::class, 'pre_option_activitypub_create_posts' ) );
+ \add_filter( 'pre_option_activitypub_distribution_mode', array( self::class, 'pre_option_activitypub_distribution_mode' ) );
+ \add_filter( 'activitypub_dispatcher_batch_size', array( self::class, 'filter_dispatcher_batch_size' ) );
+ \add_filter( 'activitypub_scheduler_async_batch_pause', array( self::class, 'filter_scheduler_batch_pause' ) );
+
\add_filter( 'pre_option_activitypub_allow_likes', array( self::class, 'maybe_disable_interactions' ) );
\add_filter( 'pre_option_activitypub_allow_replies', array( self::class, 'maybe_disable_interactions' ) );
@@ -358,6 +362,42 @@ public static function register_settings() {
)
);
+ \register_setting(
+ 'activitypub_advanced',
+ 'activitypub_distribution_mode',
+ array(
+ 'type' => 'string',
+ 'description' => 'Distribution mode for federation delivery.',
+ 'default' => 'default',
+ 'sanitize_callback' => static function ( $value ) {
+ $allowed = array( 'default', 'balanced', 'eco', 'custom' );
+ return \in_array( $value, $allowed, true ) ? $value : 'default';
+ },
+ )
+ );
+
+ \register_setting(
+ 'activitypub_advanced',
+ 'activitypub_custom_batch_size',
+ array(
+ 'type' => 'integer',
+ 'description' => 'Custom batch size for federation delivery.',
+ 'default' => 100,
+ 'sanitize_callback' => 'absint',
+ )
+ );
+
+ \register_setting(
+ 'activitypub_advanced',
+ 'activitypub_custom_batch_pause',
+ array(
+ 'type' => 'integer',
+ 'description' => 'Custom pause in seconds between batches.',
+ 'default' => 30,
+ 'sanitize_callback' => 'absint',
+ )
+ );
+
/*
* Options Group: activitypub_blog
*/
@@ -640,6 +680,87 @@ public static function default_object_type( $value ) {
return $value;
}
+ /**
+ * Pre-get option filter for the Distribution Mode.
+ *
+ * @since unreleased
+ *
+ * @param string|false $pre The pre-get option value.
+ *
+ * @return string|false The distribution mode or false if it should not be filtered.
+ */
+ public static function pre_option_activitypub_distribution_mode( $pre ) {
+ if ( false !== ACTIVITYPUB_DISTRIBUTION_MODE ) {
+ return ACTIVITYPUB_DISTRIBUTION_MODE;
+ }
+
+ return $pre;
+ }
+
+ /**
+ * Get distribution parameters for the current mode.
+ *
+ * @since unreleased
+ *
+ * @return array { batch_size: int, pause: int }
+ */
+ public static function get_distribution_params() {
+ $mode = \get_option( 'activitypub_distribution_mode', 'default' );
+
+ $modes = array(
+ 'default' => array(
+ 'batch_size' => 100,
+ 'pause' => 30,
+ ),
+ 'balanced' => array(
+ 'batch_size' => 50,
+ 'pause' => 60,
+ ),
+ 'eco' => array(
+ 'batch_size' => 20,
+ 'pause' => 300,
+ ),
+ );
+
+ if ( isset( $modes[ $mode ] ) ) {
+ return $modes[ $mode ];
+ }
+
+ // Custom mode.
+ return array(
+ 'batch_size' => \absint( \get_option( 'activitypub_custom_batch_size', 100 ) ),
+ 'pause' => \absint( \get_option( 'activitypub_custom_batch_pause', 30 ) ),
+ );
+ }
+
+ /**
+ * Filter the dispatcher batch size based on distribution mode.
+ *
+ * @since unreleased
+ *
+ * @param int $batch_size The default batch size.
+ *
+ * @return int The batch size for the current distribution mode.
+ */
+ public static function filter_dispatcher_batch_size( $batch_size ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ $params = self::get_distribution_params();
+ return $params['batch_size'];
+ }
+
+ /**
+ * Filter the scheduler batch pause based on distribution mode.
+ *
+ * @since unreleased
+ *
+ * @param int $pause The default pause in seconds.
+ *
+ * @return int The pause for the current distribution mode.
+ */
+ public static function filter_scheduler_batch_pause( $pause ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ $params = self::get_distribution_params();
+ return $params['pause'];
+ }
+
/**
* Handle relay mode option changes.
*
diff --git a/includes/constants.php b/includes/constants.php
index 8c192d511b..2e14170c5d 100644
--- a/includes/constants.php
+++ b/includes/constants.php
@@ -20,6 +20,7 @@
defined( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS' ) || define( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS', false );
defined( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE' ) || define( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE', 'wordpress-post-format' );
defined( 'ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE' ) || define( 'ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE', 100 );
+defined( 'ACTIVITYPUB_DISTRIBUTION_MODE' ) || define( 'ACTIVITYPUB_DISTRIBUTION_MODE', false );
// Backwards compatibility: map old ACTIVITYPUB_DISABLE_SIDELOADING to ACTIVITYPUB_DISABLE_REMOTE_CACHE.
if ( ! defined( 'ACTIVITYPUB_DISABLE_REMOTE_CACHE' ) && defined( 'ACTIVITYPUB_DISABLE_SIDELOADING' ) ) {
define( 'ACTIVITYPUB_DISABLE_REMOTE_CACHE', ACTIVITYPUB_DISABLE_SIDELOADING );
diff --git a/includes/wp-admin/class-advanced-settings-fields.php b/includes/wp-admin/class-advanced-settings-fields.php
index 00fe100335..b15e1bded5 100644
--- a/includes/wp-admin/class-advanced-settings-fields.php
+++ b/includes/wp-admin/class-advanced-settings-fields.php
@@ -30,6 +30,16 @@ public static function register_advanced_fields() {
'activitypub_advanced_settings'
);
+ if ( ! ACTIVITYPUB_DISTRIBUTION_MODE ) {
+ \add_settings_field(
+ 'activitypub_distribution_mode',
+ \__( 'Distribution Mode', 'activitypub' ),
+ array( self::class, 'render_distribution_mode_field' ),
+ 'activitypub_advanced_settings',
+ 'activitypub_advanced_settings'
+ );
+ }
+
if ( ! defined( 'ACTIVITYPUB_SEND_VARY_HEADER' ) ) {
\add_settings_field(
'activitypub_vary_header',
@@ -288,4 +298,86 @@ public static function render_object_type_field() {
array(
+ 'label' => \__( 'Default', 'activitypub' ),
+ 'description' => \__( 'Deliver activities as fast as possible (100 per batch, 30s pause).', 'activitypub' ),
+ ),
+ 'balanced' => array(
+ 'label' => \__( 'Balanced', 'activitypub' ),
+ 'description' => \__( 'Moderate pace with reasonable pauses between batches (50 per batch, 60s pause).', 'activitypub' ),
+ ),
+ 'eco' => array(
+ 'label' => \__( 'Eco Mode', 'activitypub' ),
+ 'description' => \__( 'Gentle on server resources, ideal for shared hosting (20 per batch, 5min pause).', 'activitypub' ),
+ ),
+ 'custom' => array(
+ 'label' => \__( 'Custom', 'activitypub' ),
+ 'description' => \__( 'Configure batch size and delay manually.', 'activitypub' ),
+ ),
+ );
+
+ ?>
+
+
+
Date: Tue, 17 Mar 2026 10:56:39 +0100
Subject: [PATCH 2/7] Add changelog
---
.github/changelog/3044-from-description | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 .github/changelog/3044-from-description
diff --git a/.github/changelog/3044-from-description b/.github/changelog/3044-from-description
new file mode 100644
index 0000000000..8d3cbfd485
--- /dev/null
+++ b/.github/changelog/3044-from-description
@@ -0,0 +1,4 @@
+Significance: patch
+Type: added
+
+Add a Distribution Mode setting to control how quickly posts are delivered to followers.
From 5bb502dff7557c75de9e3eacef005dc7168f3aa5 Mon Sep 17 00:00:00 2001
From: Matthias Pfefferle
Date: Mon, 30 Mar 2026 18:14:50 +0200
Subject: [PATCH 3/7] Address review feedback for distribution mode settings
- Fix inconsistent constant check: use `false ===` in both the UI
visibility check and the option override
- Validate ACTIVITYPUB_DISTRIBUTION_MODE constant against allowed
modes, falling back to 'default' for invalid values
- Centralize preset definitions in Options::get_distribution_modes()
and reuse in both admin UI and parameter resolution
- Add static cache in get_distribution_params() to avoid rebuilding
presets on every filter call during delivery
- Null-guard the custom fields DOM element in inline script
---
includes/class-options.php | 76 ++++++++++++++-----
.../class-advanced-settings-fields.php | 29 +++----
2 files changed, 68 insertions(+), 37 deletions(-)
diff --git a/includes/class-options.php b/includes/class-options.php
index 345665dc32..a71ab5fa8d 100644
--- a/includes/class-options.php
+++ b/includes/class-options.php
@@ -711,46 +711,84 @@ public static function default_object_type( $value ) {
*/
public static function pre_option_activitypub_distribution_mode( $pre ) {
if ( false !== ACTIVITYPUB_DISTRIBUTION_MODE ) {
- return ACTIVITYPUB_DISTRIBUTION_MODE;
+ $allowed = array_keys( self::get_distribution_modes() );
+ $allowed[] = 'custom';
+
+ if ( \in_array( ACTIVITYPUB_DISTRIBUTION_MODE, $allowed, true ) ) {
+ return ACTIVITYPUB_DISTRIBUTION_MODE;
+ }
+
+ // Invalid constant value, fall back to default.
+ return 'default';
}
return $pre;
}
/**
- * Get distribution parameters for the current mode.
+ * Get the available distribution mode presets.
+ *
+ * Centralized definition used by both the admin UI and the
+ * parameter resolution in get_distribution_params().
*
* @since unreleased
*
- * @return array { batch_size: int, pause: int }
+ * @return array Associative array of mode => { batch_size, pause, label, description }.
*/
- public static function get_distribution_params() {
- $mode = \get_option( 'activitypub_distribution_mode', 'default' );
-
- $modes = array(
+ public static function get_distribution_modes() {
+ return array(
'default' => array(
- 'batch_size' => 100,
- 'pause' => 30,
+ 'batch_size' => 100,
+ 'pause' => 30,
+ 'label' => \__( 'Default', 'activitypub' ),
+ 'description' => \__( 'Deliver activities as fast as possible (100 per batch, 30s pause).', 'activitypub' ),
),
'balanced' => array(
- 'batch_size' => 50,
- 'pause' => 60,
+ 'batch_size' => 50,
+ 'pause' => 60,
+ 'label' => \__( 'Balanced', 'activitypub' ),
+ 'description' => \__( 'Moderate pace with reasonable pauses between batches (50 per batch, 60s pause).', 'activitypub' ),
),
'eco' => array(
- 'batch_size' => 20,
- 'pause' => 300,
+ 'batch_size' => 20,
+ 'pause' => 300,
+ 'label' => \__( 'Eco Mode', 'activitypub' ),
+ 'description' => \__( 'Gentle on server resources, ideal for shared hosting (20 per batch, 5min pause).', 'activitypub' ),
),
);
+ }
+
+ /**
+ * Get distribution parameters for the current mode.
+ *
+ * @since unreleased
+ *
+ * @return array { batch_size: int, pause: int }
+ */
+ public static function get_distribution_params() {
+ static $cached = null;
+
+ if ( null !== $cached ) {
+ return $cached;
+ }
+
+ $mode = \get_option( 'activitypub_distribution_mode', 'default' );
+ $modes = self::get_distribution_modes();
if ( isset( $modes[ $mode ] ) ) {
- return $modes[ $mode ];
+ $cached = array(
+ 'batch_size' => $modes[ $mode ]['batch_size'],
+ 'pause' => $modes[ $mode ]['pause'],
+ );
+ } else {
+ // Custom mode.
+ $cached = array(
+ 'batch_size' => \absint( \get_option( 'activitypub_custom_batch_size', 100 ) ),
+ 'pause' => \absint( \get_option( 'activitypub_custom_batch_pause', 30 ) ),
+ );
}
- // Custom mode.
- return array(
- 'batch_size' => \absint( \get_option( 'activitypub_custom_batch_size', 100 ) ),
- 'pause' => \absint( \get_option( 'activitypub_custom_batch_pause', 30 ) ),
- );
+ return $cached;
}
/**
diff --git a/includes/wp-admin/class-advanced-settings-fields.php b/includes/wp-admin/class-advanced-settings-fields.php
index b15e1bded5..fdbf389e92 100644
--- a/includes/wp-admin/class-advanced-settings-fields.php
+++ b/includes/wp-admin/class-advanced-settings-fields.php
@@ -7,6 +7,8 @@
namespace Activitypub\WP_Admin;
+use Activitypub\Options;
+
/**
* Advanced Settings Fields class.
*/
@@ -30,7 +32,7 @@ public static function register_advanced_fields() {
'activitypub_advanced_settings'
);
- if ( ! ACTIVITYPUB_DISTRIBUTION_MODE ) {
+ if ( false === ACTIVITYPUB_DISTRIBUTION_MODE ) {
\add_settings_field(
'activitypub_distribution_mode',
\__( 'Distribution Mode', 'activitypub' ),
@@ -310,23 +312,11 @@ public static function render_distribution_mode_field() {
$custom_pause = \get_option( 'activitypub_custom_batch_pause', 30 );
$is_custom = 'custom' === $mode;
- $modes = array(
- 'default' => array(
- 'label' => \__( 'Default', 'activitypub' ),
- 'description' => \__( 'Deliver activities as fast as possible (100 per batch, 30s pause).', 'activitypub' ),
- ),
- 'balanced' => array(
- 'label' => \__( 'Balanced', 'activitypub' ),
- 'description' => \__( 'Moderate pace with reasonable pauses between batches (50 per batch, 60s pause).', 'activitypub' ),
- ),
- 'eco' => array(
- 'label' => \__( 'Eco Mode', 'activitypub' ),
- 'description' => \__( 'Gentle on server resources, ideal for shared hosting (20 per batch, 5min pause).', 'activitypub' ),
- ),
- 'custom' => array(
- 'label' => \__( 'Custom', 'activitypub' ),
- 'description' => \__( 'Configure batch size and delay manually.', 'activitypub' ),
- ),
+ // Use centralized presets and add the custom option for the UI.
+ $modes = Options::get_distribution_modes();
+ $modes['custom'] = array(
+ 'label' => \__( 'Custom', 'activitypub' ),
+ 'description' => \__( 'Configure batch size and delay manually.', 'activitypub' ),
);
?>
@@ -371,6 +361,9 @@ public static function render_distribution_mode_field() {
( function() {
var radios = document.querySelectorAll( 'input[name="activitypub_distribution_mode"]' );
var fields = document.getElementById( 'activitypub-custom-distribution-fields' );
+ if ( ! fields ) {
+ return;
+ }
radios.forEach( function( radio ) {
radio.addEventListener( 'change', function() {
fields.style.display = this.value === 'custom' ? '' : 'none';
From 03c4f0bf179f90899b518986cb5036980ce148a7 Mon Sep 17 00:00:00 2001
From: Matthias Pfefferle
Date: Mon, 30 Mar 2026 18:16:08 +0200
Subject: [PATCH 4/7] Document no-inline-namespaces convention in AGENTS.md
Use `use` statements at the top of files instead of inline
fully-qualified class names like \Activitypub\Options::method().
---
AGENTS.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/AGENTS.md b/AGENTS.md
index 0092dcd454..6ac0449f68 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -88,6 +88,8 @@ Text domain: always `'activitypub'`.
**MUST** backslash-prefix all WordPress functions in namespaced code: `\get_option()`, `\add_action()`, `\apply_filters()`, `\__()`, `\_e()`, etc. PHP falls back to global scope, but backslashes are a project standard for consistency and to avoid accidentally shadowing globals.
+**No inline namespaces.** Use `use` statements at the top of the file instead of inline fully-qualified class names (e.g., `use Activitypub\Options;` then `Options::method()`, not `\Activitypub\Options::method()`).
+
**For new or modified code**, MUST use `'unreleased'` for all `@since`, `@deprecated`, and deprecation function version strings so the release script can replace them. Do not introduce new hardcoded version numbers like `'5.1.0'`; existing versioned tags in the codebase are fine.
## Testing Conventions
From ab9ed9c696a38eba94da2b8c331ae67fcb363a88 Mon Sep 17 00:00:00 2001
From: Matthias Pfefferle
Date: Wed, 1 Apr 2026 18:49:07 +0200
Subject: [PATCH 5/7] Address review feedback for distribution modes
- Prevent batch size of 0 with max(1, absint()) sanitization.
- Default mode now passes through the incoming filter value so other
plugins and constants are not silently overridden.
- Remove static cache from get_distribution_params() since get_option()
is already cached by WordPress and the static var caused stale values.
- Add tests for presets, custom mode, sanitization, and filter behavior.
---
includes/class-options.php | 46 ++++++----
.../tests/includes/class-test-options.php | 85 +++++++++++++++++++
2 files changed, 114 insertions(+), 17 deletions(-)
diff --git a/includes/class-options.php b/includes/class-options.php
index a71ab5fa8d..904dd30a2b 100644
--- a/includes/class-options.php
+++ b/includes/class-options.php
@@ -383,7 +383,9 @@ public static function register_settings() {
'type' => 'integer',
'description' => 'Custom batch size for federation delivery.',
'default' => 100,
- 'sanitize_callback' => 'absint',
+ 'sanitize_callback' => static function ( $value ) {
+ return \max( 1, \absint( $value ) );
+ },
)
);
@@ -766,41 +768,42 @@ public static function get_distribution_modes() {
* @return array { batch_size: int, pause: int }
*/
public static function get_distribution_params() {
- static $cached = null;
-
- if ( null !== $cached ) {
- return $cached;
- }
-
$mode = \get_option( 'activitypub_distribution_mode', 'default' );
$modes = self::get_distribution_modes();
if ( isset( $modes[ $mode ] ) ) {
- $cached = array(
+ return array(
'batch_size' => $modes[ $mode ]['batch_size'],
'pause' => $modes[ $mode ]['pause'],
);
- } else {
- // Custom mode.
- $cached = array(
- 'batch_size' => \absint( \get_option( 'activitypub_custom_batch_size', 100 ) ),
- 'pause' => \absint( \get_option( 'activitypub_custom_batch_pause', 30 ) ),
- );
}
- return $cached;
+ // Custom mode.
+ return array(
+ 'batch_size' => \max( 1, \absint( \get_option( 'activitypub_custom_batch_size', 100 ) ) ),
+ 'pause' => \absint( \get_option( 'activitypub_custom_batch_pause', 30 ) ),
+ );
}
/**
* Filter the dispatcher batch size based on distribution mode.
*
+ * Only overrides the value when a non-default mode is active,
+ * so other plugins or constants can still set the batch size.
+ *
* @since unreleased
*
* @param int $batch_size The default batch size.
*
* @return int The batch size for the current distribution mode.
*/
- public static function filter_dispatcher_batch_size( $batch_size ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ public static function filter_dispatcher_batch_size( $batch_size ) {
+ $mode = \get_option( 'activitypub_distribution_mode', 'default' );
+
+ if ( 'default' === $mode ) {
+ return $batch_size;
+ }
+
$params = self::get_distribution_params();
return $params['batch_size'];
}
@@ -808,13 +811,22 @@ public static function filter_dispatcher_batch_size( $batch_size ) { // phpcs:ig
/**
* Filter the scheduler batch pause based on distribution mode.
*
+ * Only overrides the value when a non-default mode is active,
+ * so other plugins or constants can still set the pause.
+ *
* @since unreleased
*
* @param int $pause The default pause in seconds.
*
* @return int The pause for the current distribution mode.
*/
- public static function filter_scheduler_batch_pause( $pause ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ public static function filter_scheduler_batch_pause( $pause ) {
+ $mode = \get_option( 'activitypub_distribution_mode', 'default' );
+
+ if ( 'default' === $mode ) {
+ return $pause;
+ }
+
$params = self::get_distribution_params();
return $params['pause'];
}
diff --git a/tests/phpunit/tests/includes/class-test-options.php b/tests/phpunit/tests/includes/class-test-options.php
index 61279267d2..8c969e7a84 100644
--- a/tests/phpunit/tests/includes/class-test-options.php
+++ b/tests/phpunit/tests/includes/class-test-options.php
@@ -42,6 +42,11 @@ public function tear_down() {
// Clean up quote policy option.
\delete_option( 'activitypub_default_quote_policy' );
+ // Clean up distribution mode options.
+ \delete_option( 'activitypub_distribution_mode' );
+ \delete_option( 'activitypub_custom_batch_size' );
+ \delete_option( 'activitypub_custom_batch_pause' );
+
parent::tear_down();
}
@@ -225,6 +230,86 @@ public function test_default_quote_policy_accepts_valid_values() {
$this->assertEquals( ACTIVITYPUB_INTERACTION_POLICY_ME, \get_option( 'activitypub_default_quote_policy' ) );
}
+ /**
+ * Test distribution mode returns correct params for presets.
+ *
+ * @covers \Activitypub\Options::get_distribution_params
+ */
+ public function test_distribution_params_presets() {
+ \update_option( 'activitypub_distribution_mode', 'eco' );
+
+ $params = Options::get_distribution_params();
+
+ $this->assertEquals( 20, $params['batch_size'] );
+ $this->assertEquals( 300, $params['pause'] );
+ }
+
+ /**
+ * Test distribution mode custom params.
+ *
+ * @covers \Activitypub\Options::get_distribution_params
+ */
+ public function test_distribution_params_custom() {
+ Options::register_settings();
+
+ \update_option( 'activitypub_distribution_mode', 'custom' );
+ \update_option( 'activitypub_custom_batch_size', 42 );
+ \update_option( 'activitypub_custom_batch_pause', 120 );
+
+ $params = Options::get_distribution_params();
+
+ $this->assertEquals( 42, $params['batch_size'] );
+ $this->assertEquals( 120, $params['pause'] );
+ }
+
+ /**
+ * Test custom batch size cannot be zero.
+ *
+ * @covers \Activitypub\Options::register_settings
+ */
+ public function test_custom_batch_size_minimum() {
+ Options::register_settings();
+
+ \update_option( 'activitypub_custom_batch_size', 0 );
+ $this->assertGreaterThanOrEqual( 1, \get_option( 'activitypub_custom_batch_size' ) );
+ }
+
+ /**
+ * Test distribution mode sanitizes invalid values.
+ *
+ * @covers \Activitypub\Options::register_settings
+ */
+ public function test_distribution_mode_sanitizes_invalid() {
+ Options::register_settings();
+
+ \update_option( 'activitypub_distribution_mode', 'turbo' );
+ $this->assertEquals( 'default', \get_option( 'activitypub_distribution_mode' ) );
+ }
+
+ /**
+ * Test default mode does not override filter values.
+ *
+ * @covers \Activitypub\Options::filter_dispatcher_batch_size
+ */
+ public function test_default_mode_preserves_filter_value() {
+ \update_option( 'activitypub_distribution_mode', 'default' );
+
+ $result = Options::filter_dispatcher_batch_size( 42 );
+ $this->assertEquals( 42, $result );
+ }
+
+ /**
+ * Test non-default mode overrides filter values.
+ *
+ * @covers \Activitypub\Options::filter_dispatcher_batch_size
+ */
+ public function test_non_default_mode_overrides_filter_value() {
+ \update_option( 'activitypub_distribution_mode', 'eco' );
+
+ $result = Options::filter_dispatcher_batch_size( 42 );
+ $this->assertEquals( 20, $result );
+ }
+
/**
* Test default quote policy option sanitizes invalid values.
*
From 82eb6e5d64386b6a8d56f5e41a80548da9cc7eec Mon Sep 17 00:00:00 2001
From: Matthias Pfefferle
Date: Wed, 1 Apr 2026 23:17:44 +0200
Subject: [PATCH 6/7] Add upper bounds for custom distribution values and
reject custom constant
- Cap custom batch size at 500 and pause at 3600s to prevent
accidental server overload via misconfiguration.
- Reject 'custom' as a valid ACTIVITYPUB_DISTRIBUTION_MODE constant
value since its parameters are still DB-settable.
- Add tests for upper bound enforcement.
---
includes/class-options.php | 17 +++++++++----
.../tests/includes/class-test-options.php | 24 +++++++++++++++++++
2 files changed, 36 insertions(+), 5 deletions(-)
diff --git a/includes/class-options.php b/includes/class-options.php
index 904dd30a2b..76bfcf1079 100644
--- a/includes/class-options.php
+++ b/includes/class-options.php
@@ -384,7 +384,7 @@ public static function register_settings() {
'description' => 'Custom batch size for federation delivery.',
'default' => 100,
'sanitize_callback' => static function ( $value ) {
- return \max( 1, \absint( $value ) );
+ return \min( 500, \max( 1, \absint( $value ) ) );
},
)
);
@@ -396,7 +396,9 @@ public static function register_settings() {
'type' => 'integer',
'description' => 'Custom pause in seconds between batches.',
'default' => 30,
- 'sanitize_callback' => 'absint',
+ 'sanitize_callback' => static function ( $value ) {
+ return \min( 3600, \absint( $value ) );
+ },
)
);
@@ -713,14 +715,19 @@ public static function default_object_type( $value ) {
*/
public static function pre_option_activitypub_distribution_mode( $pre ) {
if ( false !== ACTIVITYPUB_DISTRIBUTION_MODE ) {
- $allowed = array_keys( self::get_distribution_modes() );
- $allowed[] = 'custom';
+ /*
+ * Only preset modes are allowed via the constant. The 'custom'
+ * mode is excluded because its batch size and pause values are
+ * still read from the database, which defeats the purpose of
+ * locking the mode via wp-config.php.
+ */
+ $allowed = array_keys( self::get_distribution_modes() );
if ( \in_array( ACTIVITYPUB_DISTRIBUTION_MODE, $allowed, true ) ) {
return ACTIVITYPUB_DISTRIBUTION_MODE;
}
- // Invalid constant value, fall back to default.
+ // Invalid or unsupported constant value, fall back to default.
return 'default';
}
diff --git a/tests/phpunit/tests/includes/class-test-options.php b/tests/phpunit/tests/includes/class-test-options.php
index 8c969e7a84..ca7ae7748a 100644
--- a/tests/phpunit/tests/includes/class-test-options.php
+++ b/tests/phpunit/tests/includes/class-test-options.php
@@ -274,6 +274,30 @@ public function test_custom_batch_size_minimum() {
$this->assertGreaterThanOrEqual( 1, \get_option( 'activitypub_custom_batch_size' ) );
}
+ /**
+ * Test custom batch size is capped at 500.
+ *
+ * @covers \Activitypub\Options::register_settings
+ */
+ public function test_custom_batch_size_maximum() {
+ Options::register_settings();
+
+ \update_option( 'activitypub_custom_batch_size', 100000 );
+ $this->assertEquals( 500, \get_option( 'activitypub_custom_batch_size' ) );
+ }
+
+ /**
+ * Test custom batch pause is capped at 3600.
+ *
+ * @covers \Activitypub\Options::register_settings
+ */
+ public function test_custom_batch_pause_maximum() {
+ Options::register_settings();
+
+ \update_option( 'activitypub_custom_batch_pause', 99999 );
+ $this->assertEquals( 3600, \get_option( 'activitypub_custom_batch_pause' ) );
+ }
+
/**
* Test distribution mode sanitizes invalid values.
*
From 7a84430386835ba1093452eaee87bc8f9d4ec4ea Mon Sep 17 00:00:00 2001
From: Matthias Pfefferle
Date: Thu, 2 Apr 2026 08:59:47 +0200
Subject: [PATCH 7/7] Adjust distribution mode pause values to 15s, 30s, 30s
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Default 15s, Balanced 30s, Eco 30s — the differentiation between
modes is now purely batch size (100/50/20). The previous 5min Eco
pause caused unreasonably long delivery times.
---
includes/class-options.php | 12 ++++++------
includes/wp-admin/class-advanced-settings-fields.php | 2 +-
tests/phpunit/tests/includes/class-test-options.php | 2 +-
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/includes/class-options.php b/includes/class-options.php
index 76bfcf1079..dd7b4295c8 100644
--- a/includes/class-options.php
+++ b/includes/class-options.php
@@ -748,21 +748,21 @@ public static function get_distribution_modes() {
return array(
'default' => array(
'batch_size' => 100,
- 'pause' => 30,
+ 'pause' => 15,
'label' => \__( 'Default', 'activitypub' ),
- 'description' => \__( 'Deliver activities as fast as possible (100 per batch, 30s pause).', 'activitypub' ),
+ 'description' => \__( 'Deliver activities as fast as possible (100 per batch, 15s pause).', 'activitypub' ),
),
'balanced' => array(
'batch_size' => 50,
- 'pause' => 60,
+ 'pause' => 30,
'label' => \__( 'Balanced', 'activitypub' ),
- 'description' => \__( 'Moderate pace with reasonable pauses between batches (50 per batch, 60s pause).', 'activitypub' ),
+ 'description' => \__( 'Moderate pace with reasonable pauses between batches (50 per batch, 30s pause).', 'activitypub' ),
),
'eco' => array(
'batch_size' => 20,
- 'pause' => 300,
+ 'pause' => 30,
'label' => \__( 'Eco Mode', 'activitypub' ),
- 'description' => \__( 'Gentle on server resources, ideal for shared hosting (20 per batch, 5min pause).', 'activitypub' ),
+ 'description' => \__( 'Gentle on server resources, ideal for shared hosting (20 per batch, 30s pause).', 'activitypub' ),
),
);
}
diff --git a/includes/wp-admin/class-advanced-settings-fields.php b/includes/wp-admin/class-advanced-settings-fields.php
index fdbf389e92..c42d0aa33d 100644
--- a/includes/wp-admin/class-advanced-settings-fields.php
+++ b/includes/wp-admin/class-advanced-settings-fields.php
@@ -354,7 +354,7 @@ public static function render_distribution_mode_field() {
-
+