Skip to content
Open
4 changes: 4 additions & 0 deletions .github/changelog/add-podlove-episode-summary
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Federate the episode summary for Podlove Podcast Publisher episodes.
24 changes: 24 additions & 0 deletions integration/class-podlove-podcast-publisher.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,30 @@ 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();

// 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();
}

/**
* Gets the attachment for a podcast episode.
*
Expand Down
80 changes: 80 additions & 0 deletions tests/phpunit/includes/class-episode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php
/**
* Minimal Podlove Episode model stub for tests.
*
* @package Activitypub
*/

namespace Podlove\Model;

if ( ! class_exists( __NAMESPACE__ . '\Episode' ) ) {
/**
* Stub of the Podlove Episode model exposing only what the integration reads.
*
* `find_one_by_post_id()` returns whatever the test assigns to `$mock` (null by
* default, so the absent-Podlove fallbacks keep working). The audio/duration/
* cover/title accessors return empty values so `to_object()` can build a full
* object without fatals while a test exercises the summary path. Parameters the
* real API takes are omitted; PHP ignores the extra call arguments.
Comment thread
pfefferle marked this conversation as resolved.
*/
class Episode {
/**
* The episode returned by find_one_by_post_id(), or null.
*
* @var self|null
*/
public static $mock = null;

/**
* The episode summary.
*
* @var string
*/
public $summary = '';

/**
* Resolve the episode for a post id.
*
* @return self|null The mocked episode or null.
*/
public static function find_one_by_post_id() {
return self::$mock;
}
Comment thread
pfefferle marked this conversation as resolved.

/**
* Media files for the episode.
*
* @return array Always empty in the stub.
*/
public function media_files() {
return array();
}

/**
* Episode duration.
*
* @return null Always null in the stub.
*/
public function get_duration() {
return null;
}
Comment thread
pfefferle marked this conversation as resolved.

/**
* Episode cover art.
*
* @return null Always null in the stub.
*/
public function cover_art_with_fallback() {
return null;
}

/**
* Episode title.
*
* @return string Always empty in the stub.
*/
public function title() {
return '';
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@
*/
class Test_Podlove_Podcast_Publisher extends \WP_UnitTestCase {

/**
* Load the Podlove Episode stub before each test.
*/
public function set_up() {
parent::set_up();
require_once AP_TESTS_DIR . '/includes/class-episode.php';
}

/**
* Reset the mocked episode after each test.
*/
public function tear_down() {
if ( class_exists( '\Podlove\Model\Episode' ) ) {
\Podlove\Model\Episode::$mock = null;
}
parent::tear_down();
}

/**
* Test that the transformer respects the configured object type setting.
*/
Expand Down Expand Up @@ -131,4 +149,70 @@ public function test_class_exists_and_extends_post() {
$this->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',
)
)
);

// Raw user input: tags and HTML entities should be sanitized like the default summary.
$episode = new \Podlove\Model\Episode();
$episode->summary = '<p>Episode summary &amp; more</p>';
\Podlove\Model\Episode::$mock = $episode;

$object = ( new \Activitypub\Integration\Podlove_Podcast_Publisher( $post ) )->to_object();

$this->assertSame( 'Episode summary & more', $object->get_summary() );

\wp_delete_post( $post->ID, true );
}

/**
* 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
*/
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',
)
)
);

// Markup/whitespace only: sanitizes to an empty string, so the default summary is used.
$episode = new \Podlove\Model\Episode();
$episode->summary = '<p> </p>';
\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 );
}
}