From 8c3e0522f1f2ec4f59dd1bb9cc151a204f1a2f6d Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Fri, 10 Jan 2025 06:22:58 -0600 Subject: [PATCH 1/4] Bump minimum version to PHP 7.2 (#1139) * Bump minimum version to PHP 7.2+ * Upgrade phpunit * Fix tests for WP 5.9 --------- Co-authored-by: Matthias Pfefferle --- .github/workflows/phpunit.yml | 8 +++----- CHANGELOG.md | 1 + activitypub.php | 2 +- composer.json | 4 ++-- phpcs.xml | 4 ++-- phpunit.xml.dist | 2 -- readme.txt | 3 ++- tests/includes/collection/class-test-followers.php | 6 +++--- .../rest/class-test-signature-verification.php | 2 +- tests/includes/transformer/class-test-post.php | 10 ++++++++++ 10 files changed, 25 insertions(+), 17 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 789b202bc..f6c63fb9b 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -15,13 +15,11 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=10s --health-retries=10 strategy: matrix: - php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] + php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] include: - wp-version: latest - - wp-version: '6.5' - php-versions: '7.0' - - wp-version: '6.5' - php-versions: '7.1' + - wp-version: '5.9' + php-versions: '7.2' steps: - name: Install svn run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 6097d2748..ba6f9bb52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed * Hide ActivityPub post meta keys from the custom Fields UI +* Bumped minimum required PHP version to 7.2 ### Fixed diff --git a/activitypub.php b/activitypub.php index e394dba11..f59ede795 100644 --- a/activitypub.php +++ b/activitypub.php @@ -8,7 +8,7 @@ * Author URI: https://automattic.com/ * License: MIT * License URI: http://opensource.org/licenses/MIT - * Requires PHP: 7.0 + * Requires PHP: 7.2 * Text Domain: activitypub * Domain Path: /languages * diff --git a/composer.json b/composer.json index 8aaefd3d7..f55a1d517 100644 --- a/composer.json +++ b/composer.json @@ -3,11 +3,11 @@ "description": "The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format.", "type": "wordpress-plugin", "require": { - "php": ">=7.0", + "php": ">=7.2", "composer/installers": "^1.0 || ^2.0" }, "require-dev": { - "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5 || ^8", + "phpunit/phpunit": "^8 || ^9", "phpcompatibility/php-compatibility": "*", "phpcompatibility/phpcompatibility-wp": "*", "squizlabs/php_codesniffer": "3.*", diff --git a/phpcs.xml b/phpcs.xml index 8c155257f..7f4b346e5 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -14,9 +14,9 @@ - + - + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 714f685ca..7b2d25680 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -11,9 +11,7 @@ ./tests - diff --git a/readme.txt b/readme.txt index 88bd189a3..bdec5591f 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: OStatus, fediverse, activitypub, activitystream Requires at least: 5.5 Tested up to: 6.7 Stable tag: 4.6.0 -Requires PHP: 7.0 +Requires PHP: 7.2 License: MIT License URI: http://opensource.org/licenses/MIT @@ -136,6 +136,7 @@ For reasons of data protection, it is not possible to see the followers of other * Added: A filter to make custom comment types manageable in WP.com Calypso * Changed: Hide ActivityPub post meta keys from the custom Fields UI +* Changed: Bumped minimum required PHP version to 7.2 * Fixed: Undefined array key warnings in various places * Fixed: Fetching replies from the same instance for Enable Mastodon Apps * Fixed: Image captions not being included in the ActivityPub representation when the image is attached to the post diff --git a/tests/includes/collection/class-test-followers.php b/tests/includes/collection/class-test-followers.php index cd54412ed..4eab7196b 100644 --- a/tests/includes/collection/class-test-followers.php +++ b/tests/includes/collection/class-test-followers.php @@ -129,8 +129,8 @@ public function test_add_follower() { $db_followers = Followers::get_followers( 1 ); $db_followers2 = Followers::get_followers( 2 ); - $this->assertContains( $follower, $db_followers ); - $this->assertContains( $follower2, $db_followers2 ); + $this->assertStringContainsString( $follower, serialize( $db_followers ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + $this->assertStringContainsString( $follower2, serialize( $db_followers2 ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize } /** @@ -329,7 +329,7 @@ public function test_add_duplicate_follower() { $db_followers = Followers::get_followers( 1 ); - $this->assertContains( $follower, $db_followers ); + $this->assertStringContainsString( $follower, serialize( $db_followers ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize $follower = current( $db_followers ); $meta = get_post_meta( $follower->get__id(), '_activitypub_user_id', false ); diff --git a/tests/includes/rest/class-test-signature-verification.php b/tests/includes/rest/class-test-signature-verification.php index cb86dc07e..db07bfcbc 100644 --- a/tests/includes/rest/class-test-signature-verification.php +++ b/tests/includes/rest/class-test-signature-verification.php @@ -47,7 +47,7 @@ public function test_activity_signature() { $date = gmdate( 'D, d M Y H:i:s T' ); $signature = Signature::generate_signature( 1, 'POST', $remote_actor, $date, $digest ); - $this->assertRegExp( '/keyId="http:\/\/example\.org\/\?author=1#main-key",algorithm="rsa-sha256",headers="\(request-target\) host date digest",signature="[^"]*"/', $signature ); + $this->assertMatchesRegularExpression( '/keyId="http:\/\/example\.org\/\?author=1#main-key",algorithm="rsa-sha256",headers="\(request-target\) host date digest",signature="[^"]*"/', $signature ); // Signed headers. $url_parts = wp_parse_url( $remote_actor ); diff --git a/tests/includes/transformer/class-test-post.php b/tests/includes/transformer/class-test-post.php index a6a07202c..803d86f5c 100644 --- a/tests/includes/transformer/class-test-post.php +++ b/tests/includes/transformer/class-test-post.php @@ -370,6 +370,16 @@ public function test_block_attachments_with_fallback() { ) ); + // For WP versions 6.1 and prior, we only look for attached images. + if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { + wp_update_post( + array( + 'ID' => $attachment_id, + 'post_parent' => $post_id, + ) + ); + } + $object = Post::transform( get_post( $post_id ) )->to_object(); $this->assertEquals( From 370d8c88f2c9c294b65b16ef4d54f410b9359b0e Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Fri, 10 Jan 2025 06:31:10 -0600 Subject: [PATCH 2/4] Comments: Show @-mentions in line with reply (#1137) * Comments: Show @-mentions in line with reply * Changelog * simplify code even more --------- Co-authored-by: Matthias Pfefferle --- CHANGELOG.md | 1 + includes/transformer/class-comment.php | 67 +++++++------------ readme.txt | 1 + .../transformer/class-test-comment.php | 2 +- 4 files changed, 28 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba6f9bb52..10c67ff83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed * Undefined array key warnings in various places +* @-mentions in federated comments being displayed with a line break * Fetching replies from the same instance for Enable Mastodon Apps * Image captions not being included in the ActivityPub representation when the image is attached to the post diff --git a/includes/transformer/class-comment.php b/includes/transformer/class-comment.php index f689b8cf2..9b1ec64ff 100644 --- a/includes/transformer/class-comment.php +++ b/includes/transformer/class-comment.php @@ -45,44 +45,6 @@ public function change_wp_user_id( $user_id ) { $this->wp_object->user_id = $user_id; } - /** - * Transforms the WP_Comment object to an ActivityPub Object. - * - * @see \Activitypub\Activity\Base_Object - * - * @return \Activitypub\Activity\Base_Object The ActivityPub Object. - */ - public function to_object() { - $object = parent::to_object(); - - $content = $this->get_content(); - $at_replies = ''; - $reply_context = $this->extract_reply_context( array() ); - - foreach ( $reply_context as $acct => $url ) { - $at_replies .= sprintf( - '%s ', - esc_url( $url ), - esc_html( $acct ) - ); - } - - $at_replies = trim( $at_replies ); - - if ( $at_replies ) { - $content = sprintf( '

%s

%s', $at_replies, $content ); - } - - $object->set_content( $content ); - $object->set_content_map( - array( - $this->get_locale() => $content, - ) - ); - - return $object; - } - /** * Returns the User-URL of the Author of the Post. * @@ -107,8 +69,18 @@ protected function get_attributed_to() { * @return string The content. */ protected function get_content() { - $comment = $this->wp_object; - $content = $comment->comment_content; + $comment = $this->wp_object; + $content = $comment->comment_content; + $mentions = ''; + + foreach ( $this->extract_reply_context() as $acct => $url ) { + $mentions .= sprintf( + '%s ', + esc_url( $url ), + esc_html( $acct ) + ); + } + $content = $mentions . $content; /** * Filter the content of the comment. @@ -258,11 +230,11 @@ function ( $comment_id ) { * Collect all other Users that participated in this comment-thread * to send them a notification about the new reply. * - * @param array $mentions The already mentioned ActivityPub users. + * @param array $mentions Optional. The already mentioned ActivityPub users. Default empty array. * * @return array The list of all Repliers. */ - public function extract_reply_context( $mentions ) { + public function extract_reply_context( $mentions = array() ) { // Check if `$this->wp_object` is a WP_Comment. if ( 'WP_Comment' !== get_class( $this->wp_object ) ) { return $mentions; @@ -364,4 +336,15 @@ public function get_to() { get_rest_url_by_path( $path ), ); } + + /** + * Returns the content map for the comment. + * + * @return array The content map for the comment. + */ + public function get_content_map() { + return array( + $this->get_locale() => $this->get_content(), + ); + } } diff --git a/readme.txt b/readme.txt index bdec5591f..e47085bc2 100644 --- a/readme.txt +++ b/readme.txt @@ -138,6 +138,7 @@ For reasons of data protection, it is not possible to see the followers of other * Changed: Hide ActivityPub post meta keys from the custom Fields UI * Changed: Bumped minimum required PHP version to 7.2 * Fixed: Undefined array key warnings in various places +* Fixed: @-mentions in federated comments being displayed with a line break * Fixed: Fetching replies from the same instance for Enable Mastodon Apps * Fixed: Image captions not being included in the ActivityPub representation when the image is attached to the post diff --git a/tests/includes/transformer/class-test-comment.php b/tests/includes/transformer/class-test-comment.php index a4a3efd73..f7dd29559 100644 --- a/tests/includes/transformer/class-test-comment.php +++ b/tests/includes/transformer/class-test-comment.php @@ -89,7 +89,7 @@ public function test_content_with_reply_context() { $content = $object->get_content(); // Test that reply context is added. - $this->assertEquals( '

@remote@example.net @author@remote.example

This is a comment

', $content ); + $this->assertSame( '

@remote@example.net @author@remote.example This is a comment

', $content ); // Clean up. wp_delete_comment( $reply_comment_id, true ); From bca6fd7649e827f011b4e6befe70d08ab797591c Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Fri, 10 Jan 2025 06:40:04 -0600 Subject: [PATCH 3/4] Comments: Update counts when (de)activating the plugin (#1128) --- CHANGELOG.md | 1 + includes/class-activitypub.php | 9 +++++++++ readme.txt | 1 + 3 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10c67ff83..40f85087e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Comment counts get updated when the plugin is activated/deactivated/deleted * Added a filter to make custom comment types manageable in WP.com Calypso ### Changed diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index bf94e303c..b63c1ae33 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -62,6 +62,9 @@ public static function init() { public static function activate() { self::flush_rewrite_rules(); Scheduler::register_schedules(); + + \add_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ), 10, 3 ); + Migration::update_comment_counts(); } /** @@ -70,6 +73,9 @@ public static function activate() { public static function deactivate() { self::flush_rewrite_rules(); Scheduler::deregister_schedules(); + + \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) ); + Migration::update_comment_counts( 2000 ); } /** @@ -77,6 +83,9 @@ public static function deactivate() { */ public static function uninstall() { Scheduler::deregister_schedules(); + + \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) ); + Migration::update_comment_counts( 2000 ); } /** diff --git a/readme.txt b/readme.txt index e47085bc2..f16c10315 100644 --- a/readme.txt +++ b/readme.txt @@ -134,6 +134,7 @@ For reasons of data protection, it is not possible to see the followers of other = Unreleased = +* Added: Comment counts get updated when the plugin is activated/deactivated/deleted * Added: A filter to make custom comment types manageable in WP.com Calypso * Changed: Hide ActivityPub post meta keys from the custom Fields UI * Changed: Bumped minimum required PHP version to 7.2 From dfc23ada7dce22c860cb9d7665858f95a29ded84 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Fri, 10 Jan 2025 13:09:08 -0600 Subject: [PATCH 4/4] Webfinger: Use Rest Controller structure (#1146) * Rename file to include controller * Update webfinger endpoint * Remove blank line * Fix tests --- activitypub.php | 2 +- includes/rest/class-webfinger-controller.php | 173 +++++++++++++++ includes/rest/class-webfinger.php | 116 ----------- tests/bootstrap.php | 1 + tests/class-test-rest-controller-testcase.php | 80 +++++++ .../rest/class-test-webfinger-controller.php | 197 ++++++++++++++++++ 6 files changed, 452 insertions(+), 117 deletions(-) create mode 100644 includes/rest/class-webfinger-controller.php delete mode 100644 includes/rest/class-webfinger.php create mode 100644 tests/class-test-rest-controller-testcase.php create mode 100644 tests/includes/rest/class-test-webfinger-controller.php diff --git a/activitypub.php b/activitypub.php index f59ede795..594dd1463 100644 --- a/activitypub.php +++ b/activitypub.php @@ -44,12 +44,12 @@ function rest_init() { Rest\Inbox::init(); Rest\Followers::init(); Rest\Following::init(); - Rest\Webfinger::init(); Rest\Comment::init(); Rest\Server::init(); Rest\Collection::init(); Rest\Interaction::init(); Rest\Post::init(); + ( new Rest\Webfinger_Controller() )->register_routes(); // Load NodeInfo endpoints only if blog is public. if ( is_blog_public() ) { diff --git a/includes/rest/class-webfinger-controller.php b/includes/rest/class-webfinger-controller.php new file mode 100644 index 000000000..ee75e7add --- /dev/null +++ b/includes/rest/class-webfinger-controller.php @@ -0,0 +1,173 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'resource' => array( + 'description' => 'The WebFinger resource.', + 'type' => 'string', + 'required' => true, + 'pattern' => '^(acct:)|^(https?://)(.+)$', + ), + ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Retrieves the WebFinger profile. + * + * @param \WP_REST_Request $request The request object. + * + * @return \WP_REST_Response Response object. + */ + public function get_item( $request ) { + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client. + */ + \do_action( 'activitypub_rest_webfinger_pre' ); + + $resource = $request->get_param( 'resource' ); + $response = $this->get_profile( $resource ); + $code = 200; + + if ( \is_wp_error( $response ) ) { + $code = 400; + $error_data = $response->get_error_data(); + + if ( isset( $error_data['status'] ) ) { + $code = $error_data['status']; + } + } + + return new \WP_REST_Response( + $response, + $code, + array( + 'Access-Control-Allow-Origin' => '*', + 'Content-Type' => 'application/jrd+json; charset=' . \get_option( 'blog_charset' ), + ) + ); + } + + /** + * Get the WebFinger profile. + * + * @param string $webfinger The WebFinger resource. + * + * @return array|\WP_Error The WebFinger profile or WP_Error if not found. + */ + public function get_profile( $webfinger ) { + /** + * Filter the WebFinger data. + * + * @param array $data The WebFinger data. + * @param string $webfinger The WebFinger resource. + */ + return \apply_filters( 'webfinger_data', array(), $webfinger ); + } + + /** + * Retrieves the schema for the WebFinger endpoint. + * + * @return array Schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $this->schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'webfinger', + 'type' => 'object', + 'required' => array( 'subject', 'links' ), + 'properties' => array( + 'subject' => array( + 'description' => 'The subject of this WebFinger record.', + 'type' => 'string', + 'format' => 'uri', + ), + 'aliases' => array( + 'description' => 'Alternative identifiers for the subject.', + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'format' => 'uri', + ), + ), + 'links' => array( + 'description' => 'Links associated with the subject.', + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'rel' => array( + 'description' => 'The relation type of the link.', + 'type' => 'string', + 'required' => true, + ), + 'type' => array( + 'description' => 'The content type of the link.', + 'type' => 'string', + ), + 'href' => array( + 'description' => 'The target URL of the link.', + 'type' => 'string', + 'format' => 'uri', + ), + 'template' => array( + 'description' => 'A URI template for the link.', + 'type' => 'string', + 'format' => 'uri', + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php deleted file mode 100644 index 3348eae8c..000000000 --- a/includes/rest/class-webfinger.php +++ /dev/null @@ -1,116 +0,0 @@ - \WP_REST_Server::READABLE, - 'callback' => array( self::class, 'webfinger' ), - 'args' => self::request_parameters(), - 'permission_callback' => '__return_true', - ), - ) - ); - } - - /** - * WebFinger endpoint. - * - * @param \WP_REST_Request $request The request object. - * - * @return WP_REST_Response The response object. - */ - public static function webfinger( $request ) { - /** - * Action triggered prior to the ActivityPub profile being created and sent to the client. - */ - \do_action( 'activitypub_rest_webfinger_pre' ); - - $code = 200; - - $resource = $request->get_param( 'resource' ); - $response = self::get_profile( $resource ); - - if ( \is_wp_error( $response ) ) { - $code = 400; - $error_data = $response->get_error_data(); - - if ( isset( $error_data['status'] ) ) { - $code = $error_data['status']; - } - } - - return new WP_REST_Response( - $response, - $code, - array( - 'Access-Control-Allow-Origin' => '*', - 'Content-Type' => 'application/jrd+json; charset=' . get_option( 'blog_charset' ), - ) - ); - } - - /** - * The supported parameters. - * - * @return array list of parameters - */ - public static function request_parameters() { - $params = array(); - - $params['resource'] = array( - 'required' => true, - 'type' => 'string', - 'pattern' => '^(acct:)|^(https?://)(.+)$', - 'sanitize_callback' => 'sanitize_text_field', - ); - - return $params; - } - - /** - * Get the WebFinger profile. - * - * @param string $webfinger the WebFinger resource. - * - * @return array|\WP_Error The WebFinger profile or WP_Error if not found. - */ - public static function get_profile( $webfinger ) { - /** - * Filter the WebFinger data. - * - * @param array $data The WebFinger data. - * @param string $webfinger The WebFinger resource. - */ - return apply_filters( 'webfinger_data', array(), $webfinger ); - } -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 719d49dba..ec1919333 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -99,5 +99,6 @@ function http_disable_request( $response, $args, $url ) { // Start up the WP testing environment. require $_tests_dir . '/includes/bootstrap.php'; require __DIR__ . '/class-activitypub-testcase-cache-http.php'; +require __DIR__ . '/class-test-rest-controller-testcase.php'; \Activitypub\Migration::add_default_settings(); diff --git a/tests/class-test-rest-controller-testcase.php b/tests/class-test-rest-controller-testcase.php new file mode 100644 index 000000000..3f1e6a58c --- /dev/null +++ b/tests/class-test-rest-controller-testcase.php @@ -0,0 +1,80 @@ +fail( + sprintf( + 'REST API URL "%s" should have a leading slash.', + $path + ) + ); + } + + return $url; + } +} diff --git a/tests/includes/rest/class-test-webfinger-controller.php b/tests/includes/rest/class-test-webfinger-controller.php new file mode 100644 index 000000000..c431cd32b --- /dev/null +++ b/tests/includes/rest/class-test-webfinger-controller.php @@ -0,0 +1,197 @@ +user->create_and_get( + array( + 'user_login' => 'test_user', + 'user_email' => 'user@example.org', + ) + ); + self::$user->add_cap( 'activitypub' ); + } + + /** + * Clean up test fixtures. + */ + public static function wpTearDownAfterClass() { + self::delete_user( self::$user->ID ); + } + + /** + * Create test environment. + */ + public function set_up() { + parent::set_up(); + + \add_filter( 'webfinger_data', array( '\Activitypub\Integration\Webfinger', 'add_pseudo_user_discovery' ), 1, 2 ); + } + + /** + * Test route registration. + * + * @covers ::register_routes + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( '/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger', $routes ); + } + + /** + * Test schema. + * + * @covers ::get_item_schema + */ + public function test_get_item_schema() { + $request = new \WP_REST_Request( 'OPTIONS', '/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger' ); + $response = rest_get_server()->dispatch( $request )->get_data(); + + $this->assertArrayHasKey( 'schema', $response ); + $schema = $response['schema']; + + $this->assertIsArray( $schema ); + $this->assertArrayHasKey( 'properties', $schema ); + $this->assertArrayHasKey( 'subject', $schema['properties'] ); + $this->assertArrayHasKey( 'aliases', $schema['properties'] ); + $this->assertArrayHasKey( 'links', $schema['properties'] ); + } + + /** + * Test get_item with valid resource. + * + * @covers ::get_item + */ + public function test_get_item() { + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger' ); + $request->set_param( 'resource', 'acct:test_user@example.org' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertStringContainsString( 'application/jrd+json', $response->get_headers()['Content-Type'] ); + $this->assertEquals( '*', $response->get_headers()['Access-Control-Allow-Origin'] ); + } + + /** + * Test get_item with invalid resource. + * + * @covers ::get_item + */ + public function test_get_item_with_invalid_resource() { + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger' ); + $request->set_param( 'resource', 'invalid-resource' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test get_item with missing resource. + * + * @covers ::get_item + */ + public function test_get_item_with_missing_resource() { + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test webfinger_data filter. + * + * @covers ::get_profile + */ + public function test_webfinger_data_filter() { + $test_data = array( + 'subject' => 'acct:test_user@example.org', + 'aliases' => array( 'https://example.org/@test_user' ), + 'links' => array( + array( + 'rel' => 'self', + 'type' => 'application/activity+json', + 'href' => 'https://example.org/author/test_user', + ), + ), + ); + + \add_filter( + 'webfinger_data', + function ( $data, $webfinger ) use ( $test_data ) { + $this->assertEquals( 'acct:test_user@example.org', $webfinger ); + return $test_data; + }, + 10, + 2 + ); + + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger' ); + $request->set_param( 'resource', 'acct:test_user@example.org' ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( $test_data, $data ); + } + + /** + * Test get_item with author URL resource. + * + * @covers ::get_item + */ + public function test_get_item_with_author_url() { + $author_url = \get_author_posts_url( self::$user->ID ); + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger' ); + $request->set_param( 'resource', $author_url ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertStringContainsString( 'application/jrd+json', $response->get_headers()['Content-Type'] ); + $this->assertContains( $author_url, $data['aliases'] ); + $this->assertArrayHasKey( 'links', $data ); + } + + /** + * Test that the Webfinger response matches its schema. + * + * @covers ::get_item + * @covers ::get_item_schema + */ + public function test_response_matches_schema() { + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger' ); + $request->set_param( 'resource', 'acct:test_user@example.org' ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $schema = ( new \Activitypub\Rest\Webfinger_Controller() )->get_item_schema(); + + $valid = \rest_validate_value_from_schema( $data, $schema ); + $this->assertNotWPError( $valid, 'Response failed schema validation: ' . ( \is_wp_error( $valid ) ? $valid->get_error_message() : '' ) ); + } +}