$value ) { if ( 'vars' === $key && is_array( $value ) ) { foreach ( $value as $vk => $vv ) { $payload['config']['tokens']['vars'][ $vk ] = is_scalar( $vv ) ? streamsera_sanitize_css_value( (string) $vv ) : ''; } } elseif ( is_string( $value ) ) { $payload['config']['tokens'][ $key ] = streamsera_sanitize_css_value( $value ); } } return $payload; } /** * Register (not enqueue) the frontend bundle (wp_enqueue_scripts / enqueue_block_editor_assets). */ public function register_assets() { self::register_handles(); } /** * Enqueue the player scripts and styles (core + add-ons) into the block editor's content iframe. */ public function editor_frame_assets() { if ( ! is_admin() ) { return; } self::register_standalone(); $handles = self::frontend_handles(); foreach ( $handles['scripts'] as $handle ) { if ( wp_script_is( $handle, 'registered' ) ) { wp_enqueue_script( $handle ); } } foreach ( $handles['styles'] as $handle ) { if ( wp_style_is( $handle, 'registered' ) ) { wp_enqueue_style( $handle ); } } } /** * Register the core frontend handles once. * * @return void */ public static function register_handles() { if ( wp_script_is( 'streamsera-frontend', 'registered' ) ) { return; } $asset_file = STREAMSERA_PATH . 'build/frontend.asset.php'; if ( ! file_exists( $asset_file ) ) { return; } $asset = require $asset_file; wp_register_script( 'streamsera-frontend', STREAMSERA_URL . 'build/frontend.js', $asset['dependencies'], $asset['version'], true ); wp_set_script_translations( 'streamsera-frontend', 'streamsera', STREAMSERA_PATH . 'languages' ); wp_register_style( 'streamsera-frontend', STREAMSERA_URL . 'build/frontend.css', array(), $asset['version'] ); wp_style_add_data( 'streamsera-frontend', 'rtl', 'replace' ); // build/frontend-rtl.css on right-to-left sites. wp_add_inline_script( 'streamsera-frontend', 'window.StreamSeraConfig = ' . wp_json_encode( array( 'restUrl' => esc_url_raw( rest_url( 'streamsera/v1' ) ), 'version' => STREAMSERA_VERSION, 'defaultVolume' => (int) streamsera_get_setting( 'default_volume', 80 ), 'keyboardShortcuts' => (bool) streamsera_get_setting( 'keyboard_shortcuts', true ), // Site-wide player behaviour (moved out of the per-player builder). 'pollInterval' => max( 5, (int) streamsera_get_setting( 'cache_ttl', 15 ) ), // Logged-in previews (admin builder, block editor, the ?streamsera_preview page) show draft // stations, whose now playing needs the user's REST nonce. Never on public pages, // which may be cached. 'nonce' => self::is_private_view() ? wp_create_nonce( 'wp_rest' ) : '', ) ) . ';', 'before' ); } /** * Whether this request is a logged-in editing view (wp-admin, or the player preview page). * * @return bool */ private static function is_private_view() { if ( ! is_user_logged_in() || ! current_user_can( 'edit_posts' ) ) { return false; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only check of which page this is. return is_admin() || isset( $_GET[ Preview::QUERY ] ); } /** * Script/style handles that go with the player (core + add-ons). * * @return array{scripts:string[],styles:string[]} */ public static function frontend_handles() { /** * Script/style handles enqueued with the player (add-ons append theirs). * * @param array $handles { scripts: string[], styles: string[] }. */ $handles = (array) apply_filters( 'streamsera_frontend_handles', array( 'scripts' => array( 'streamsera-frontend' ), 'styles' => array( 'streamsera-frontend' ), ) ); return array( 'scripts' => array_values( array_unique( array_map( 'strval', isset( $handles['scripts'] ) ? (array) $handles['scripts'] : array() ) ) ), 'styles' => array_values( array_unique( array_map( 'strval', isset( $handles['styles'] ) ? (array) $handles['styles'] : array() ) ) ), ); } /** * Enqueue the frontend bundle. */ public static function enqueue() { $handles = self::frontend_handles(); foreach ( $handles['scripts'] as $handle ) { wp_enqueue_script( $handle ); } foreach ( $handles['styles'] as $handle ) { wp_enqueue_style( $handle ); } } /** * Register the player handles for a stand-alone page (pop-out, preview, listen / station pages, * overlays). Those documents are served on `template_redirect` and print their own , so * `wp_enqueue_scripts` never runs: register exactly the core handles here and let add-ons * register theirs on `streamsera_register_standalone_assets`. * * @return void */ public static function register_standalone() { self::register_handles(); if ( ! did_action( 'streamsera_register_standalone_assets' ) ) { /** * Fires when a stand-alone StreamSera page registers its assets (no `wp_enqueue_scripts` there). * Add-ons register the handles they add through `streamsera_frontend_handles` here. */ do_action( 'streamsera_register_standalone_assets' ); } } /** * Print styles in the of a stand-alone page (listen page, station pages, preview, overlay): * the given registered handles plus the page's own stylesheet, `assets/css/{page}.css`. * * @param string $page Page stylesheet name in assets/css ('' for none), e.g. 'listen'. * @param string[]|null $handles Registered handles to print first (null: the player's style handles). * @return void */ public static function print_standalone_styles( $page, $handles = null ) { $handles = null === $handles ? self::frontend_handles()['styles'] : (array) $handles; $handles = array_values( array_filter( $handles, static fn( $h ) => wp_style_is( $h, 'registered' ) ) ); $page = sanitize_key( (string) $page ); if ( '' !== $page && file_exists( STREAMSERA_PATH . 'assets/css/' . $page . '.css' ) ) { $handle = 'streamsera-page-' . $page; wp_register_style( $handle, STREAMSERA_URL . 'assets/css/' . $page . '.css', array(), STREAMSERA_VERSION ); $handles[] = $handle; } wp_print_styles( $handles ); } /** * Print scripts at the end of a stand-alone page: the given registered handles plus the page's * own script, `assets/js/{page}.js`. * * @param string $page Page script name in assets/js ('' for none), e.g. 'pages'. * @param string[]|null $handles Registered handles to print first (null: the player's script handles). * @return void */ public static function print_standalone_scripts( $page, $handles = null ) { $handles = null === $handles ? self::frontend_handles()['scripts'] : (array) $handles; $handles = array_values( array_filter( $handles, static fn( $h ) => wp_script_is( $h, 'registered' ) ) ); $page = sanitize_key( (string) $page ); if ( '' !== $page && file_exists( STREAMSERA_PATH . 'assets/js/' . $page . '.js' ) ) { $handle = 'streamsera-page-' . $page; wp_register_script( $handle, STREAMSERA_URL . 'assets/js/' . $page . '.js', array(), STREAMSERA_VERSION, true ); $handles[] = $handle; } wp_print_scripts( $handles ); } /** * Render a player island markup. Config + station are inlined so the * player paints without an extra request; now-playing polls afterwards. * * @param int $player_id Player ID. * @param array $overrides Attribute overrides (layout, skin, ...). * @param array $extra Render context handed to the player as `extra` (`className`, `oneRow`, add-on data). * @return string */ public static function render_player( $player_id, $overrides = array(), $extra = array() ) { // Admin preview may render a draft player, but only for users who may edit that player. $config = Player::get_config( $player_id, ! empty( $extra['preview'] ) && current_user_can( 'edit_post', (int) $player_id ) ); if ( ! $config ) { return current_user_can( 'edit_posts' ) ? '
' . esc_html__( 'StreamSera: player not found.', 'streamsera' ) . '
' : ''; } foreach ( array( 'layout', 'skin', 'theme', 'station' ) as $key ) { if ( ! empty( $overrides[ $key ] ) ) { $config[ $key ] = $overrides[ $key ]; } } // A skin named in the shortcode/block replaces the whole look, not just the class: the saved // colours and custom skin would otherwise paint over it (add-ons reset their own look-related // config on `streamsera_skin_override_config`). if ( ! empty( $overrides['skin'] ) ) { $skins = streamsera_skins(); if ( isset( $skins[ $overrides['skin'] ] ) ) { $skin = $skins[ $overrides['skin'] ]; $config['customSkin'] = ''; $config['customColors'] = false; $config['colorOverrides'] = array(); $config['tokens'] = array_merge( is_array( $config['tokens'] ) ? $config['tokens'] : array(), array( 'background' => '', 'text' => '', 'vars' => isset( $skin['vars'] ) ? $skin['vars'] : array() ), isset( $skin['tokens'] ) ? $skin['tokens'] : array() ); /** * Filter the config after a skin override from a shortcode/block was applied. * * @param array $config Config. * @param string $skin Skin id. */ $config = apply_filters( 'streamsera_skin_override_config', $config, $overrides['skin'] ); } } $config = streamsera_sanitize_player_config( $config ); $config['id'] = $player_id; $config = Player::with_live_station( $config ); $station = Station::to_public( $config['station'] ); // The first station was deleted or unpublished: play the next one on the list instead. while ( ! $station && ! empty( $config['stations'] ) ) { $config['station'] = (int) array_shift( $config['stations'] ); $station = Station::to_public( $config['station'] ); } if ( ! $station ) { return current_user_can( 'edit_posts' ) ? '
' . esc_html__( 'StreamSera: this player has no published station assigned.', 'streamsera' ) . '
' : ''; } self::enqueue(); $stations = array(); foreach ( (array) $config['stations'] as $sid ) { $extra_station = Station::to_public( $sid ); if ( $extra_station ) { $stations[] = $extra_station; } } $payload = array( 'config' => $config, 'station' => Station::without_contact( $station ), 'stations' => array_map( array( Station::class, 'without_contact' ), $stations ), 'extra' => $extra, ); /** * Filter the payload handed to the player island (modules attach playlists, schedules, ...). * * @param array $payload Payload. * @param int $player_id Player. */ $payload = apply_filters( 'streamsera_player_payload', $payload, $player_id ); /** * Extra `data-*` attributes on the player root (add-ons mark roots their scripts look for). * * @param array $attrs Attribute name (without `data-`) => value. * @param int $player_id Player. * @param array $extra Render context (`listen`, `embed`, `className`, `oneRow`, add-on keys …). */ $attrs = (array) apply_filters( 'streamsera_player_root_attributes', array(), $player_id, $extra ); $html = ''; foreach ( $attrs as $name => $value ) { $name = sanitize_key( (string) $name ); if ( '' !== $name && is_scalar( $value ) ) { $html .= ' data-' . $name . '="' . esc_attr( (string) $value ) . '"'; } } return sprintf( '
', (int) $player_id, esc_attr( $config['layout'] ), esc_attr( (string) wp_json_encode( $payload ) ), $html ); } }