'&',
'<' => '<',
'>' => '>',
'"' => '"',
"'" => ''',
)
);
}
/**
* Accept only absolute HTTP(S) URLs, matching the package contract.
*
* @param mixed $value Candidate URL.
*/
public static function safe_url( $value ): ?string {
if ( ! is_string( $value ) ) {
return null;
}
$value = trim( $value );
return preg_match( '~^https?://~i', $value ) ? $value : null;
}
/**
* A title link, or plain text for an unsafe URL.
*
* @param mixed $href URL.
* @param mixed $text Title.
*/
private static function link( $href, $text ): string {
$text = is_string( $text ) ? $text : '';
$url = self::safe_url( $href );
return null !== $url ? '' . self::escape( $text ) . '' : self::escape( $text );
}
/**
* Player and play-button fallback.
*
* @param array $episode Public payload or insertion snapshot.
* @param array $options Renderer options.
*/
public static function episode( array $episode, array $options = array() ): string {
$html = self::link( $episode['links']['listen'] ?? null, $episode['title'] ?? '' );
$url = self::safe_url( $episode['audio']['url'] ?? null );
if ( null !== $url ) {
if ( ! empty( $options['sourceTag'] ) ) {
// encodeURIComponent leaves these five characters unescaped.
$tag = strtr(
rawurlencode( $options['sourceTag'] ),
array(
'%21' => '!',
'%27' => "'",
'%28' => '(',
'%29' => ')',
'%2A' => '*',
)
);
$url .= ( false === strpos( $url, '?' ) ? '?' : '&' ) . 'src=' . $tag;
}
$html .= '';
}
return $html;
}
/**
* List fallback.
*
* @param array $podcast Show payload or snapshot.
* @param array> $episodes Episodes.
* @param array $options Renderer options.
*/
public static function episode_list( array $podcast, array $episodes, array $options = array() ): string {
$shown = array_slice( $episodes, 0, max( 0, (int) ( $options['limit'] ?? count( $episodes ) ) ) );
if ( ! $shown ) {
return self::link( $podcast['links']['listen'] ?? null, $podcast['title'] ?? '' );
}
$html = '';
foreach ( $shown as $episode ) {
$html .= '- ' . self::link( $episode['links']['listen'] ?? null, $episode['title'] ?? '' ) . '
';
}
return $html . '
';
}
/**
* Serialised PodcastEpisode JSON-LD, safe inside a script element.
*
* @param array $episode Public episode.
*/
public static function json_ld( array $episode ): string {
$data = array(
'@context' => 'https://schema.org',
'@type' => 'PodcastEpisode',
'name' => is_string( $episode['title'] ?? null ) ? $episode['title'] : '',
);
$url = self::safe_url( $episode['links']['listen'] ?? null );
if ( $url ) {
$data['url'] = $url;
}
if ( is_string( $episode['published_at'] ?? null ) && '' !== $episode['published_at'] ) {
try {
$date = new \DateTimeImmutable( $episode['published_at'], new \DateTimeZone( 'UTC' ) );
} catch ( \Exception $exception ) {
// Invalid dates are omitted, as in the package.
$date = null;
}
if ( null !== $date ) {
$data['datePublished'] = $date->setTimezone( new \DateTimeZone( 'UTC' ) )->format( 'Y-m-d\TH:i:s.v\Z' );
}
}
if ( ! empty( $episode['description'] ) ) {
$data['description'] = $episode['description'];
}
$duration = $episode['audio']['duration_seconds'] ?? null;
if ( ( is_int( $duration ) || is_float( $duration ) ) && is_finite( (float) $duration ) && $duration > 0 ) {
$seconds = (int) round( $duration );
$hours = intdiv( $seconds, 3600 );
$minutes = intdiv( $seconds % 3600, 60 );
$rest = $seconds % 60;
$data['duration'] = 'PT' . ( $hours ? $hours . 'H' : '' ) . ( $minutes ? $minutes . 'M' : '' ) . ( $rest || 0 === $seconds ? $rest . 'S' : '' );
}
$image = self::safe_url( $episode['artwork']['url'] ?? null );
if ( $image ) {
$data['image'] = $image;
}
if ( isset( $episode['episode_number'] ) && ( is_int( $episode['episode_number'] ) || is_float( $episode['episode_number'] ) ) ) {
$data['episodeNumber'] = $episode['episode_number'];
}
if ( isset( $episode['season_number'] ) && ( is_int( $episode['season_number'] ) || is_float( $episode['season_number'] ) ) ) {
$data['partOfSeason'] = array(
'@type' => 'PodcastSeason',
'seasonNumber' => $episode['season_number'],
);
}
if ( is_string( $episode['podcast']['title'] ?? null ) && '' !== $episode['podcast']['title'] ) {
$series = array(
'@type' => 'PodcastSeries',
'name' => $episode['podcast']['title'],
);
$url = self::safe_url( $episode['podcast']['links']['listen'] ?? null );
if ( $url ) {
$series['url'] = $url;
}
$data['partOfSeries'] = $series;
}
$audio = self::safe_url( $episode['audio']['url'] ?? null );
if ( $audio ) {
$media = array(
'@type' => 'MediaObject',
'contentUrl' => $audio,
);
if ( ! empty( $episode['audio']['content_type'] ) ) {
$media['encodingFormat'] = $episode['audio']['content_type'];
}
$data['associatedMedia'] = $media;
}
return str_replace( array( '<', '>', '&' ), array( '\u003c', '\u003e', '\u0026' ), (string) wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
}
}