diff --git a/.github/changelog/fix-outbox-add-infinite-recursion b/.github/changelog/fix-outbox-add-infinite-recursion new file mode 100644 index 0000000000..029925ad68 --- /dev/null +++ b/.github/changelog/fix-outbox-add-infinite-recursion @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Fix an infinite loop when saving activities to the outbox on sites where the outbox post type passes through content filters. diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index ba53cbf810..5945223379 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -126,6 +126,13 @@ public static function add( Activity $activity, $user_id, $visibility = ACTIVITY \kses_remove_filters(); } + // Prevent infinite recursion: wp_insert_post fires wp_after_insert_post, + // which would re-enter Post::triage() -> add_to_outbox() -> Outbox::add(). + $has_triage = false !== \has_action( 'wp_after_insert_post', array( Scheduler\Post::class, 'triage' ) ); + if ( $has_triage ) { + \remove_action( 'wp_after_insert_post', array( Scheduler\Post::class, 'triage' ), 33 ); + } + $id = \wp_insert_post( $outbox_item, true ); // Update the activity ID if the post was inserted successfully. @@ -140,6 +147,10 @@ public static function add( Activity $activity, $user_id, $visibility = ACTIVITY ); } + if ( $has_triage ) { + \add_action( 'wp_after_insert_post', array( Scheduler\Post::class, 'triage' ), 33, 4 ); + } + if ( $has_kses ) { \kses_init_filters(); } diff --git a/tests/phpunit/tests/includes/collection/class-test-outbox.php b/tests/phpunit/tests/includes/collection/class-test-outbox.php index 4dae6238ae..9005e6d95f 100644 --- a/tests/phpunit/tests/includes/collection/class-test-outbox.php +++ b/tests/phpunit/tests/includes/collection/class-test-outbox.php @@ -429,6 +429,70 @@ public function undo_object_provider() { ); } + /** + * Test that Outbox::add() does not cause infinite recursion via Post::triage(). + * + * When wp_insert_post() fires inside Outbox::add(), it triggers wp_after_insert_post, + * which could re-enter Post::triage() → add_to_outbox() → Outbox::add() in an infinite + * loop if the triage hook is not temporarily removed. + * + * @covers ::add + */ + public function test_add_does_not_recurse_via_post_triage() { + // Ensure the triage hook is registered as a baseline. + \add_action( 'wp_after_insert_post', array( \Activitypub\Scheduler\Post::class, 'triage' ), 33, 4 ); + + $triage_hooked_during_insert = null; + + // Check whether Post::triage is hooked when wp_after_insert_post fires + // during the outbox insert. + \add_action( + 'wp_after_insert_post', + function ( $post_id ) use ( &$triage_hooked_during_insert ) { + if ( Outbox::POST_TYPE === \get_post_type( $post_id ) ) { + $triage_hooked_during_insert = \has_action( + 'wp_after_insert_post', + array( \Activitypub\Scheduler\Post::class, 'triage' ) + ); + } + }, + 0 // Run before priority 33 to inspect hook state. + ); + + $object = new Base_Object(); + $object->set_id( 'https://example.com/recursion-test' ); + $object->set_type( 'Note' ); + $object->set_content( '
Recursion test
' ); + + $id = \Activitypub\add_to_outbox( $object, 'Create', self::$user_id ); + + $this->assertIsInt( $id ); + $this->assertFalse( $triage_hooked_during_insert, 'Post::triage should be unhooked during Outbox::add() to prevent recursion.' ); + } + + /** + * Test that Outbox::add() restores the Post::triage hook after inserting. + * + * @covers ::add + */ + public function test_add_restores_triage_hook() { + // Ensure the triage hook is registered as a baseline. + \add_action( 'wp_after_insert_post', array( \Activitypub\Scheduler\Post::class, 'triage' ), 33, 4 ); + + $object = new Base_Object(); + $object->set_id( 'https://example.com/restore-hook-test' ); + $object->set_type( 'Note' ); + $object->set_content( 'Hook restore test
' ); + + \Activitypub\add_to_outbox( $object, 'Create', self::$user_id ); + + // After add_to_outbox completes, the triage hook should be restored. + $this->assertNotFalse( + \has_action( 'wp_after_insert_post', array( \Activitypub\Scheduler\Post::class, 'triage' ) ), + 'Post::triage hook should be restored after Outbox::add() completes.' + ); + } + /** * Helper method to create a dummy activity object for testing. *