fix_single_pagination(); $this->redirect_custom_archives(); } /** * Register rewrite rules. * * @since 1.0.0 */ public function init() { $pages_settings = aiopod_get_option( 'aiopod_pages_settings' ); $site_url = home_url(); // Rewrite tags add_rewrite_tag( '%aiopod_podcast%', '([^/]+)' ); add_rewrite_tag( '%aiopod_episode%', '([^/]+)' ); add_rewrite_tag( '%aiopod_category%', '([^/]+)' ); add_rewrite_tag( '%aiopod_tag%', '([^/]+)' ); add_rewrite_tag( '%aiopod_participant%', '([^/]+)' ); add_rewrite_tag( '%aiopod_feed%', '([^/]+)' ); add_rewrite_tag( '%aiopod_download%', '([^/]+)' ); // RSS feed page $podcast_post_type = get_post_type_object( 'aiopod_podcasts' ); if ( $podcast_post_type && ! empty( $podcast_post_type->rewrite['slug'] ) ) { $podcast_base = trim( $podcast_post_type->rewrite['slug'], '/' ); add_rewrite_rule( "^{$podcast_base}/([^/]+)/feed/?$", 'index.php?aiopod_podcast=$matches[1]&aiopod_feed=podcast', 'top' ); } // Episode download tracking — matches: // /{base}/{slug}/file/{filename}.{ext} (new format, with extension) // /{base}/{slug}/file/ (legacy format, no filename) $episode_post_type = get_post_type_object( 'aiopod_episodes' ); if ( $episode_post_type && ! empty( $episode_post_type->rewrite['slug'] ) ) { $episode_base = trim( $episode_post_type->rewrite['slug'], '/' ); add_rewrite_rule( "^{$episode_base}/([^/]+)/file(?:/[^/]+)?/?$", 'index.php?aiopod_episode=$matches[1]&aiopod_download=1', 'top' ); } // Single category page if ( ! empty( $pages_settings['category'] ) ) { $id = (int) $pages_settings['category']; $post = get_post( $id ); if ( $post && 'publish' === $post->post_status ) { $permalink = get_permalink( $id ); if ( $permalink ) { $slug = str_replace( $site_url, '', $permalink ); $slug = trim( $slug, '/' ); $slug = urldecode( $slug ); add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&aiopod_category=$matches[1]&paged=$matches[2]', 'top' ); add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?page_id=' . $id . '&aiopod_category=$matches[1]', 'top' ); } } } // Single tag page if ( ! empty( $pages_settings['tag'] ) ) { $id = (int) $pages_settings['tag']; $post = get_post( $id ); if ( $post && 'publish' === $post->post_status ) { $permalink = get_permalink( $id ); if ( $permalink ) { $slug = str_replace( $site_url, '', $permalink ); $slug = trim( $slug, '/' ); $slug = urldecode( $slug ); add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&aiopod_tag=$matches[1]&paged=$matches[2]', 'top' ); add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?page_id=' . $id . '&aiopod_tag=$matches[1]', 'top' ); } } } // Single participant page if ( ! empty( $pages_settings['participant'] ) ) { $id = (int) $pages_settings['participant']; $post = get_post( $id ); if ( $post && 'publish' === $post->post_status ) { $permalink = get_permalink( $id ); if ( $permalink ) { $slug = str_replace( $site_url, '', $permalink ); $slug = trim( $slug, '/' ); $slug = urldecode( $slug ); add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&aiopod_participant=$matches[1]&paged=$matches[2]', 'top' ); add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?page_id=' . $id . '&aiopod_participant=$matches[1]', 'top' ); } } } // Podcast archive page if ( ! empty( $pages_settings['podcast_archive_page'] ) ) { $id = (int) $pages_settings['podcast_archive_page']; $post = get_post( $id ); if ( $post && 'publish' === $post->post_status ) { $permalink = get_permalink( $id ); if ( $permalink ) { $slug = str_replace( $site_url, '', $permalink ); $slug = trim( $slug, '/' ); $slug = urldecode( $slug ); add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?post_type=aiopod_podcasts&name=$matches[1]&paged=$matches[2]', 'top' ); add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?post_type=aiopod_podcasts&name=$matches[1]', 'top' ); add_rewrite_rule( "^$slug/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&paged=$matches[1]', 'top' ); } } } // Episode archive page if ( ! empty( $pages_settings['episode_archive_page'] ) ) { $id = (int) $pages_settings['episode_archive_page']; $post = get_post( $id ); if ( $post && 'publish' === $post->post_status ) { $permalink = get_permalink( $id ); if ( $permalink ) { $slug = str_replace( $site_url, '', $permalink ); $slug = trim( $slug, '/' ); $slug = urldecode( $slug ); add_rewrite_rule( "^$slug/([^/]+)/page/?([0-9]{1,})/?$", 'index.php?post_type=aiopod_episodes&name=$matches[1]&paged=$matches[2]', 'top' ); add_rewrite_rule( "^$slug/([^/]+)/?$", 'index.php?post_type=aiopod_episodes&name=$matches[1]', 'top' ); add_rewrite_rule( "^$slug/page/?([0-9]{1,})/?$", 'index.php?page_id=' . $id . '&paged=$matches[1]', 'top' ); } } } } /** * Register custom query vars. * * @since 1.0.0 */ public function query_vars( $vars ) { $vars[] = 'view'; $vars[] = 'aiopod_podcast'; $vars[] = 'aiopod_episode'; $vars[] = 'aiopod_category'; $vars[] = 'aiopod_tag'; $vars[] = 'aiopod_participant'; $vars[] = 'aiopod_feed'; $vars[] = 'aiopod_download'; $vars[] = 'aiopod_transcript'; $vars[] = 'aiopod_format'; return $vars; } /** * Resolve pages that sit underneath a custom archive page. * * The archive rules map `{archive-page}/{segment}` to a single podcast or episode. A child page * of that same archive page has the identical shape, and because these rules are registered at * the top of the list they are consulted before WordPress reaches its own page rules — so the * page is looked up as a podcast, is not found, and returns 404. * * This reaches any page nested under an archive page: the category, tag and participant pages * when they are children of it, and the episode archive page itself when it is a child of the * podcast one. * * An existing podcast or episode always keeps the address, so the requests these rules were * written to serve are untouched. Only a slug matching no post of that type, but matching a * published page in the archive page's own branch, is handed back to WordPress as a page. * * @since 1.0.4 * @param array $query_vars Query variables built from the matched rewrite rule. * @return array $query_vars Query variables for this request to use. */ public function resolve_archive_child_pages( $query_vars ) { if ( empty( $query_vars['name'] ) || empty( $query_vars['post_type'] ) ) { return $query_vars; } $archive_pages = array( 'aiopod_podcasts' => 'podcast_archive_page', 'aiopod_episodes' => 'episode_archive_page' ); if ( ! isset( $archive_pages[ $query_vars['post_type'] ] ) ) { return $query_vars; } $pages_settings = aiopod_get_option( 'aiopod_pages_settings' ); $setting = $archive_pages[ $query_vars['post_type'] ]; if ( empty( $pages_settings[ $setting ] ) ) { return $query_vars; } $archive_uri = get_page_uri( (int) $pages_settings[ $setting ] ); if ( empty( $archive_uri ) ) { return $query_vars; } // Tested first because it fails for every ordinary podcast and episode, which is the // overwhelming majority of what reaches here, and settles them in a single lookup $page = get_page_by_path( trailingslashit( $archive_uri ) . $query_vars['name'] ); if ( empty( $page ) || 'publish' !== $page->post_status ) { return $query_vars; } // Whatever its status, a post of this type owns the address, so a page only takes it over // where no such post exists if ( get_page_by_path( $query_vars['name'], OBJECT, $query_vars['post_type'] ) ) { return $query_vars; } $resolved = array( 'page_id' => $page->ID ); // Carried over so a paginated child page still lands on the right page of its own loop if ( ! empty( $query_vars['paged'] ) ) { $resolved['paged'] = $query_vars['paged']; } // `WP_Query::parse_query()` sets `is_preview` from this var alone, and `the_preview` — the // filter that swaps the revision in — runs only when it is set. Dropping it would show the // published page instead of the unsaved changes when a nested page is previewed if ( ! empty( $query_vars['preview'] ) ) { $resolved['preview'] = $query_vars['preview']; } return $resolved; } /** * Add podcasts and episodes to the core category and tag archives. * * `/category/foo/` and `/tag/bar/` set no `post_type` of their own, so `WP_Query` falls back * to `post` alone. Without this, a core term link on a podcast leads to an archive that does * not contain the podcast the visitor came from — it shows the wrong posts, or nothing. * * Those archives stay the theme's: our posts are rendered by `archive.php` alongside blog * posts, with no player. That is the intended result of the setting, and the reason the whole * feature is off by default. * * Registered at priority 99 and merges rather than replaces, so a plugin setting `post_type` * at the usual priority 10 cannot drop us. `aiopod_core_archive_post_types` opts back out. * * @since 1.0.4 * @param WP_Query $query The query object, before it runs. */ public function include_in_core_archives( $query ) { // Never the admin list tables, and never a theme's or a plugin's secondary loop if ( is_admin() || ! $query->is_main_query() ) { return; } // Deliberately not guarded against `is_feed()`. `/category/foo/feed/` is the core feed of // a core archive: if the archive lists our episodes, its feed lists them too. This does // not touch the plugin's own podcast feeds if ( ! $query->is_category() && ! $query->is_tag() ) { return; } $taxonomies_settings = aiopod_get_option( 'aiopod_taxonomies_settings' ); if ( empty( $taxonomies_settings['wp_taxonomies'] ) || empty( $taxonomies_settings['include_in_core_archives'] ) ) { return; } // The archive being viewed has to belong to a taxonomy that is actually enabled. A site // running tags only must find its category archives exactly as they were $setting = $query->is_category() ? 'categories' : 'tags'; if ( ! isset( $taxonomies_settings['wp_taxonomies'][ $setting ] ) ) { return; } $existing = $query->get( 'post_type' ); // `any` is a keyword, not a list, and already covers both post types if ( 'any' === $existing ) { return; } // Additive, never a replacement - another plugin's post type stays. An empty value is // the `post` that WP_Query would have fallen back to on its own $post_types = empty( $existing ) ? array( 'post' ) : (array) $existing; $post_types = array_unique( array_merge( $post_types, array( 'aiopod_podcasts', 'aiopod_episodes' ) ) ); $query->set( 'post_type', apply_filters( 'aiopod_core_archive_post_types', $post_types, $query ) ); } /** * Register the stylesheets for the public-facing side of the site. * * @since 1.0.0 */ public function register_styles() { $performance_settings = aiopod_get_option( 'aiopod_performance_settings' ); $appearance_settings = aiopod_get_option( 'aiopod_appearance_settings' ); wp_register_style( AIOPOD_PLUGIN_SLUG . '-public', AIOPOD_PLUGIN_URL . 'public/assets/css/public.min.css', array(), AIOPOD_PLUGIN_VERSION, 'all' ); $primary_color = ! empty( $appearance_settings['primary_color'] ) ? esc_attr( $appearance_settings['primary_color'] ) : '#00b2ff'; $inline_css = '.aiopod { --aiopod-color-main: ' . $primary_color . '; --plyr-color-main: ' . $primary_color . '; --swiper-theme-color: ' . $primary_color . '; }'; wp_add_inline_style( AIOPOD_PLUGIN_SLUG . '-public', $inline_css ); // Force load the plugin's CSS file if ( isset( $performance_settings['force_load_assets']['css'] ) ) { wp_enqueue_style( AIOPOD_PLUGIN_SLUG . '-public' ); } } /** * Register the JavaScript for the public-facing side of the site. * * @since 1.0.0 */ public function register_scripts() { $performance_settings = aiopod_get_option( 'aiopod_performance_settings' ); $episodes_settings = aiopod_get_option( 'aiopod_episodes_settings' ); $scroll_offset = ! empty( $episodes_settings['scroll_offset'] ) ? (int) $episodes_settings['scroll_offset'] : 0; if ( $scroll_offset && is_admin_bar_showing() ) $scroll_offset += 32; $scroll_offset = apply_filters( 'aiopod_scroll_offset', $scroll_offset ); // Register the dependencies wp_register_script( AIOPOD_PLUGIN_SLUG . '-public', AIOPOD_PLUGIN_URL . 'public/assets/js/public.min.js', array( 'jquery' ), AIOPOD_PLUGIN_VERSION, array( 'strategy' => 'defer' ) ); wp_localize_script( AIOPOD_PLUGIN_SLUG . '-public', 'aiopod', array( 'plugin_url' => AIOPOD_PLUGIN_URL, 'ajax_url' => admin_url( 'admin-ajax.php' ), 'ajax_nonce' => wp_create_nonce( 'aiopod_ajax_nonce' ), 'scroll_offset' => $scroll_offset, 'can_play_video' => apply_filters( 'aiopod_player_can_play_video', false ), ) ); // Force load the plugin's JavaScript file if ( isset( $performance_settings['force_load_assets']['js'] ) ) { wp_enqueue_script( AIOPOD_PLUGIN_SLUG . '-public' ); } } /** * Set MySQL's RAND function seed value in a cookie. * * @since 1.0.0 */ public function set_mysql_rand_seed_value() { $privacy_settings = aiopod_get_option( 'aiopod_privacy_settings' ); if ( isset( $privacy_settings['disable_cookies'] ) && isset( $privacy_settings['disable_cookies']['aiopod_rand_seed'] ) ) { unset( $_COOKIE['aiopod_rand_seed'] ); return false; } if ( headers_sent() ) { return false; } $paged = aiopod_get_page_number(); if ( ! isset( $_COOKIE['aiopod_rand_seed'] ) || 1 === $paged ) { $seed = wp_rand(); setcookie( 'aiopod_rand_seed', $seed, time() + ( 86400 * 1 ), COOKIEPATH, COOKIE_DOMAIN ); // Update $_COOKIE for immediate use in this request $_COOKIE['aiopod_rand_seed'] = $seed; } } /** * Enqueue block assets inside the block editor (iframe). * * Hooked to enqueue_block_assets with an is_admin() guard so styles and scripts * are injected inside the iframed block editor (WP 6.3+ / WP 7.0 always) only, * and not duplicated on the front end where wp_enqueue_scripts already handles them. * * @since 1.0.0 */ public function enqueue_block_assets() { if ( ! is_admin() ) { return; } $this->enqueue_editor_assets(); } /** * Enqueue the plugin's public styles and scripts in any editor context. * * Called by enqueue_block_assets() (WordPress block editor, guarded by is_admin()) * and hooked directly to Elementor actions so assets are also available in the * Elementor editor panel and its frontend live-preview iframe: * - elementor/editor/after_enqueue_scripts (admin context) * - elementor/preview/enqueue_scripts (frontend context, is_admin() = false) * * @since 1.0.0 */ public function enqueue_editor_assets() { // Styles $this->register_styles(); wp_enqueue_style( AIOPOD_PLUGIN_SLUG . '-public' ); // Scripts $this->register_scripts(); wp_enqueue_script( AIOPOD_PLUGIN_SLUG . '-public' ); } /** * Flush rewrite rules when it's necessary. * * @since 1.0.0 */ public function maybe_flush_rules() { // One-time delayed flush after activation/update/settings change. // Runs on wp_loaded (after init), so all CPTs, taxonomies and rewrite // rules are registered. Use a hard flush here so the server-level // permalink config (e.g. .htaccess) is rebuilt too — a soft flush only // updates the rewrite_rules option and can leave feed/CPT URLs 404ing // on a fresh install until permalinks are saved manually. if ( ! get_option( 'aiopod_rewrite_rules_flushed' ) ) { flush_rewrite_rules( true ); update_option( 'aiopod_rewrite_rules_flushed', 1 ); return; } $performance_settings = aiopod_get_option( 'aiopod_performance_settings' ); if ( empty( $performance_settings['maybe_flush_rewrite_rules'] ) ) { return; } // Allow developers to restrict the check to admin only $admin_only = apply_filters( 'aiopod_flush_rewrite_rules_admin_only', false ); if ( $admin_only && ! is_admin() ) { return; } $stored_rules = get_option( 'rewrite_rules' ); if ( empty( $stored_rules ) || ! is_array( $stored_rules ) ) { return; } global $wp_rewrite; if ( empty( $wp_rewrite ) ) { return; } $generated_rules = $wp_rewrite->rewrite_rules(); if ( empty( $generated_rules ) || ! is_array( $generated_rules ) ) { return; } foreach ( $generated_rules as $rule => $rewrite ) { if ( ! array_key_exists( $rule, $stored_rules ) ) { flush_rewrite_rules( false ); return; } } } /** * Override the default page/post title. * * @since 1.0.0 * @param string $title The document title. * @param string $sep Title separator. * @param string $seplocation Location of the separator (left or right). * @return string The filtered title. */ public function wp_title( $title, $sep, $seplocation ) { global $post; if ( ! isset( $post ) ) return $title; $pages_settings = aiopod_get_option( 'aiopod_pages_settings' ); $site_name = sanitize_text_field( get_bloginfo( 'name' ) ); $custom_title = ''; // Get category page title if ( $post->ID === (int) $pages_settings['category'] ) { if ( $slug = get_query_var( 'aiopod_category' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_categories' ) ) { $custom_title = $term->name; } } } // Get tag page title if ( $post->ID === (int) $pages_settings['tag'] ) { if ( $slug = get_query_var( 'aiopod_tag' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_tags' ) ) { $custom_title = $term->name; } } } // Get participant page title if ( $post->ID === (int) $pages_settings['participant'] ) { if ( $slug = get_query_var( 'aiopod_participant' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_participants' ) ) { $custom_title = $term->name; } } } // Set custom title if ( ! empty( $custom_title ) ) { $title = ( 'left' === $seplocation ) ? "$site_name $sep $custom_title" : "$custom_title $sep $site_name"; } return $title; } /** * Override the default post/page title depending on the AIOPOD view. * * @since 1.0.0 * @param array $title The document title parts. * @return Filtered title parts. */ public function document_title_parts( $title ) { global $post; if ( ! isset( $post ) ) return $title; $pages_settings = aiopod_get_option( 'aiopod_pages_settings' ); // Get category page title if ( $post->ID === (int) $pages_settings['category'] ) { if ( $slug = get_query_var( 'aiopod_category' ) ) { $term = get_term_by( 'slug', $slug, 'aiopod_categories' ); $title['title'] = $term->name; } } // Get tag page title if ( $post->ID === (int) $pages_settings['tag'] ) { if ( $slug = get_query_var( 'aiopod_tag' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_tags' ) ) { $title['title'] = $term->name; } } } // Get participant page title if ( $post->ID === (int) $pages_settings['participant'] ) { if ( $slug = get_query_var( 'aiopod_participant' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_participants' ) ) { $title['title'] = $term->name; } } } // Return return $title; } /** * Adds Facebook Open Graph tags and Twitter Cards to the page head. * * @since 1.0.0 */ public function wp_head() { $seo_settings = aiopod_get_option( 'aiopod_seo_settings' ); // Facebook Open Graph tags and Twitter Cards. global $post; $content_type = 'page'; $term_slug = ''; if ( is_singular( 'aiopod_podcasts' ) ) { $content_type = 'podcast'; } elseif ( is_singular( 'aiopod_episodes' ) ) { $content_type = 'episode'; } elseif ( $term_slug = get_query_var( 'aiopod_category' ) ) { $content_type = 'category'; } elseif ( $term_slug = get_query_var( 'aiopod_tag' ) ) { $content_type = 'tag'; } elseif ( $term_slug = get_query_var( 'aiopod_participant' ) ) { $content_type = 'participant'; } // Check if OG tags are enabled for this content type. if ( empty( $seo_settings['open_graph_tags'] ) || ! isset( $seo_settings['open_graph_tags'][ $content_type ] ) ) { return; } $site_name = get_bloginfo( 'name' ); $twitter_username = ! empty( $seo_settings['twitter_username'] ) ? trim( $seo_settings['twitter_username'] ) : ''; $page_url = ''; $page_title = ''; $page_description = ''; $image_url = ''; $media_url = ''; $media_type = ''; $is_video = 0; /** * Category / Tag / Participant pages (custom query vars) */ if ( in_array( $content_type, array( 'category', 'tag', 'participant' ) ) ) { $taxonomy = 'aiopod_categories'; if ( 'tag' === $content_type ) { $taxonomy = 'aiopod_tags'; } elseif ( 'participant' === $content_type ) { $taxonomy = 'aiopod_participants'; } $term = get_term_by( 'slug', $term_slug, $taxonomy ); if ( ! $term || is_wp_error( $term ) ) { return; } $page_url = get_term_link( $term ); $page_title = trim( $term->name ); $page_description = aiopod_truncate_text( $term->description, 160, '' ); $image_data = aiopod_get_image( array( 'term_id' => $term->term_id, 'size' => 'large', 'alt' => '' ) ); $image_url = ! empty( $image_data['src'] ) ? $image_data['src'] : ''; /** * Singular post pages */ } else { if ( empty( $post ) || ! isset( $post->ID ) ) { return; } $page_url = get_permalink( $post ); $page_title = trim( get_the_title( $post ) ); $page_description = aiopod_get_excerpt( $post->ID, 160, '', false ); $image_data = aiopod_get_image( array( 'post_id' => $post->ID, 'size' => 'large', 'alt' => '' ) ); $image_url = ! empty( $image_data['src'] ) ? $image_data['src'] : ''; $media_url = get_post_meta( $post->ID, 'episode_file', true ); if ( ! empty( $media_url ) ) { $media_type = aiopod_get_episode_file_type( $post->ID, $media_url ); $is_video = false !== strpos( $media_type, 'audio/' ); } } /** * Open Graph type mapping */ $og_type = 'website'; switch ( $content_type ) { case 'podcast': $og_type = 'music.album'; break; case 'episode': $og_type = $is_video ? 'video.episode' : 'music.song'; break; case 'participant': $og_type = 'profile'; break; } // Fallback title if ( ! $page_title ) { $page_title = $site_name; } /** * Output tags */ echo PHP_EOL . '' . PHP_EOL; echo sprintf( '' . PHP_EOL, esc_attr( $site_name ) ); echo sprintf( '' . PHP_EOL, esc_url( $page_url ) ); echo sprintf( '' . PHP_EOL, esc_attr( $og_type ) ); echo sprintf( '' . PHP_EOL, esc_attr( $page_title ) ); if ( $page_description ) { echo sprintf( '' . PHP_EOL, esc_attr( $page_description ) ); } if ( $image_url ) { echo sprintf( '' . PHP_EOL, esc_url( $image_url ) ); } /** * Media tags (Open Graph) */ if ( $media_url ) { if ( $is_video ) { echo sprintf( '' . PHP_EOL, esc_url( $media_url ) ); if ( $media_type ) { echo sprintf( '' . PHP_EOL, esc_attr( $media_type ) ); } if ( 0 === strpos( $media_url, 'https://' ) ) { echo sprintf( '' . PHP_EOL, esc_url( $media_url ) ); } echo '' . PHP_EOL; echo '' . PHP_EOL; } else { echo sprintf( '' . PHP_EOL, esc_url( $media_url ) ); if ( $media_type ) { echo sprintf( '' . PHP_EOL, esc_attr( $media_type ) ); } if ( 0 === strpos( $media_url, 'https://' ) ) { echo sprintf( '' . PHP_EOL, esc_url( $media_url ) ); } } } /** * Twitter Cards (summary only – no player yet) */ echo '' . PHP_EOL; if ( $twitter_username ) { if ( '@' !== $twitter_username[0] ) $twitter_username = '@' . $twitter_username; echo sprintf( '' . PHP_EOL, esc_attr( $twitter_username ) ); } echo sprintf( '' . PHP_EOL, esc_attr( $page_title ) ); if ( $page_description ) { echo sprintf( '' . PHP_EOL, esc_attr( $page_description ) ); } if ( $image_url ) { echo sprintf( '' . PHP_EOL, esc_url( $image_url ) ); } } /** * Change the current page title if applicable. * * @since 1.0.0 * @param string $title Current page title. * @param int $post_id The post ID. * @return string $title Filtered page title. */ public function the_title( $title, $id = 0 ) { if ( ! in_the_loop() || ! is_main_query() ) { return $title; } $post_id = get_the_ID(); if ( ! empty( $id ) ) { if ( $post_id !== $id ) { return $title; } } $pages_settings = aiopod_get_option( 'aiopod_pages_settings' ); // Change category page title if ( $post_id === (int) $pages_settings['category'] ) { if ( $slug = get_query_var( 'aiopod_category' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_categories' ) ) { $title = $term->name; } } } // Change tag page title if ( $post_id === (int) $pages_settings['tag'] ) { if ( $slug = get_query_var( 'aiopod_tag' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_tags' ) ) { $title = $term->name; } } } // Change participant page title if ( $post_id === (int) $pages_settings['participant'] ) { if ( $slug = get_query_var( 'aiopod_participant' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_participants' ) ) { $title = $term->name; } } } return $title; } /** * Filters whether a podcast-related post has a thumbnail. * * This ensures that AiO Podcast Studio posts can report a thumbnail * even when the image is provided via plugin-specific image settings, * while still respecting user options to hide featured images. * * @since 1.0.0 * @param bool $has_thumbnail Whether the post has a thumbnail. * @param int|WP_Post|null $post Post ID or WP_Post object. * @param int|string $thumbnail_id Post thumbnail ID or empty string. * @return bool Whether the post should be treated as having a thumbnail. */ public function has_post_thumbnail( $has_thumbnail, $post, $thumbnail_id ) { $post = get_post( $post ); // Bail early if the post object is invalid. if ( ! $post ) { return $has_thumbnail; } // Apply only on single podcast-related post types. if ( ! is_singular( array( 'aiopod_podcasts', 'aiopod_episodes' ) ) ) { return $has_thumbnail; } // Ensure this runs only for the main queried object. global $wp_the_query; if ( (int) $post->ID !== (int) $wp_the_query->get_queried_object_id() ) { return $has_thumbnail; } // Respect plugin image visibility settings. $appearance_settings = aiopod_get_option( 'aiopod_appearance_settings' ); if ( ! empty( $appearance_settings['hide_on_single_post_pages'] ) ) { return false; } // If a native featured image exists, honor WordPress behavior. if ( ! empty( $thumbnail_id ) ) { return $has_thumbnail; } // Fallback: Check for plugin-managed images. $image_data = aiopod_get_image( array( 'post_id' => $post->ID, 'size' => 'large', 'alt' => '' ) ); if ( ! empty( $image_data['src'] ) ) { return true; } return $has_thumbnail; } /** * Filters the podcast post thumbnail HTML. * * Allows AiO Podcast Studio posts to render thumbnails provided * via plugin-specific image settings when a native featured image * is not available, while still respecting user visibility options. * * @since 1.0.0 * @param string $html The post thumbnail HTML. * @param int $post_id The post ID. * @param int|string $post_thumbnail_id The post thumbnail ID or empty string. * @param string|array $size The post thumbnail size. * @param array $attr Image attributes. * @return string The filtered post thumbnail HTML. */ public function post_thumbnail_html( $html, $post_id, $post_thumbnail_id, $size, $attr ) { // Apply only on single podcast-related post types. if ( ! is_singular( array( 'aiopod_podcasts', 'aiopod_episodes' ) ) ) { return $html; } // Ensure this runs only for the main queried object. global $wp_the_query; if ( (int) $post_id !== (int) $wp_the_query->get_queried_object_id() ) { return $html; } // Respect plugin image visibility settings. $appearance_settings = aiopod_get_option( 'aiopod_appearance_settings' ); if ( ! empty( $appearance_settings['hide_on_single_post_pages'] ) ) { return ''; } // If a native featured image exists, honor WordPress behavior. if ( ! empty( $post_thumbnail_id ) ) { return $html; } $thumbnail_html = ''; /** * 1. Plugin-managed attachment image (preferred) */ $image_id = get_post_meta( $post_id, 'image_id', true ); if ( $image_id ) { $thumbnail_html = wp_get_attachment_image( $image_id, $size, false, $attr ); } /** * 2. External image URL fallback */ if ( ! $thumbnail_html ) { $image_url = get_post_meta( $post_id, 'image', true ); if ( $image_url ) { $attributes = wp_parse_args( (array) $attr, array( 'alt' => get_the_title( $post_id ) ) ); /** * Match WordPress image attribute filtering behavior. */ $attributes = apply_filters( 'wp_get_attachment_image_attributes', $attributes, null, $size ); $attributes = array_map( 'esc_attr', $attributes ); $thumbnail_html = sprintf( ' $value ) { $thumbnail_html .= sprintf( ' %s="%s"', $name, $value ); } $thumbnail_html .= ' />'; } } return $thumbnail_html ? $thumbnail_html : $html; } /** * Always use our custom page for AIOPOD categories & tags. * * @since 1.0.0 * @param string $url The term URL. * @param object $term The term object. * @param string $taxonomy The taxonomy slug. * @return string $url Filtered term URL. */ public function term_link( $url, $term, $taxonomy ) { if ( 'aiopod_categories' === $taxonomy ) { $url = aiopod_get_category_page_url( $term ); } if ( 'aiopod_tags' === $taxonomy ) { $url = aiopod_get_tag_page_url( $term ); } if ( 'aiopod_participants' === $taxonomy ) { $url = aiopod_get_participant_page_url( $term ); } return $url; } /** * Fix pagination on single podcast / episode / participant pages. * * @access private * @since 1.0.0 */ private function fix_single_pagination() { if ( ! is_singular( array( 'aiopod_podcasts', 'aiopod_episodes' ) ) ) { return; } global $wp_query; if ( ! $wp_query->is_main_query() ) { return; } $page = (int) $wp_query->get( 'page' ); if ( $page > 1 ) { // Convert `page` to `paged` $wp_query->set( 'page', 1 ); $wp_query->set( 'paged', $page ); } // Prevent canonical redirect breaking pagination remove_action( 'template_redirect', 'redirect_canonical' ); } /** * Redirect default CPT archives to custom archive pages. * * @access private * @since 1.0.0 */ private function redirect_custom_archives() { if ( ! is_post_type_archive() ) { return; } $pages_settings = aiopod_get_option( 'aiopod_pages_settings' ); // Explicit CPT → option key mapping (future-proof) $archive_map = array( 'aiopod_podcasts' => 'podcast_archive_page', 'aiopod_episodes' => 'episode_archive_page' ); $post_type = get_query_var( 'post_type' ); if ( empty( $archive_map[ $post_type ] ) ) { return; } $option_key = $archive_map[ $post_type ]; if ( empty( $pages_settings[ $option_key ] ) ) { return; } $page_id = (int) $pages_settings[ $option_key ]; $page_url = get_permalink( $page_id ); if ( ! $page_url ) { return; } $current_url = home_url( add_query_arg( null, null ) ); // Avoid redirect loop if ( untrailingslashit( $current_url ) === untrailingslashit( $page_url ) ) { return; } wp_safe_redirect( $page_url, 301 ); exit; } }