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 7cdb42b29..ff4748455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,15 +10,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Support for custom emoji in interaction contents and actor names +* 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 +* Bumped minimum required PHP version to 7.2 ### 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/activitypub.php b/activitypub.php index e394dba11..594dd1463 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 * @@ -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/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/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/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/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/phpcs.xml b/phpcs.xml index 8c155257f..7f4b346e5 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -14,9 +14,9 @@@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 ); 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(