__( 'Setup', 'streamsera' ), 'route' => 'setup', 'cap' => 'manage_options', 'order' => 200, ); return $pages; } /** * Keep the setup page registered but out of the sidebar. This runs on admin_head, after WordPress * has checked access to the current page, so opening or reloading admin.php?page=streamsera-setup * directly still works. */ public function hide_setup_menu() { remove_submenu_page( \StreamSera\Admin\Menu::SLUG, 'streamsera-setup' ); } /** * Templates + saved presets for the admin app. * * @param array $data Data. * @return array */ public function admin_data( $data ) { $data['templates'] = array_values( Templates::all() ); $data['stylePresets'] = self::presets(); $data['setupCompleted'] = (bool) streamsera_get_setting( 'setup_completed', false ); return $data; } /* ------------------------------------------------------------------ REST */ /** * Routes: theme palette + style presets CRUD. */ public function routes() { register_rest_route( self::NS, '/theme-palette', array( 'methods' => 'GET', 'callback' => array( $this, 'theme_palette' ), 'permission_callback' => array( $this, 'can_read_appearance' ), ) ); register_rest_route( self::NS, '/presets', array( array( 'methods' => 'GET', 'callback' => static function () { return new WP_REST_Response( self::presets() ); }, 'permission_callback' => array( $this, 'can_read_appearance' ), ), array( 'methods' => 'POST', 'callback' => array( $this, 'create_preset' ), 'permission_callback' => array( $this, 'can_write_presets' ), 'args' => array( 'label' => array( 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ), ), ), ) ); register_rest_route( self::NS, '/presets/(?P[a-z0-9\-_]+)', array( array( 'methods' => 'PUT', 'callback' => array( $this, 'update_preset' ), 'permission_callback' => array( $this, 'can_write_presets' ), 'args' => array( 'label' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ), ), ), array( 'methods' => 'DELETE', 'callback' => array( $this, 'delete_preset' ), 'permission_callback' => array( $this, 'can_write_presets' ), ), ) ); } /** * REST permission: read the theme palette and style presets (anyone who can use the builder). * * @return bool */ public function can_read_appearance() { /** This filter is documented in includes/Admin/Menu.php */ return current_user_can( apply_filters( 'streamsera_required_cap', 'edit_posts', 'appearance' ) ); } /** * REST permission: create, update or delete shared style presets. A site-wide appearance * change, so editors and up (the role model may remap it). * * @return bool */ public function can_write_presets() { /** This filter is documented in includes/Admin/Menu.php */ return current_user_can( apply_filters( 'streamsera_required_cap', 'edit_others_posts', 'appearance' ) ); } /** * Colors + typography from the active (block) theme so the builder can "match my theme". * * @return WP_REST_Response */ public function theme_palette() { $out = array( 'primary' => '', 'accent' => '', 'background' => '', 'text' => '', 'fontFamily' => '', 'palette' => array(), 'fonts' => array(), 'source' => 'none', ); if ( function_exists( 'wp_get_global_settings' ) ) { $settings = wp_get_global_settings(); $palette = array(); foreach ( array( 'theme', 'custom', 'default' ) as $origin ) { if ( ! empty( $settings['color']['palette'][ $origin ] ) ) { foreach ( (array) $settings['color']['palette'][ $origin ] as $c ) { if ( ! empty( $c['color'] ) ) { $palette[] = array( 'slug' => isset( $c['slug'] ) ? sanitize_key( $c['slug'] ) : '', 'name' => isset( $c['name'] ) ? sanitize_text_field( $c['name'] ) : '', 'color' => sanitize_text_field( $c['color'] ), ); } } if ( $palette ) { $out['source'] = 'theme' === $origin ? 'theme.json' : $origin; break; } } } $out['palette'] = $palette; $by_slug = array(); foreach ( $palette as $c ) { $by_slug[ $c['slug'] ] = $c['color']; } $pick = static function ( $keys ) use ( $by_slug, $palette ) { foreach ( $keys as $k ) { foreach ( $by_slug as $slug => $color ) { if ( false !== strpos( $slug, $k ) ) { return $color; } } } return ''; }; $out['primary'] = $pick( array( 'primary', 'accent', 'brand', 'vivid' ) ); $out['accent'] = $pick( array( 'secondary', 'accent-2', 'tertiary', 'accent' ) ); $out['background'] = $pick( array( 'base', 'background', 'white' ) ); $out['text'] = $pick( array( 'contrast', 'foreground', 'text', 'black' ) ); if ( ! $out['primary'] && ! empty( $palette[0]['color'] ) ) { $out['primary'] = $palette[0]['color']; } if ( ! $out['accent'] && ! empty( $palette[1]['color'] ) ) { $out['accent'] = $palette[1]['color']; } $styles = function_exists( 'wp_get_global_styles' ) ? wp_get_global_styles() : array(); if ( ! empty( $styles['typography']['fontFamily'] ) ) { $out['fontFamily'] = self::resolve_preset( (string) $styles['typography']['fontFamily'], $settings ); } if ( ! empty( $styles['color']['background'] ) && ! $out['background'] ) { $out['background'] = self::resolve_preset( (string) $styles['color']['background'], $settings ); } if ( ! empty( $styles['color']['text'] ) && ! $out['text'] ) { $out['text'] = self::resolve_preset( (string) $styles['color']['text'], $settings ); } foreach ( array( 'theme', 'custom', 'default' ) as $origin ) { if ( ! empty( $settings['typography']['fontFamilies'][ $origin ] ) ) { foreach ( (array) $settings['typography']['fontFamilies'][ $origin ] as $f ) { if ( ! empty( $f['fontFamily'] ) ) { $out['fonts'][] = array( 'slug' => isset( $f['slug'] ) ? sanitize_key( $f['slug'] ) : '', 'name' => isset( $f['name'] ) ? sanitize_text_field( $f['name'] ) : '', 'fontFamily' => sanitize_text_field( $f['fontFamily'] ), ); } } } } } if ( 'none' === $out['source'] ) { // Classic themes: fall back to the customizer header text color / background. $bg = get_background_color(); if ( $bg ) { $out['background'] = '#' . ltrim( $bg, '#' ); } $out['source'] = 'classic'; } return new WP_REST_Response( $out ); } /** * Resolve var:preset|color|slug references to concrete values. * * @param string $value Value. * @param array $settings Global settings. * @return string */ private static function resolve_preset( $value, $settings ) { if ( preg_match( '/var:preset\|(color|font-family)\|([a-z0-9\-]+)/i', $value, $m ) || preg_match( '/var\(--wp--preset--(color|font-family)--([a-z0-9\-]+)\)/i', $value, $m ) ) { $type = 'color' === $m[1] ? array( 'color', 'palette', 'color' ) : array( 'typography', 'fontFamilies', 'fontFamily' ); foreach ( array( 'theme', 'custom', 'default' ) as $origin ) { foreach ( (array) ( isset( $settings[ $type[0] ][ $type[1] ][ $origin ] ) ? $settings[ $type[0] ][ $type[1] ][ $origin ] : array() ) as $item ) { if ( isset( $item['slug'] ) && $item['slug'] === $m[2] && ! empty( $item[ $type[2] ] ) ) { return sanitize_text_field( $item[ $type[2] ] ); } } } return ''; } return sanitize_text_field( $value ); } /* ------------------------------------------------------------------ presets */ /** * Saved style presets. * * @return array */ public static function presets() { $list = get_option( self::PRESETS, array() ); $list = is_array( $list ) ? array_values( $list ) : array(); /** * Filter the saved style presets (add-ons normalise the extra keys they store on a preset). * * @param array $list Presets. */ $list = (array) apply_filters( 'streamsera_style_presets', $list ); return $list; } /** * Sanitize an incoming preset. * * @param array $input Input. * @param string $id Id. * @return array */ private static function sanitize_preset( $input, $id ) { $cfg = streamsera_sanitize_player_config( array_merge( streamsera_default_player_config(), is_array( $input ) ? $input : array() ) ); $preset = array( 'id' => $id, 'label' => isset( $input['label'] ) && '' !== trim( sanitize_text_field( $input['label'] ) ) ? mb_substr( trim( sanitize_text_field( $input['label'] ) ), 0, 60 ) : __( 'Preset', 'streamsera' ), 'skin' => $cfg['skin'], 'theme' => $cfg['theme'], 'tokens' => $cfg['tokens'], ); /** * Filter a preset before it is stored (add-ons sanitise and attach their own style keys). * * @param array $preset Sanitised preset. * @param array $input Raw input. */ return (array) apply_filters( 'streamsera_sanitize_preset', $preset, is_array( $input ) ? $input : array() ); } /** * Create. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function create_preset( WP_REST_Request $request ) { $list = self::presets(); if ( count( $list ) >= self::MAX_PRESET ) { return new WP_Error( 'streamsera_presets_full', __( 'You can store up to 50 presets. Delete one first.', 'streamsera' ), array( 'status' => 400 ) ); } $input = (array) $request->get_json_params(); if ( '' === trim( sanitize_text_field( isset( $input['label'] ) && is_scalar( $input['label'] ) ? (string) $input['label'] : '' ) ) ) { return new WP_Error( 'streamsera_preset_label', __( 'Give the preset a name.', 'streamsera' ), array( 'status' => 400 ) ); } $preset = self::sanitize_preset( $input, 'p' . wp_generate_password( 8, false ) ); $list[] = $preset; update_option( self::PRESETS, $list, false ); return new WP_REST_Response( $preset, 201 ); } /** * Update. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function update_preset( WP_REST_Request $request ) { $id = preg_replace( '/[^A-Za-z0-9_\-]/', '', streamsera_url_param( $request ) ); // Ids are mixed case. $list = self::presets(); foreach ( $list as $i => $p ) { if ( $p['id'] === $id ) { $list[ $i ] = self::sanitize_preset( (array) $request->get_json_params(), $id ); update_option( self::PRESETS, $list, false ); return new WP_REST_Response( $list[ $i ] ); } } return new WP_Error( 'streamsera_not_found', __( 'Preset not found.', 'streamsera' ), array( 'status' => 404 ) ); } /** * Delete. * * @param WP_REST_Request $request Request. * @return WP_REST_Response */ public function delete_preset( WP_REST_Request $request ) { $id = preg_replace( '/[^A-Za-z0-9_\-]/', '', streamsera_url_param( $request ) ); // Ids are mixed case. $list = array_values( array_filter( self::presets(), static function ( $p ) use ( $id ) { return $p['id'] !== $id; } ) ); update_option( self::PRESETS, $list, false ); return new WP_REST_Response( array( 'deleted' => true ) ); } /* ------------------------------------------------------------------ frontend */ /** * Branding flag + skin vars on the payload. * * @param array $payload Payload. * @param int $player_id Player. * @return array */ public function payload( $payload, $player_id ) { /** * Whether the small "Powered by StreamSera" link renders on hero layouts (and add-on layouts that show it). It is opt-in * (Settings → Player defaults → Credit link) and never shows without the site owner's consent. * * @param bool $show Default: the "Credit link" setting (off). * @param int $player_id Player. */ $payload['extra']['branding'] = (bool) apply_filters( 'streamsera_show_branding', (bool) streamsera_get_setting( 'show_branding', false ), $player_id ); // Skins can ship extra CSS vars; merge them under the player's own tokens.vars. $skins = streamsera_skins(); $skin = isset( $payload['config']['skin'] ) ? $payload['config']['skin'] : ''; if ( isset( $skins[ $skin ]['vars'] ) && is_array( $skins[ $skin ]['vars'] ) ) { $payload['config']['tokens']['vars'] = array_merge( array_map( 'streamsera_sanitize_css_value', $skins[ $skin ]['vars'] ), isset( $payload['config']['tokens']['vars'] ) ? (array) $payload['config']['tokens']['vars'] : array() ); } return $payload; } }