diff --git a/CHANGELOG.md b/CHANGELOG.md index 050f4fe9b..ed099414d 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 +* `icon` support for `Audio` and `Video` attachments * Send "new follower" emails ### Improved diff --git a/includes/functions.php b/includes/functions.php index 32eb34a18..d16a30bf7 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -822,6 +822,9 @@ function object_to_uri( $data ) { // Return part of Object that makes most sense. switch ( $type ) { + case 'Image': + $data = $data['url']; + break; case 'Link': $data = $data['href']; break; diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index a14490075..40239f0e9 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -13,6 +13,7 @@ use Activitypub\Collection\Actors; use function Activitypub\esc_hashtag; +use function Activitypub\object_to_uri; use function Activitypub\is_single_user; use function Activitypub\get_enclosures; use function Activitypub\get_upload_baseurl; @@ -208,7 +209,63 @@ protected function get_image() { */ $thumbnail = apply_filters( 'activitypub_get_image', - self::get_wordpress_attachment( $id, $image_size ), + $this->get_wordpress_attachment( $id, $image_size ), + $id, + $image_size + ); + + if ( ! $thumbnail ) { + return null; + } + + $mime_type = \get_post_mime_type( $id ); + + $image = array( + 'type' => 'Image', + 'url' => \esc_url( $thumbnail[0] ), + 'mediaType' => \esc_attr( $mime_type ), + ); + + $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true ); + if ( $alt ) { + $image['name'] = \wp_strip_all_tags( \html_entity_decode( $alt ) ); + } + + return $image; + } + + /** + * Returns an Icon, based on the Featured Image with a fallback to the site-icon. + * + * @return array|null The Icon or null if no icon is available. + */ + protected function get_icon() { + $post_id = $this->wp_object->ID; + + // List post thumbnail first if this post has one. + if ( \has_post_thumbnail( $post_id ) ) { + $id = \get_post_thumbnail_id( $post_id ); + } else { + // Try site_logo, falling back to site_icon, first. + $id = get_option( 'site_icon' ); + } + + if ( ! $id ) { + return null; + } + + $image_size = 'thumbnail'; + + /** + * Filter the image URL returned for each post. + * + * @param array|false $thumbnail The image URL, or false if no image is available. + * @param int $id The attachment ID. + * @param string $image_size The image size to retrieve. Set to 'large' by default. + */ + $thumbnail = apply_filters( + 'activitypub_get_image', + $this->get_wordpress_attachment( $id, $image_size ), $id, $image_size ); @@ -273,7 +330,7 @@ protected function get_attachment() { $media = $this->get_classic_editor_images( $media, $max_media ); } - $media = self::filter_media_by_object_type( $media, \get_post_format( $this->wp_object ), $this->wp_object ); + $media = $this->filter_media_by_object_type( $media, \get_post_format( $this->wp_object ), $this->wp_object ); $unique_ids = \array_unique( \array_column( $media, 'id' ) ); $media = \array_intersect_key( $media, $unique_ids ); $media = \array_slice( $media, 0, $max_media ); @@ -288,7 +345,7 @@ protected function get_attachment() { */ $media = \apply_filters( 'activitypub_attachment_ids', $media, $this->wp_object ); - $attachments = \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $media ) ); + $attachments = \array_filter( \array_map( array( $this, 'wp_attachment_to_activity_attachment' ), $media ) ); /** * Filter the attachments for a post. @@ -361,7 +418,7 @@ protected function get_block_attachments( $media, $max_media ) { $blocks = \parse_blocks( $this->wp_object->post_content ); - return self::get_media_from_blocks( $blocks, $media ); + return $this->get_media_from_blocks( $blocks, $media ); } /** @@ -372,11 +429,11 @@ protected function get_block_attachments( $media, $max_media ) { * * @return array The image IDs. */ - protected static function get_media_from_blocks( $blocks, $media ) { + protected function get_media_from_blocks( $blocks, $media ) { foreach ( $blocks as $block ) { // Recurse into inner blocks. if ( ! empty( $block['innerBlocks'] ) ) { - $media = self::get_media_from_blocks( $block['innerBlocks'], $media ); + $media = $this->get_media_from_blocks( $block['innerBlocks'], $media ); } switch ( $block['blockName'] ) { @@ -587,7 +644,7 @@ protected function get_classic_editor_image_attachments( $max_images ) { * * @return array The filtered media IDs. */ - protected static function filter_media_by_object_type( $media, $type, $wp_object ) { + protected function filter_media_by_object_type( $media, $type, $wp_object ) { /** * Filter the object type for media attachments. * @@ -612,7 +669,7 @@ protected static function filter_media_by_object_type( $media, $type, $wp_object * * @return array The ActivityPub Attachment. */ - public static function wp_attachment_to_activity_attachment( $media ) { + public function wp_attachment_to_activity_attachment( $media ) { if ( ! isset( $media['id'] ) ) { return $media; } @@ -635,7 +692,7 @@ public static function wp_attachment_to_activity_attachment( $media ) { */ $thumbnail = apply_filters( 'activitypub_get_image', - self::get_wordpress_attachment( $id, $image_size ), + $this->get_wordpress_attachment( $id, $image_size ), $id, $image_size ); @@ -674,7 +731,11 @@ public static function wp_attachment_to_activity_attachment( $media ) { $attachment['width'] = \esc_attr( $meta['width'] ); $attachment['height'] = \esc_attr( $meta['height'] ); } - // @todo: add `icon` support for audio/video attachments. Maybe use post thumbnail? + + if ( $this->get_icon() ) { + $attachment['icon'] = object_to_uri( $this->get_icon() ); + } + break; } @@ -697,7 +758,7 @@ public static function wp_attachment_to_activity_attachment( $media ) { * * @return array|false Array of image data, or boolean false if no image is available. */ - protected static function get_wordpress_attachment( $id, $image_size = 'large' ) { + protected function get_wordpress_attachment( $id, $image_size = 'large' ) { /** * Hook into the image retrieval process. Before image retrieval. * @@ -920,7 +981,7 @@ protected function get_content() { */ do_action( 'activitypub_before_get_content', $post ); - add_filter( 'render_block_core/embed', array( self::class, 'revert_embed_links' ), 10, 2 ); + add_filter( 'render_block_core/embed', array( $this, 'revert_embed_links' ), 10, 2 ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $post = $this->wp_object; @@ -1119,7 +1180,7 @@ public function get_summary_map() { * * @return string A block level link */ - public static function revert_embed_links( $block_content, $block ) { + public function revert_embed_links( $block_content, $block ) { if ( ! isset( $block['attrs']['url'] ) ) { return $block_content; } diff --git a/integration/class-seriously-simple-podcasting.php b/integration/class-seriously-simple-podcasting.php index 8ca343b45..167af7e3e 100644 --- a/integration/class-seriously-simple-podcasting.php +++ b/integration/class-seriously-simple-podcasting.php @@ -9,6 +9,7 @@ use Activitypub\Transformer\Post; +use function Activitypub\object_to_uri; use function Activitypub\generate_post_summary; /** @@ -35,9 +36,13 @@ public function get_attachment() { 'name' => \esc_attr( \get_the_title( $post->ID ) ?? '' ), ); - $cover = \get_post_meta( $post->ID, 'cover_image', true ); - if ( $cover ) { - $attachment['icon'] = \esc_url( $cover ); + $icon = \get_post_meta( $post->ID, 'cover_image', true ); + if ( ! $icon ) { + $icon = $this->get_icon(); + } + + if ( $icon ) { + $attachment['icon'] = \esc_url( object_to_uri( $icon ) ); } return array( $attachment ); diff --git a/readme.txt b/readme.txt index e5a5d8dc8..ea0b95adb 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: `icon` support for `Audio` and `Video` attachments * Added: Send "new follower" emails * Improved: Email templates for Likes and Reposts * Improved: Interactions moderation diff --git a/tests/includes/transformer/class-test-post.php b/tests/includes/transformer/class-test-post.php index e9903b21c..63cb574cf 100644 --- a/tests/includes/transformer/class-test-post.php +++ b/tests/includes/transformer/class-test-post.php @@ -438,4 +438,79 @@ public function create_upload_object( $file, $parent_id = 0 ) { return $id; } + + /** + * Test get_icon method. + * + * @covers ::get_icon + */ + public function test_get_icon() { + $post_id = $this->factory->post->create( + array( + 'post_title' => 'Test Post', + 'post_content' => 'Test content', + ) + ); + $post = get_post( $post_id ); + + // Create test image. + $attachment_id = $this->create_upload_object( dirname( __DIR__, 2 ) . '/assets/test.jpg' ); + + // Set up reflection method. + $reflection = new ReflectionClass( Post::class ); + $method = $reflection->getMethod( 'get_icon' ); + $method->setAccessible( true ); + + // Test with featured image. + set_post_thumbnail( $post_id, $attachment_id ); + + $transformer = new Post( $post ); + $icon = $method->invoke( $transformer ); + + $this->assertIsArray( $icon ); + $this->assertEquals( 'Image', $icon['type'] ); + $this->assertArrayHasKey( 'url', $icon ); + $this->assertArrayHasKey( 'mediaType', $icon ); + $this->assertEquals( get_post_mime_type( $attachment_id ), $icon['mediaType'] ); + + // Test with site icon. + delete_post_thumbnail( $post_id ); + update_option( 'site_icon', $attachment_id ); + + $icon = $method->invoke( $transformer ); + + $this->assertIsArray( $icon ); + $this->assertEquals( 'Image', $icon['type'] ); + $this->assertArrayHasKey( 'url', $icon ); + $this->assertArrayHasKey( 'mediaType', $icon ); + $this->assertEquals( get_post_mime_type( $attachment_id ), $icon['mediaType'] ); + + // Test with alt text. + $alt_text = 'Test Alt Text'; + update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt_text ); + + $icon = $method->invoke( $transformer ); + + $this->assertIsArray( $icon ); + $this->assertEquals( 'Image', $icon['type'] ); + $this->assertArrayHasKey( 'name', $icon ); + $this->assertEquals( $alt_text, $icon['name'] ); + + // Test without any images. + delete_post_thumbnail( $post_id ); + delete_option( 'site_icon' ); + delete_post_meta( $attachment_id, '_wp_attachment_image_alt' ); + + $icon = $method->invoke( $transformer ); + $this->assertNull( $icon ); + + // Test with invalid image. + set_post_thumbnail( $post_id, 99999 ); + $icon = $method->invoke( $transformer ); + $this->assertNull( $icon ); + + // Cleanup. + wp_delete_post( $post_id, true ); + wp_delete_attachment( $attachment_id, true ); + } }