From 73051073935e652404e713098aa9d48eebffbae5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 Jun 2026 15:19:26 +0200 Subject: [PATCH 1/2] Federate the Podlove episode summary Podlove stores an episode's summary separately from post_content, so it was never federated. Override the transformer's get_summary() to prefer the episode summary (falling back to the default summary, and leaving Notes without one). Adds a Podlove Episode test stub and regression tests for the summary and the fallback. Fixes #3455. --- .github/changelog/add-podlove-episode-summary | 4 + .../class-podlove-podcast-publisher.php | 16 ++++ tests/phpunit/includes/class-episode.php | 80 ++++++++++++++++++ .../class-test-podlove-podcast-publisher.php | 81 +++++++++++++++++++ 4 files changed, 181 insertions(+) create mode 100644 .github/changelog/add-podlove-episode-summary create mode 100644 tests/phpunit/includes/class-episode.php diff --git a/.github/changelog/add-podlove-episode-summary b/.github/changelog/add-podlove-episode-summary new file mode 100644 index 0000000000..23adc36661 --- /dev/null +++ b/.github/changelog/add-podlove-episode-summary @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Federate the episode summary for Podlove Podcast Publisher episodes. diff --git a/integration/class-podlove-podcast-publisher.php b/integration/class-podlove-podcast-publisher.php index 62ef614281..bc0f14e1a7 100644 --- a/integration/class-podlove-podcast-publisher.php +++ b/integration/class-podlove-podcast-publisher.php @@ -40,6 +40,22 @@ protected function get_episode() { return $this->episode; } + /** + * Returns the summary for the ActivityPub Item. + * + * @return string|null The summary or null. + */ + protected function get_summary() { + $episode = $this->get_episode(); + + // Podlove keeps the episode summary out of post_content, so federate it explicitly (Notes carry no summary). + if ( $episode && ! empty( $episode->summary ) && 'Note' !== $this->get_type() ) { + return \wp_strip_all_tags( $episode->summary ); + } + + return parent::get_summary(); + } + /** * Gets the attachment for a podcast episode. * diff --git a/tests/phpunit/includes/class-episode.php b/tests/phpunit/includes/class-episode.php new file mode 100644 index 0000000000..46abf130d1 --- /dev/null +++ b/tests/phpunit/includes/class-episode.php @@ -0,0 +1,80 @@ +assertTrue( class_exists( '\Activitypub\Integration\Podlove_Podcast_Publisher' ) ); $this->assertTrue( is_subclass_of( '\Activitypub\Integration\Podlove_Podcast_Publisher', '\Activitypub\Transformer\Post' ) ); } + + /** + * Test that the episode summary is federated as the object summary. + * + * @covers ::get_summary + */ + public function test_get_summary_uses_episode_summary() { + \update_option( 'activitypub_object_type', 'wordpress-post-format' ); + + $post = \get_post( + \wp_insert_post( + array( + 'post_author' => 1, + 'post_title' => 'Episode title', + 'post_content' => 'Episode content.', + 'post_status' => 'publish', + 'post_type' => 'post', + ) + ) + ); + + $episode = new \Podlove\Model\Episode(); + $episode->summary = 'The episode summary that should be federated.'; + \Podlove\Model\Episode::$mock = $episode; + + $object = ( new \Activitypub\Integration\Podlove_Podcast_Publisher( $post ) )->to_object(); + + $this->assertSame( 'The episode summary that should be federated.', $object->get_summary() ); + + \wp_delete_post( $post->ID, true ); + } + + /** + * Test that the transformer falls back to the default summary without an episode summary. + * + * @covers ::get_summary + */ + public function test_get_summary_falls_back_without_episode_summary() { + \update_option( 'activitypub_object_type', 'wordpress-post-format' ); + + $post = \get_post( + \wp_insert_post( + array( + 'post_author' => 1, + 'post_title' => 'Episode title', + 'post_content' => 'Episode content that should be summarized.', + 'post_status' => 'publish', + 'post_type' => 'post', + ) + ) + ); + + $episode = new \Podlove\Model\Episode(); + $episode->summary = ''; + \Podlove\Model\Episode::$mock = $episode; + + $podlove = ( new \Activitypub\Integration\Podlove_Podcast_Publisher( $post ) )->to_object(); + $default = ( new \Activitypub\Transformer\Post( $post ) )->to_object(); + + $this->assertSame( $default->get_summary(), $podlove->get_summary() ); + + \wp_delete_post( $post->ID, true ); + } } From 1210ed7b7f157e87606f95e5c29d777a53d40863 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 Jun 2026 16:36:48 +0200 Subject: [PATCH 2/2] Sanitize the Podlove episode summary like the default summary Strip shortcodes, tags, and decode entities (as generate_post_summary does) before federating, and fall back to the default summary when the sanitized result is empty. Tests now cover entity/tag sanitization and a markup-only summary falling back. --- integration/class-podlove-podcast-publisher.php | 14 +++++++++++--- .../class-test-podlove-podcast-publisher.php | 11 +++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/integration/class-podlove-podcast-publisher.php b/integration/class-podlove-podcast-publisher.php index bc0f14e1a7..4d234837cd 100644 --- a/integration/class-podlove-podcast-publisher.php +++ b/integration/class-podlove-podcast-publisher.php @@ -48,9 +48,17 @@ protected function get_episode() { protected function get_summary() { $episode = $this->get_episode(); - // Podlove keeps the episode summary out of post_content, so federate it explicitly (Notes carry no summary). - if ( $episode && ! empty( $episode->summary ) && 'Note' !== $this->get_type() ) { - return \wp_strip_all_tags( $episode->summary ); + // Notes carry no summary, so there is nothing to override for them. + if ( $episode && 'Note' !== $this->get_type() ) { + // Sanitize like generate_post_summary() does, since Podlove stores the summary as raw user input. + $summary = \strip_shortcodes( (string) $episode->summary ); + $summary = \wp_strip_all_tags( $summary ); + $summary = \trim( \html_entity_decode( $summary, ENT_QUOTES, 'UTF-8' ) ); + + // Federate the episode summary explicitly; Podlove keeps it out of post_content. + if ( '' !== $summary ) { + return $summary; + } } return parent::get_summary(); diff --git a/tests/phpunit/tests/integration/class-test-podlove-podcast-publisher.php b/tests/phpunit/tests/integration/class-test-podlove-podcast-publisher.php index aef9473c13..4d63a1e248 100644 --- a/tests/phpunit/tests/integration/class-test-podlove-podcast-publisher.php +++ b/tests/phpunit/tests/integration/class-test-podlove-podcast-publisher.php @@ -170,19 +170,21 @@ public function test_get_summary_uses_episode_summary() { ) ); + // Raw user input: tags and HTML entities should be sanitized like the default summary. $episode = new \Podlove\Model\Episode(); - $episode->summary = 'The episode summary that should be federated.'; + $episode->summary = '

Episode summary & more

'; \Podlove\Model\Episode::$mock = $episode; $object = ( new \Activitypub\Integration\Podlove_Podcast_Publisher( $post ) )->to_object(); - $this->assertSame( 'The episode summary that should be federated.', $object->get_summary() ); + $this->assertSame( 'Episode summary & more', $object->get_summary() ); \wp_delete_post( $post->ID, true ); } /** - * Test that the transformer falls back to the default summary without an episode summary. + * Test that the transformer falls back to the default summary when the episode + * summary is empty (including markup/whitespace that sanitizes to nothing). * * @covers ::get_summary */ @@ -201,8 +203,9 @@ public function test_get_summary_falls_back_without_episode_summary() { ) ); + // Markup/whitespace only: sanitizes to an empty string, so the default summary is used. $episode = new \Podlove\Model\Episode(); - $episode->summary = ''; + $episode->summary = '

'; \Podlove\Model\Episode::$mock = $episode; $podlove = ( new \Activitypub\Integration\Podlove_Podcast_Publisher( $post ) )->to_object();