diff --git a/includes/collection/class-interactions.php b/includes/collection/class-interactions.php new file mode 100644 index 0000000..42fdcfe --- /dev/null +++ b/includes/collection/class-interactions.php @@ -0,0 +1,133 @@ + 400 ) + ); + } + + if ( ! isset( $activity['object']['inReplyTo'] ) ) { + return new WP_Error( + 'activitypub_no_reply', + __( 'Object is no reply.', 'activitypub' ), + array( 'status' => 400 ) + ); + } + + $in_reply_to = \esc_url_raw( $activity['object']['inReplyTo'] ); + $comment_post_id = \url_to_postid( $in_reply_to ); + $parent_comment = object_id_to_comment( $in_reply_to ); + + // save only replys and reactions + if ( ! $comment_post_id && $parent_comment ) { + $comment_post_id = $parent_comment->comment_post_ID; + } + + // not a reply to a post or comment + if ( ! $comment_post_id ) { + return new WP_Error( + 'activitypub_no_reply', + __( 'Object is no reply.', 'activitypub' ), + array( 'status' => 400 ) + ); + } + + $meta = get_remote_metadata_by_actor( $activity['actor'] ); + + if ( ! $meta || \is_wp_error( $meta ) ) { + return new WP_Error( + 'activitypub_invalid_follower', + __( 'Invalid Follower', 'activitypub' ), + array( 'status' => 400 ) + ); + } + + $commentdata = array( + 'comment_post_ID' => $comment_post_id, + 'comment_author' => \esc_attr( $meta['name'] ), + 'comment_author_url' => \esc_url_raw( $meta['url'] ), + 'comment_content' => addslashes( \wp_kses( $activity['object']['content'], 'pre_comment_content' ) ), + 'comment_type' => 'comment', + 'comment_author_email' => '', + 'comment_parent' => $parent_comment ? $parent_comment->comment_ID : 0, + 'comment_meta' => array( + 'source_id' => \esc_url_raw( $activity['object']['id'] ), + 'source_url' => \esc_url_raw( $activity['object']['url'] ), + 'avatar_url' => \esc_url_raw( $meta['icon']['url'] ), + 'protocol' => 'activitypub', + ), + ); + + // disable flood control + \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 ); + + // do not require email for AP entries + \add_filter( 'pre_option_require_name_email', '__return_false' ); + // No nonce possible for this submission route + \add_filter( + 'akismet_comment_nonce', + function() { + return 'inactive'; + } + ); + \add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 ); + + $comment = \wp_new_comment( $commentdata, true ); + + \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 ); + \remove_filter( 'pre_option_require_name_email', '__return_false' ); + + // re-add flood control + \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); + + return $comment; + } + + /** + * Adds line breaks to the list of allowed comment tags. + * + * @param array $allowedtags Allowed HTML tags. + * @param string $context Context. + * @return array Filtered tag list. + */ + public static function allowed_comment_html( $allowedtags, $context = '' ) { + if ( 'pre_comment_content' !== $context ) { + // Do nothing. + return $allowedtags; + } + + // Add `p` and `br` to the list of allowed tags. + if ( ! array_key_exists( 'br', $allowedtags ) ) { + $allowedtags['br'] = array(); + } + + if ( ! array_key_exists( 'p', $allowedtags ) ) { + $allowedtags['p'] = array(); + } + + return $allowedtags; + } +} diff --git a/includes/handler/class-create.php b/includes/handler/class-create.php index f2d2929..40fffcd 100644 --- a/includes/handler/class-create.php +++ b/includes/handler/class-create.php @@ -1,8 +1,11 @@ 400 ) + ); } // check if Activity is public or not - if ( ! is_activity_public( $activity ) ) { + if ( ! is_activity_public( $array ) ) { // @todo maybe send email - return; + return new WP_Error( + 'activitypub_activity_not_public', + __( 'Activity is not public.', 'activitypub' ), + array( 'status' => 400 ) + ); } - $comment_post_id = \url_to_postid( $activity['object']['inReplyTo'] ); + $check_dupe = object_id_to_comment( $array['object']['id'] ); - // save only replys and reactions - if ( ! $comment_post_id ) { - return false; + // if comment exists, call update action + if ( $check_dupe ) { + \do_action( 'activitypub_inbox_update', $array, $user_id, $object ); + return new WP_Error( + 'activitypub_comment_exists', + __( 'Comment already exists, initiated Update process.', 'activitypub' ), + array( 'status' => 400 ) + ); } - $commentdata = array( - 'comment_post_ID' => $comment_post_id, - 'comment_author' => \esc_attr( $meta['name'] ), - 'comment_author_url' => \esc_url_raw( $activity['actor'] ), - 'comment_content' => \wp_filter_kses( $activity['object']['content'] ), - 'comment_type' => 'comment', - 'comment_author_email' => '', - 'comment_parent' => 0, - 'comment_meta' => array( - 'source_id' => \esc_url_raw( $activity['object']['id'] ), - 'source_url' => \esc_url_raw( $activity['object']['url'] ), - 'avatar_url' => \esc_url_raw( $meta['icon']['url'] ), - 'protocol' => 'activitypub', - ), - ); + $reaction = Interactions::add_comment( $array ); + $state = null; - // disable flood control - \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 ); - - // do not require email for AP entries - \add_filter( 'pre_option_require_name_email', '__return_false' ); - - // No nonce possible for this submission route - \add_filter( - 'akismet_comment_nonce', - function() { - return 'inactive'; - } - ); - - $state = \wp_new_comment( $commentdata, true ); - - \remove_filter( 'pre_option_require_name_email', '__return_false' ); - - // re-add flood control - \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); - - \do_action( 'activitypub_handled_create', $activity, $user_id, $state, $commentdata ); - } - - /** - * Handles "Create" requests - * - * @param array $object The activity-object - * @param int $user_id The id of the local blog-user - */ - public static function handle_create_alt( $object, $user_id ) { - $commentdata = self::convert_object_to_comment_data( $object, $user_id ); - if ( ! $commentdata ) { - return false; + if ( $reaction ) { + $state = $reaction['comment_ID']; } - // disable flood control - \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 ); - - // do not require email for AP entries - \add_filter( 'pre_option_require_name_email', '__return_false' ); - - // No nonce possible for this submission route - \add_filter( - 'akismet_comment_nonce', - function() { - return 'inactive'; - } - ); - - $state = \wp_new_comment( $commentdata, true ); - - \remove_filter( 'pre_option_require_name_email', '__return_false' ); - - // re-add flood control - \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); - - do_action( 'activitypub_handled_create', $object, $user_id, $state, $commentdata ); + \do_action( 'activitypub_handled_create', $array, $user_id, $state, $reaction ); } } diff --git a/includes/handler/class-delete.php b/includes/handler/class-delete.php index a33ffdd..893ccf9 100644 --- a/includes/handler/class-delete.php +++ b/includes/handler/class-delete.php @@ -12,6 +12,7 @@ class Delete { */ public static function init() { \add_action( 'activitypub_inbox_delete', array( self::class, 'handle_delete' ), 10, 2 ); + // } /** @@ -42,6 +43,8 @@ class Delete { $follower->delete(); } + // delete all activities from this user. + break; case 'Tombstone': // Handle tombstone. diff --git a/tests/test-class-activitypub-create-handler.php b/tests/test-class-activitypub-create-handler.php new file mode 100644 index 0000000..4cf2fea --- /dev/null +++ b/tests/test-class-activitypub-create-handler.php @@ -0,0 +1,70 @@ +user_id = 1; + $authordata = \get_userdata( $this->user_id ); + $this->user_url = $authordata->user_url; + + $this->post_id = \wp_insert_post( + array( + 'post_author' => $this->user_id, + 'post_content' => 'test', + ) + ); + $this->post_permalink = \get_permalink( $this->post_id ); + + \add_filter( 'pre_get_remote_metadata_by_actor', array( '\Test_Activitypub_Create_Handler', 'get_remote_metadata_by_actor' ), 0, 2 ); + } + + public static function get_remote_metadata_by_actor( $value, $actor ) { + return array( + 'name' => 'Example User', + 'icon' => array( + 'url' => 'https://example.com/icon', + ), + 'url' => $actor, + 'id' => 'http://example.org/users/example', + ); + } + + public function create_test_object( $id = 'https://example.com/123' ) { + return array( + 'actor' => $this->user_url, + 'id' => 'https://example.com/id/' . microtime( true ), + 'to' => [ $this->user_url ], + 'cc' => [ 'https://www.w3.org/ns/activitystreams#Public' ], + 'object' => array( + 'id' => $id, + 'url' => 'https://example.com/example', + 'inReplyTo' => $this->post_permalink, + 'content' => 'example', + ), + ); + } + + public function test_handle_create_object_unset_rejected() { + $object = $this->create_test_object(); + unset( $object['object'] ); + $converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id ); + $this->assertEquals( $converted->get_error_code(), 'activitypub_no_valid_object' ); + } + + public function test_handle_create_non_public_rejected() { + $object = $this->create_test_object(); + $object['cc'] = []; + $converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id ); + $this->assertEquals( $converted->get_error_code(), 'activitypub_activity_not_public' ); + } + + public function test_handle_create_no_id_rejected() { + $object = $this->create_test_object(); + unset( $object['object']['id'] ); + $converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id ); + $this->assertEquals( $converted->get_error_code(), 'activitypub_no_valid_object' ); + } +} diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-activitypub-followers.php similarity index 99% rename from tests/test-class-db-activitypub-followers.php rename to tests/test-class-activitypub-followers.php index 8fc0068..8b00d19 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-activitypub-followers.php @@ -1,5 +1,5 @@ array( 'id' => 'https://example.org/users/username', diff --git a/tests/test-class-activitypub-interactions.php b/tests/test-class-activitypub-interactions.php new file mode 100644 index 0000000..1767b1f --- /dev/null +++ b/tests/test-class-activitypub-interactions.php @@ -0,0 +1,112 @@ +user_id = 1; + $authordata = \get_userdata( $this->user_id ); + $this->user_url = $authordata->user_url; + + $this->post_id = \wp_insert_post( + array( + 'post_author' => $this->user_id, + 'post_content' => 'test', + ) + ); + $this->post_permalink = \get_permalink( $this->post_id ); + + \add_filter( 'pre_get_remote_metadata_by_actor', array( '\Test_Activitypub_Interactions', 'get_remote_metadata_by_actor' ), 0, 2 ); + } + + public static function get_remote_metadata_by_actor( $value, $actor ) { + return array( + 'name' => 'Example User', + 'icon' => array( + 'url' => 'https://example.com/icon', + ), + 'url' => $actor, + 'id' => 'http://example.org/users/example', + ); + } + + public function create_test_object( $id = 'https://example.com/123' ) { + return array( + 'actor' => $this->user_url, + 'id' => 'https://example.com/id/' . microtime( true ), + 'to' => [ $this->user_url ], + 'cc' => [ 'https://www.w3.org/ns/activitystreams#Public' ], + 'object' => array( + 'id' => $id, + 'url' => 'https://example.com/example', + 'inReplyTo' => $this->post_permalink, + 'content' => 'example', + ), + ); + } + + public function test_handle_create_basic() { + $comment_id = Activitypub\Collection\Interactions::add_comment( $this->create_test_object() ); + $comment = get_comment( $comment_id, ARRAY_A ); + + $this->assertIsArray( $comment ); + $this->assertEquals( $this->post_id, $comment['comment_post_ID'] ); + $this->assertEquals( 'Example User', $comment['comment_author'] ); + $this->assertEquals( $this->user_url, $comment['comment_author_url'] ); + $this->assertEquals( 'example', $comment['comment_content'] ); + $this->assertEquals( 'comment', $comment['comment_type'] ); + $this->assertEquals( '', $comment['comment_author_email'] ); + $this->assertEquals( 0, $comment['comment_parent'] ); + $this->assertEquals( 'https://example.com/123', get_comment_meta( $comment_id, 'source_id', true ) ); + $this->assertEquals( 'https://example.com/example', get_comment_meta( $comment_id, 'source_url', true ) ); + $this->assertEquals( 'https://example.com/icon', get_comment_meta( $comment_id, 'avatar_url', true ) ); + $this->assertEquals( 'activitypub', get_comment_meta( $comment_id, 'protocol', true ) ); + } + + public function test_convert_object_to_comment_not_reply_rejected() { + $object = $this->create_test_object(); + unset( $object['object']['inReplyTo'] ); + $converted = Activitypub\Collection\Interactions::add_comment( $object ); + $this->assertEquals( $converted->get_error_code(), 'activitypub_no_reply' ); + } + + public function test_convert_object_to_comment_already_exists_rejected() { + $object = $this->create_test_object( 'https://example.com/test_convert_object_to_comment_already_exists_rejected' ); + Activitypub\Collection\Interactions::add_comment( $object ); + $converted = Activitypub\Collection\Interactions::add_comment( $object ); + $this->assertEquals( $converted->get_error_code(), 'comment_duplicate' ); + } + + public function test_convert_object_to_comment_reply_to_comment() { + $id = 'https://example.com/test_convert_object_to_comment_reply_to_comment'; + $object = $this->create_test_object( $id ); + Activitypub\Collection\Interactions::add_comment( $object ); + $comment = \Activitypub\object_id_to_comment( $id ); + + $object['object']['inReplyTo'] = $id; + $object['object']['id'] = 'https://example.com/234'; + $id = Activitypub\Collection\Interactions::add_comment( $object ); + $converted = get_comment( $id, ARRAY_A ); + + $this->assertIsArray( $converted ); + $this->assertEquals( $this->post_id, $converted['comment_post_ID'] ); + $this->assertEquals( $comment->comment_ID, $converted['comment_parent'] ); + } + + public function test_convert_object_to_comment_reply_to_non_existent_comment_rejected() { + $object = $this->create_test_object(); + $object['object']['inReplyTo'] = 'https://example.com/not_found'; + $converted = Activitypub\Collection\Interactions::add_comment( $object ); + $this->assertEquals( $converted->get_error_code(), 'activitypub_no_reply' ); + } + + public function test_handle_create_basic2() { + $id = 'https://example.com/test_handle_create_basic'; + $object = $this->create_test_object( $id ); + Activitypub\Collection\Interactions::add_comment( $object ); + $comment = \Activitypub\object_id_to_comment( $id ); + $this->assertInstanceOf( WP_Comment::class, $comment ); + } +}