# jetpack/16.3/jetpack_vendor/automattic/jetpack-podcast/src/class-admin-page.php

Jetpack – WP Security, Backup, Speed, &amp; Growth, version 16.3. 385 lines.

- Page: https://pluginprobe.com/plugins/jetpack/16.3/code/jetpack_vendor/automattic/jetpack-podcast/src/class-admin-page.php
- Raw: https://pluginprobe.com/plugins/jetpack/16.3/raw/jetpack_vendor/automattic/jetpack-podcast/src/class-admin-page.php
- Modified: 2026-09-29T02:50:08+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/jetpack/16.3/code/jetpack_vendor/automattic/jetpack-podcast/src/class-admin-page.php#L10-L20`.

```php
<?php
/**
 * Registers the Jetpack Podcast wp-admin page and loads the wp-build dashboard.
 *
 * @package automattic/jetpack-podcast
 */

namespace Automattic\Jetpack\Podcast;

use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Status\Host;
use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id;

/**
 * Adds the "Jetpack > Podcast" wp-admin screen.
 */
class Admin_Page {

	const ADMIN_PAGE_SLUG = 'jetpack-podcast';

	/**
	 * Where the Podcast item used to sit in the Jetpack submenu on self-hosted.
	 *
	 * Unread since Podcast registers without a position; kept so consumers do not fatal.
	 *
	 * @deprecated 2.1.1
	 */
	const MENU_POSITION = 11;

	/**
	 * Slug emitted by `@wordpress/build`. wp-build's auto-generated enqueue
	 * callback only fires when `$screen->id` matches this value, so we alias
	 * the screen id around that check without changing the user-facing URL.
	 */
	const WP_BUILD_SLUG = 'jetpack-podcast-dashboard';

	/**
	 * Whether `init()` has already wired its hooks.
	 *
	 * @var bool
	 */
	private static $initialized = false;

	/**
	 * The screen ID alias_screen_id_for_wp_build() replaced, until it is restored.
	 *
	 * @var string|null
	 */
	private static $wp_build_original_screen_id = null;

	/**
	 * The dashboard screen hide_jitms_on_wp_build_dashboard() opts out of JITMs.
	 *
	 * @var string|null
	 */
	private static $jitm_opt_out_screen_id = null;

	/**
	 * Wire admin hooks. Idempotent.
	 */
	public static function init() {
		if ( self::$initialized ) {
			return;
		}
		self::$initialized = true;

		add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 );

		// On Simple/Atomic, wpcom-admin-menu.php builds the Jetpack menu at
		// priority 999999 and calls add_wp_admin_submenu() itself. Self-hosted
		// has no such file, so we register our own. Priority 999 queues the item
		// before Admin_Menu's priority-1000 callback.
		if ( ! ( new Host() )->is_wpcom_platform() ) {
			add_action( 'admin_menu', array( __CLASS__, 'add_wp_admin_submenu' ), 999 );
		}
	}

	/**
	 * Register the Podcast submenu under the Jetpack menu.
	 */
	public static function add_wp_admin_submenu() {
		// Prefer the wp-build render function once it's defined (by
		// maybe_load_wp_build() at admin_menu priority 1); fall back otherwise.
		$wp_build_render = 'jetpack_podcast_jetpack_podcast_dashboard_wp_admin_render_page';
		$callback        = function_exists( $wp_build_render ) ? $wp_build_render : array( __CLASS__, 'render' );

		if ( ( new Host() )->is_wpcom_platform() ) {
			$page_suffix = add_submenu_page(
				'jetpack',
				/** "Podcast" is a product name, do not translate. */
				'Podcast',
				'Podcast',
				'manage_options',
				self::ADMIN_PAGE_SLUG,
				$callback
			);
		} else {
			$page_suffix = Admin_Menu::add_menu(
				/** "Podcast" is a product name, do not translate. */
				'Podcast',
				'Podcast',
				'manage_options',
				self::ADMIN_PAGE_SLUG,
				$callback,
				null,
				// Podcast has no My Jetpack product class, so the module is the only gate available.
				array(
					'module' => 'podcast',
					'key'    => 'jetpack-podcast',
				)
			);
		}

		if ( $page_suffix ) {
			add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
			self::opt_out_of_jitms( $page_suffix );
		}
	}

	/**
	 * Wire admin-init actions once we know the Podcast page is loading.
	 */
	public static function admin_init() {
		// MediaUpload (cover-image-control) reads wp.media.view — only defined after this runs.
		add_action( 'admin_enqueue_scripts', 'wp_enqueue_media' );
		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_tracks_transport' ) );
	}

	/**
	 * Load the Tracks transport for the dashboard's client-side events.
	 *
	 * `jetpackAnalytics.tracks.recordEvent()` only pushes onto `window._tkq`,
	 * which stays an inert array until `w.js` loads and drains it. Nothing
	 * supplies that on Atomic or self-hosted, so without this the queue grows
	 * for the life of the page. Simple is skipped because stats.php already
	 * prints the same script on `admin_footer`, and loading it twice would
	 * re-drain a queue that has already been flushed.
	 */
	public static function enqueue_tracks_transport() {
		if ( ( new Host() )->is_wpcom_simple() ) {
			return;
		}

		wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
	}

	/**
	 * Hooked at admin_menu priority 1 so polyfills register before
	 * `wp_default_scripts` fires and the wp-build render function is defined
	 * before `add_wp_admin_submenu()` runs (priority 999 on self-hosted, 999999
	 * on Simple/Atomic).
	 */
	public static function maybe_load_wp_build() {
		if ( ! self::is_podcast_admin_request() ) {
			return;
		}

		self::load_wp_build_with_screen_alias();
		add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'inject_podcast_script_data' ) );
	}

	/**
	 * Add the podcast gate boolean to `window.JetpackScriptData`.
	 *
	 * Hooked from `maybe_load_wp_build()` so it only runs when the request is
	 * for the podcast admin page.
	 *
	 * @param array $data Script data being injected.
	 * @return array
	 */
	public static function inject_podcast_script_data( $data ) {
		if ( ! is_array( $data ) ) {
			$data = array();
		}

		$is_wpcom = ( new Host() )->is_wpcom_platform();

		if ( ! $is_wpcom && empty( $data['site']['wpcom']['blog_id'] ) ) {
			$blog_id = (int) Connection_Manager::get_site_id( true );
			if ( $blog_id > 0 ) {
				$data['site']['wpcom']['blog_id'] = $blog_id;
			}
		}

		// Self-hosted upsells the Growth plan; WordPress.com keeps Premium.
		// `product_slug` is fed straight to the checkout URL; `plan_name` is a
		// product name shown in the locked-preview copy (not translated).
		$data['podcast'] = array(
			'has_product_access'  => Podcast_Gate::has_product_access(),
			'is_connected'        => $is_wpcom || ( new Connection_Manager( 'jetpack' ) )->is_connected(),
			'show_url_hosts'      => Settings::SHOW_URL_HOSTS,
			'show_url_max_length' => Settings::SHOW_URL_MAX_LENGTH,
			'feed_limit_max'      => Settings::feed_limit_max(),
			'preload'             => rest_preload_api_request( array(), '/wpcom/v2/podcast/settings' ),
			'selected_category'   => self::get_selected_category(),
			'tracks_user_data'    => self::get_tracks_user_data(),
			'upgrade'             => array(
				'product_slug' => $is_wpcom ? 'premium' : 'jetpack_growth_yearly',
				'plan_name'    => $is_wpcom ? 'Premium' : 'Growth',
			),
		);

		return $data;
	}

	/**
	 * Connected-user identity for Tracks, so client events aren't anonymous on
	 * Atomic and self-hosted. Null on Simple, where stats.php already pushes
	 * `identifyUser` before our bundle runs.
	 *
	 * Deliberately narrower than `get_connected_user_tracks_identity()`, which
	 * also returns email, blogid and locale — none of which Tracks needs here.
	 *
	 * @return array{userid:mixed, username:mixed}|null
	 */
	private static function get_tracks_user_data() {
		if ( ! class_exists( 'Jetpack_Tracks_Client' ) ) {
			return null;
		}

		$identity = \Jetpack_Tracks_Client::get_connected_user_tracks_identity();
		if ( ! is_array( $identity ) || ! isset( $identity['userid'] ) || ! isset( $identity['username'] ) ) {
			return null;
		}

		return array(
			'userid'   => $identity['userid'],
			'username' => $identity['username'],
		);
	}

	/**
	 * The currently designated podcast category, injected so the settings
	 * picker can label its selected option on first paint instead of waiting on
	 * the client-side taxonomy→terms fetch. The full list still loads lazily.
	 *
	 * @return array{id:int, name:string}|null Null when no category is set.
	 */
	public static function get_selected_category() {
		$category_id = (int) get_option( 'podcasting_category_id', 0 );
		if ( $category_id <= 0 ) {
			return null;
		}

		$term = get_term( $category_id, 'category' );
		if ( ! $term instanceof \WP_Term ) {
			return null;
		}

		return array(
			'id'   => (int) $term->term_id,
			'name' => $term->name,
		);
	}

	/**
	 * The build artifact may be absent on a fresh checkout before
	 * `pnpm build` has run; in that case `add_wp_admin_submenu()` falls back
	 * to `render()` so the page still loads (just without the React app).
	 */
	private static function load_wp_build() {
		$build_index = dirname( __DIR__ ) . '/build/build.php';

		if ( ! file_exists( $build_index ) ) {
			return;
		}

		require_once $build_index;

		WP_Build_Polyfills::register(
			'jetpack-podcast',
			array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS )
		);
	}

	/**
	 * Load wp-build with the screen ID aliased across its generated enqueue check.
	 *
	 * @see WP_Build_Screen_Id::load_with_alias()
	 * @return void
	 */
	private static function load_wp_build_with_screen_alias() {
		// Fallback: an older wp-build-polyfills under the jetpack-autoloader may predate load_with_alias().
		if ( method_exists( WP_Build_Screen_Id::class, 'load_with_alias' ) ) {
			WP_Build_Screen_Id::load_with_alias(
				array( __CLASS__, 'alias_screen_id_for_wp_build' ),
				array( __CLASS__, 'restore_screen_id_after_wp_build' ),
				function () {
					self::load_wp_build();
				}
			);
			return;
		}

		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'alias_screen_id_for_wp_build' ) );
		self::load_wp_build();
		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'restore_screen_id_after_wp_build' ) );
	}

	/**
	 * Alias the current screen id to wp-build's expected slug.
	 *
	 * @since 2.1.3 Takes no argument; hooked on `admin_enqueue_scripts`.
	 */
	public static function alias_screen_id_for_wp_build() {
		$screen = get_current_screen();
		if ( ! $screen ) {
			return;
		}

		self::$wp_build_original_screen_id = $screen->id;
		$screen->id                        = self::WP_BUILD_SLUG;
	}

	/**
	 * Undo alias_screen_id_for_wp_build(), so code after the generated check sees the real screen ID.
	 *
	 * @since 2.1.3
	 */
	public static function restore_screen_id_after_wp_build() {
		$screen = get_current_screen();
		if ( ! $screen || null === self::$wp_build_original_screen_id ) {
			return;
		}

		$screen->id                        = self::$wp_build_original_screen_id;
		self::$wp_build_original_screen_id = null;
	}

	/**
	 * Opt the dashboard's screen out of JITMs.
	 *
	 * @param string $screen_id The hook suffix the page was registered under, which is its screen ID.
	 */
	private static function opt_out_of_jitms( $screen_id ) {
		self::$jitm_opt_out_screen_id = $screen_id;
		add_filter( 'jetpack_display_jitms_on_screen', array( __CLASS__, 'hide_jitms_on_wp_build_dashboard' ), 10, 2 );
	}

	/**
	 * Keep JITMs off the wp-build dashboard, which has no `#jp-admin-notices` to show them in.
	 *
	 * Fetching a JITM records a view, so one the page hides would still be counted.
	 *
	 * @since 2.1.3
	 *
	 * @param bool   $show      Whether to show JITMs on the screen.
	 * @param string $screen_id The screen ID.
	 * @return bool
	 */
	public static function hide_jitms_on_wp_build_dashboard( $show, $screen_id ) {
		if ( null !== self::$jitm_opt_out_screen_id && self::$jitm_opt_out_screen_id === $screen_id ) {
			return false;
		}

		return $show;
	}

	/**
	 * Fallback render used when the wp-build artifact is missing.
	 */
	public static function render() {
		?>
		<div class="wrap">
			<h1>Podcast</h1>
		</div>
		<?php
	}

	/**
	 * Whether the current request targets the Podcast admin page.
	 */
	private static function is_podcast_admin_request() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! is_admin() || ! isset( $_GET['page'] ) ) {
			return false;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		return self::ADMIN_PAGE_SLUG === sanitize_text_field( wp_unslash( $_GET['page'] ) );
	}
}

```
