tags. * * @since 1.0.5 */ class AIOPOD_Public_Transcript { /** * Serve a transcript request. * * Access is checked before anything is said about the transcript itself, so a restricted * episode answers 403 whether or not it has one. * * @since 1.0.5 * @return void */ public function handle_request() { // Never run in admin or AJAX context if ( is_admin() || wp_doing_ajax() ) { return; } $language = sanitize_text_field( (string) get_query_var( 'aiopod_transcript' ) ); if ( '' === $language ) { return; } $episode_slug = get_query_var( 'aiopod_episode' ); $format = sanitize_key( (string) get_query_var( 'aiopod_format' ) ); $episode = null; if ( ! empty( $episode_slug ) ) { $episodes = get_posts( array( 'name' => sanitize_title( $episode_slug ), 'post_type' => 'aiopod_episodes', 'post_status' => 'publish', 'posts_per_page' => 1, 'no_found_rows' => true ) ); $episode = ! empty( $episodes ) ? $episodes[0] : null; } if ( ! $episode ) { wp_die( esc_html__( 'Episode not found.', 'aio-podcast-studio' ), '', array( 'response' => 404 ) ); } if ( ! aiopod_current_user_can( 'view_aiopod_podcast', $episode->ID ) ) { wp_die( esc_html__( 'You do not have permission to view this transcript.', 'aio-podcast-studio' ), '', array( 'response' => 403 ) ); } $output = $this->get_output( $episode, $language, $format ); if ( '' === $output ) { wp_die( esc_html__( 'Transcript not found.', 'aio-podcast-studio' ), '', array( 'response' => 404 ) ); } $this->send( $output, $format, aiopod_is_content_restricted( $episode->ID ) ); } /** * Build the requested transcript. * * @since 1.0.5 * @access private * @param WP_Post $episode Episode post object. * @param string $language Requested language, or 'default' for the row in the podcast's language. * @param string $format Requested format key. * @return string The transcript, or '' when there is none to serve. */ private function get_output( $episode, $language, $format ) { if ( ! get_post_meta( $episode->ID, 'enable_transcription', true ) ) { return ''; } $language = 'default' === $language ? '' : aiopod_normalize_language_code( $language ); $transcript = null; foreach ( aiopod_get_usable_transcripts( $episode->ID ) as $row ) { if ( aiopod_normalize_language_code( $row['language'] ) === $language ) { $transcript = $row; break; } } if ( null === $transcript ) { return ''; } $loaded = aiopod_load_transcript( $transcript, $episode->ID ); if ( '' !== $loaded['error'] || ! in_array( $format, aiopod_get_transcript_output_formats( $loaded['type'] ), true ) ) { return ''; } // Built from what was loaded, so a cached transcript is never parsed again if ( ! empty( $loaded['cues'] ) ) { switch ( $format ) { case 'vtt': return aiopod_build_vtt_transcript( $loaded['cues'] ); case 'srt': return aiopod_build_srt_transcript( $loaded['cues'] ); case 'json': return aiopod_build_json_transcript( $loaded['cues'] ); case 'html': return $this->get_html_document( aiopod_build_html_transcript( $loaded['cues'] ), $episode, $transcript ); } return ''; } $output = aiopod_build_untimed_transcript( $loaded['text'], $loaded['type'], $format ); if ( 'html' === $format && '' !== $output ) { $output = $this->get_html_document( $output, $episode, $transcript ); } return $output; } /** * Wrap an HTML transcript fragment in a document of its own. * * @since 1.0.5 * @access private * @param string $fragment Escaped HTML transcript. * @param WP_Post $episode Episode post object. * @param array $transcript Transcript row. * @return string HTML document. */ private function get_html_document( $fragment, $episode, $transcript ) { $language = '' !== $transcript['language'] ? $transcript['language'] : aiopod_get_podcast_language( (int) $episode->post_parent ); /* translators: %s: Episode title */ $title = sprintf( __( 'Transcript: %s', 'aio-podcast-studio' ), get_the_title( $episode ) ); return '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n" . '' . esc_html( $title ) . '' . "\n" . '' . "\n" . '' . "\n" . $fragment . '' . "\n" . '' . "\n"; } /** * Send a transcript with its headers and end the request. * * The entity tag is the checksum of the body, as for the feed, so a repeat request for an * unchanged transcript costs a 304 and nothing more. * * @since 1.0.5 * @access private * @param string $output Transcript body. * @param string $format Format key. * @param bool $is_restricted Whether the episode is restricted. * @return void */ private function send( $output, $format, $is_restricted ) { $formats = aiopod_get_transcript_formats(); $etag = '"' . md5( $output ) . '"'; status_header( 200 ); nocache_headers(); header( 'Content-Type: ' . $formats[ $format ] . '; charset=utf-8' ); header( 'X-Content-Type-Options: nosniff' ); // The episode page carries this text already; the copies served here are for podcast apps header( 'X-Robots-Tag: noindex' ); // Revalidated on every use, so a transcript that becomes restricted stops being served at once header( 'Cache-Control: no-cache, must-revalidate, max-age=0' . ( $is_restricted ? ', private' : '' ) ); header( 'ETag: ' . $etag ); // Web-based players may fetch a public transcript from another site; never one behind a member's credential if ( ! $is_restricted ) { header( 'Access-Control-Allow-Origin: *' ); } $if_none_match = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_IF_NONE_MATCH'] ) ) : ''; if ( '' !== $if_none_match && false !== strpos( $if_none_match, $etag ) ) { status_header( 304 ); header_remove( 'Content-Type' ); exit; } if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'HEAD' === $_SERVER['REQUEST_METHOD'] ) { exit; } echo $output; // phpcs:ignore WordPress.Security.EscapeOutput -- built escaped, see get_output() exit; } }