From 292ceb513185533ec32ef4d5291218646e6c8191 Mon Sep 17 00:00:00 2001
From: Matthias Pfefferle
+ +
'integer', + 'description' => 'Send a notification when someone quotes a user of the blog.', + 'default' => 1, + ) + ); + \register_setting( 'activitypub_blog', 'activitypub_blog_user_also_known_as', diff --git a/includes/wp-admin/class-user-settings-fields.php b/includes/wp-admin/class-user-settings-fields.php index a011651e8..d7267185f 100644 --- a/includes/wp-admin/class-user-settings-fields.php +++ b/includes/wp-admin/class-user-settings-fields.php @@ -250,6 +250,12 @@ public static function notifications_callback() { ++ +
+ + + ++ ' . esc_html( $args['actor']['webfinger'] ) . '' ); + ?> +
+ + + + + ++ + + +
+ + + + + + 1, $blog_prefix . 'activitypub_mailer_new_follower' => 1, $blog_prefix . 'activitypub_mailer_new_mention' => 1, + $blog_prefix . 'activitypub_mailer_new_quote' => 1, ), ) ); @@ -743,4 +744,238 @@ public function test_blog_mention_with_disabled_option() { delete_option( 'activitypub_blog_user_mailer_new_mention' ); delete_option( 'activitypub_actor_mode' ); } + + /** + * Test quote notification. + * + * @covers ::quoted + */ + public function test_quoted() { + $activity = array( + 'type' => 'Create', + 'actor' => 'https://example.com/author', + 'object' => array( + 'id' => 'https://example.com/post/1', + 'type' => 'Note', + 'content' => 'Great article! I have some thoughts on this.
', + 'quoteUrl' => get_permalink( self::$post_id ), + ), + ); + + // Mock remote metadata. + add_filter( + 'pre_get_remote_metadata_by_actor', + function () { + return array( + 'name' => 'Test Quoter', + 'url' => 'https://example.com/author', + 'preferredUsername' => 'quoter', + ); + } + ); + + // Capture email. + add_filter( + 'wp_mail', + function ( $args ) { + $this->assertStringContainsString( 'Test Quoter', $args['subject'] ); + $this->assertStringContainsString( 'Quote from', $args['subject'] ); + $this->assertStringContainsString( 'https://example.com/post/1', $args['message'] ); + $this->assertStringContainsString( 'Great article', $args['message'] ); + $this->assertEquals( get_user_by( 'id', self::$user_id )->user_email, $args['to'] ); + return $args; + } + ); + + Mailer::quote( $activity, self::$user_id, true ); + + // Clean up. + remove_all_filters( 'pre_get_remote_metadata_by_actor' ); + remove_all_filters( 'wp_mail' ); + } + + /** + * Test quote notification when state is false. + * + * @covers ::quoted + */ + public function test_quoted_with_false_state() { + $activity = array( + 'type' => 'Create', + 'actor' => 'https://example.com/author', + 'object' => array( + 'id' => 'https://example.com/post/1', + 'type' => 'Note', + 'content' => 'Great article!
', + 'quoteUrl' => get_permalink( self::$post_id ), + ), + ); + + // Add a filter to fail the test if an email is sent. + $mock = new \MockAction(); + add_action( 'wp_before_load_template', array( $mock, 'action' ) ); + + // Call with false state - should not send email. + Mailer::quote( $activity, self::$user_id, false ); + + // Assert no email was sent. + $this->assertEquals( 0, $mock->get_call_count() ); + + // Clean up. + remove_all_filters( 'wp_before_load_template' ); + } + + /** + * Test quote notification when user option is disabled. + * + * @covers ::quoted + */ + public function test_quoted_with_disabled_option() { + // Disable the user option. + update_user_option( self::$user_id, 'activitypub_mailer_new_quote', false ); + + $activity = array( + 'type' => 'Create', + 'actor' => 'https://example.com/author', + 'object' => array( + 'id' => 'https://example.com/post/1', + 'type' => 'Note', + 'content' => 'Great article!
', + 'quoteUrl' => get_permalink( self::$post_id ), + ), + ); + + // Add a filter to fail the test if an email is sent. + $mock = new \MockAction(); + add_action( 'wp_before_load_template', array( $mock, 'action' ) ); + + Mailer::quote( $activity, self::$user_id, true ); + + // Assert no email was sent. + $this->assertEquals( 0, $mock->get_call_count() ); + + // Clean up. + remove_all_filters( 'wp_before_load_template' ); + delete_user_option( self::$user_id, 'activitypub_mailer_new_quote' ); + } + + /** + * Test quote notification for blog user. + * + * @covers ::quoted + */ + public function test_blog_quoted() { + update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + update_option( 'activitypub_blog_user_mailer_new_quote', '1' ); + + $activity = array( + 'type' => 'Create', + 'actor' => 'https://example.com/author', + 'object' => array( + 'id' => 'https://example.com/post/1', + 'type' => 'Note', + 'content' => 'Great blog post!
', + 'quoteUrl' => get_permalink( self::$post_id ), + ), + ); + + // Mock remote metadata. + add_filter( + 'pre_get_remote_metadata_by_actor', + function () { + return array( + 'name' => 'Test Quoter', + 'url' => 'https://example.com/author', + 'preferredUsername' => 'quoter', + ); + } + ); + + // Capture email. + add_filter( + 'wp_mail', + function ( $args ) { + $this->assertStringContainsString( 'Test Quoter', $args['subject'] ); + $this->assertStringContainsString( 'https://example.com/post/1', $args['message'] ); + $this->assertEquals( get_option( 'admin_email' ), $args['to'] ); + return $args; + } + ); + + Mailer::quote( $activity, Actors::BLOG_USER_ID, true ); + + // Clean up. + remove_all_filters( 'pre_get_remote_metadata_by_actor' ); + remove_all_filters( 'wp_mail' ); + delete_option( 'activitypub_blog_user_mailer_new_quote' ); + delete_option( 'activitypub_actor_mode' ); + } + + /** + * Test quote notification for blog user when option is disabled. + * + * @covers ::quoted + */ + public function test_blog_quoted_with_disabled_option() { + // Set blog option to false (0). + update_option( 'activitypub_blog_user_mailer_new_quote', '0' ); + update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + + $activity = array( + 'type' => 'Create', + 'actor' => 'https://example.com/author', + 'object' => array( + 'id' => 'https://example.com/post/1', + 'type' => 'Note', + 'content' => 'Great blog!
', + 'quoteUrl' => get_permalink( self::$post_id ), + ), + ); + + // Add a filter to fail the test if an email is sent. + $mock = new \MockAction(); + add_action( 'wp_before_load_template', array( $mock, 'action' ) ); + + // Call the method with blog user ID. + Mailer::quote( $activity, Actors::BLOG_USER_ID, true ); + + // Assert no email was sent. + $this->assertEquals( 0, $mock->get_call_count() ); + + // Clean up. + remove_all_filters( 'wp_before_load_template' ); + delete_option( 'activitypub_blog_user_mailer_new_quote' ); + delete_option( 'activitypub_actor_mode' ); + } + + /** + * Test quote notification does not send to Application user. + * + * @covers ::quoted + */ + public function test_quoted_application_user() { + $activity = array( + 'type' => 'Create', + 'actor' => 'https://example.com/author', + 'object' => array( + 'id' => 'https://example.com/post/1', + 'type' => 'Note', + 'content' => 'Great article!
', + 'quoteUrl' => get_permalink( self::$post_id ), + ), + ); + + // Add a filter to fail the test if an email is sent. + $mock = new \MockAction(); + add_action( 'wp_before_load_template', array( $mock, 'action' ) ); + + // Call with Application user - should not send email. + Mailer::quote( $activity, Actors::APPLICATION_USER_ID, true ); + + // Assert no email was sent. + $this->assertEquals( 0, $mock->get_call_count() ); + + // Clean up. + remove_all_filters( 'wp_before_load_template' ); + } } diff --git a/tests/phpunit/tests/includes/handler/class-test-quote-request.php b/tests/phpunit/tests/includes/handler/class-test-quote-request.php index 32e8f53bf..3e7dd3ec3 100644 --- a/tests/phpunit/tests/includes/handler/class-test-quote-request.php +++ b/tests/phpunit/tests/includes/handler/class-test-quote-request.php @@ -151,10 +151,11 @@ public function test_handle_quote_request_policies( $policy, $setup_callback, $e 'pre_get_remote_metadata_by_actor', function () use ( $actor_url ) { return array( - 'id' => $actor_url, - 'actor' => $actor_url, - 'type' => 'Person', - 'inbox' => str_replace( '/users/', '/inbox/', $actor_url ), + 'id' => $actor_url, + 'actor' => $actor_url, + 'type' => 'Person', + 'preferredUsername' => 'remote_user', + 'inbox' => str_replace( '/users/', '/inbox/', $actor_url ), ); } ); From e8703fc6419a2ad803b4873c3f53dbfc217942d5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle
' . esc_html( $args['actor']['webfinger'] ) . '' );
?>
From 06fd4b0e86a8d00358f325e007dfc28ae2d4839d Mon Sep 17 00:00:00 2001
From: Automattic Bot
' . esc_html( $args['actor']['webfinger'] ) . '' );
?>
From ccbed26bc658902b061c1e3536d633406fae684f Mon Sep 17 00:00:00 2001
From: Matthias Pfefferle
' . esc_html( $args['actor']['webfinger'] ) . '' );
?>
From 0b8fd5eb792a3bfe12a03cd139a09925c4e120f6 Mon Sep 17 00:00:00 2001
From: Matthias Pfefferle
' . esc_html( $args['actor']['webfinger'] ) . '' );
+ printf(
+ wp_kses( $message, array( 'a' => array( 'href' => array() ) ) ),
+ wp_kses( $comment_author, array( 'a' => array( 'href' => array() ) ) ),
+ esc_url( $args['quoted_url'] ),
+ esc_html( $args['quoted_title'] )
+ );
?>
+
+
-
-
+
-
- -
- - + -- - + + +
+ Date: Tue, 14 Oct 2025 09:51:47 -0500 Subject: [PATCH 16/16] Update quote notification tests to properly mock remote object fetching - Use activitypub_pre_http_get_remote_object filter for mocking - Template expects quote_object to always be an array from mailer - All 1116 tests pass successfully --- .../tests/includes/class-test-mailer.php | 31 +++++++++++++++++-- .../handler/class-test-quote-request.php | 18 +++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/includes/class-test-mailer.php b/tests/phpunit/tests/includes/class-test-mailer.php index 1bdaaf472..3263f5ed0 100644 --- a/tests/phpunit/tests/includes/class-test-mailer.php +++ b/tests/phpunit/tests/includes/class-test-mailer.php @@ -774,13 +774,25 @@ function () { } ); + // Mock HTTP request to fetch quote object. + add_filter( + 'activitypub_pre_http_get_remote_object', + function ( $preempt, $url ) use ( $activity ) { + if ( 'https://example.com/post/1' === $url || $url === $activity['object'] ) { + return $activity['object']; + } + return $preempt; + }, + 10, + 2 + ); + // Capture email. add_filter( 'wp_mail', function ( $args ) { $this->assertStringContainsString( 'Test Quoter', $args['subject'] ); $this->assertStringContainsString( 'Quote from', $args['subject'] ); - $this->assertStringContainsString( 'https://example.com/post/1', $args['message'] ); $this->assertStringContainsString( 'Great article', $args['message'] ); $this->assertEquals( get_user_by( 'id', self::$user_id )->user_email, $args['to'] ); return $args; @@ -791,6 +803,7 @@ function ( $args ) { // Clean up. remove_all_filters( 'pre_get_remote_metadata_by_actor' ); + remove_all_filters( 'activitypub_pre_http_get_remote_object' ); remove_all_filters( 'wp_mail' ); } @@ -891,12 +904,25 @@ function () { } ); + // Mock HTTP request to fetch quote object. + add_filter( + 'activitypub_pre_http_get_remote_object', + function ( $preempt, $url ) use ( $activity ) { + if ( 'https://example.com/post/1' === $url || $url === $activity['object'] ) { + return $activity['object']; + } + return $preempt; + }, + 10, + 2 + ); + // Capture email. add_filter( 'wp_mail', function ( $args ) { $this->assertStringContainsString( 'Test Quoter', $args['subject'] ); - $this->assertStringContainsString( 'https://example.com/post/1', $args['message'] ); + $this->assertStringContainsString( 'Great blog post!', $args['message'] ); $this->assertEquals( get_option( 'admin_email' ), $args['to'] ); return $args; } @@ -906,6 +932,7 @@ function ( $args ) { // Clean up. remove_all_filters( 'pre_get_remote_metadata_by_actor' ); + remove_all_filters( 'activitypub_pre_http_get_remote_object' ); remove_all_filters( 'wp_mail' ); delete_option( 'activitypub_blog_user_mailer_new_quote' ); delete_option( 'activitypub_actor_mode' ); diff --git a/tests/phpunit/tests/includes/handler/class-test-quote-request.php b/tests/phpunit/tests/includes/handler/class-test-quote-request.php index 3e7dd3ec3..c9454f58a 100644 --- a/tests/phpunit/tests/includes/handler/class-test-quote-request.php +++ b/tests/phpunit/tests/includes/handler/class-test-quote-request.php @@ -160,6 +160,23 @@ function () use ( $actor_url ) { } ); + // Mock HTTP request to fetch quote object (instrument). + add_filter( + 'activitypub_pre_http_get_remote_object', + function ( $preempt, $url ) use ( $activity ) { + if ( $activity['instrument'] === $url ) { + return array( + 'id' => $activity['instrument'], + 'type' => 'Note', + 'content' => 'Great post! I have some thoughts.
', + ); + } + return $preempt; + }, + 10, + 2 + ); + $remote_actor_id = false; // Run setup callback if provided. @@ -473,6 +490,7 @@ public function test_init_registers_hooks() { public function tear_down() { // Remove all the filters we added during tests. remove_all_filters( 'pre_get_remote_metadata_by_actor' ); + remove_all_filters( 'activitypub_pre_http_get_remote_object' ); parent::tear_down(); }