label. * * @return array */ public static function pages() { $pages = array( 'streamsera' => array( 'label' => __( 'Dashboard', 'streamsera' ), 'route' => 'dashboard', 'cap' => 'edit_posts', 'order' => 10, ), 'streamsera-stations' => array( 'label' => __( 'Stations', 'streamsera' ), 'route' => 'stations', 'cap' => 'edit_posts', 'order' => 20, ), 'streamsera-players' => array( 'label' => __( 'Players', 'streamsera' ), 'route' => 'players', 'cap' => 'edit_posts', 'order' => 30, ), 'streamsera-settings' => array( 'label' => __( 'Settings', 'streamsera' ), 'route' => 'settings', 'cap' => 'manage_options', 'order' => 100, ), ); // Analytics is a Pro feature: the free plugin lists it with a locked preview (no data is collected). if ( ! apply_filters( 'streamsera_pro_active', defined( 'STREAMSERA_PRO_VERSION' ) ) && apply_filters( 'streamsera_upgrade_url', 'https://wpsera.com/streamsera/pricing/' ) ) { $pages['streamsera-analytics'] = array( 'label' => __( 'Analytics', 'streamsera' ), 'route' => 'analytics', 'cap' => 'edit_others_posts', 'order' => 50, ); } /** * Register admin pages rendered by the React app. Key = submenu slug (must start with streamsera-), * value = { label, route (first hash segment), cap, order }. The JS side registers the matching page component. * * @param array $pages Pages. */ $pages = (array) apply_filters( 'streamsera_admin_pages', $pages ); uasort( $pages, static function ( $a, $b ) { return ( isset( $a['order'] ) ? $a['order'] : 50 ) <=> ( isset( $b['order'] ) ? $b['order'] : 50 ); } ); return $pages; } /** * Admin pages the current user may open: slug => { label, route }. * * @return array */ public static function allowed_pages() { $out = array(); foreach ( self::pages() as $slug => $page ) { /** This filter is documented in includes/Admin/Menu.php */ $cap = apply_filters( 'streamsera_required_cap', isset( $page['cap'] ) ? $page['cap'] : 'edit_posts', 'page:' . $slug ); if ( current_user_can( $cap ) ) { $out[ $slug ] = array( 'label' => $page['label'], 'route' => $page['route'], ); } } return $out; } /** * Hook in. */ public function register() { add_action( 'admin_menu', array( $this, 'menu' ) ); add_action( 'admin_init', array( $this, 'activation_redirect' ), 1 ); add_action( 'admin_menu', array( $this, 'group_menu' ), PHP_INT_MAX ); add_action( 'admin_enqueue_scripts', array( $this, 'menu_styles' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'assets' ) ); add_filter( 'plugin_action_links_' . STREAMSERA_BASENAME, array( $this, 'action_links' ) ); add_filter( 'admin_body_class', array( $this, 'body_class' ) ); add_filter( 'admin_footer_text', array( $this, 'footer_text' ), 99 ); add_filter( 'update_footer', array( $this, 'footer_text' ), 99 ); } /** * Blank the WordPress footer ("Thank you for creating with WordPress" / version) on our screens. * * @param string $text Footer text. * @return string */ public function footer_text( $text ) { return $this->is_our_page() ? '' : $text; } /** * Menu entries. */ public function menu() { // Menu icon: the StreamSera broadcast mark, as the base64 SVG data URI WordPress expects (a single // filled path, so the admin colour scheme can tint it). $icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI2E3YWFhZCIgZD0iTTYuOTggMTcuMDJBNy4xMCA3LjEwIDAgMCAxIDYuOTggNi45OEExLjEgMS4xIDAgMCAxIDguNTQgOC41NEE0LjkwIDQuOTAgMCAwIDAgOC41NCAxNS40NkExLjEgMS4xIDAgMCAxIDYuOTggMTcuMDJaTTE3LjAyIDYuOThBNy4xMCA3LjEwIDAgMCAxIDE3LjAyIDE3LjAyQTEuMSAxLjEgMCAwIDEgMTUuNDYgMTUuNDZBNC45MCA0LjkwIDAgMCAwIDE1LjQ2IDguNTRBMS4xIDEuMSAwIDAgMSAxNy4wMiA2Ljk4Wk00LjE1IDE5Ljg1QTExLjEwIDExLjEwIDAgMCAxIDQuMTUgNC4xNUExLjEgMS4xIDAgMCAxIDUuNzEgNS43MUE4LjkwIDguOTAgMCAwIDAgNS43MSAxOC4yOUExLjEgMS4xIDAgMCAxIDQuMTUgMTkuODVaTTE5Ljg1IDQuMTVBMTEuMTAgMTEuMTAgMCAwIDEgMTkuODUgMTkuODVBMS4xIDEuMSAwIDAgMSAxOC4yOSAxOC4yOUE4LjkwIDguOTAgMCAwIDAgMTguMjkgNS43MUExLjEgMS4xIDAgMCAxIDE5Ljg1IDQuMTVaTTEyIDkuM2EyLjcgMi43IDAgMSAxIDAgNS40YTIuNyAyLjcgMCAxIDEgMC01LjRaIi8+PC9zdmc+'; /** * Filter the capability required for a StreamSera context (add-ons map custom roles here). * * @param string $cap Default capability. * @param string $context 'menu', 'page:', 'rest_read', 'rest_manage'. */ $menu_cap = apply_filters( 'streamsera_required_cap', 'edit_posts', 'menu' ); add_menu_page( __( 'StreamSera', 'streamsera' ), __( 'StreamSera', 'streamsera' ), $menu_cap, self::SLUG, array( $this, 'render' ), $icon, 58 ); foreach ( self::pages() as $slug => $page ) { $cap = apply_filters( 'streamsera_required_cap', isset( $page['cap'] ) ? $page['cap'] : 'edit_posts', 'page:' . $slug ); add_submenu_page( self::SLUG, $page['label'], $page['label'], $cap, $slug, array( $this, 'render' ) ); } /** * Let add-ons register extra submenu pages. * * @param string $parent Parent slug. */ do_action( 'streamsera_admin_menu', self::SLUG ); } /** Option holding the pending after-activation redirect ('core' or 'pro'). */ const REDIRECT_OPTION = 'streamsera_activation_redirect'; /** * Remember to open StreamSera on the next admin page load after activation. Skipped for bulk, * network-wide and WP-CLI activations. A Pro activation ('pro') wins over the free one ('core'). * * @param string $source 'core' or 'pro'. * @param bool $network_wide Network-wide activation. */ public static function queue_activation_redirect( $source, $network_wide = false ) { // phpcs:disable WordPress.Security.NonceVerification -- reading the action WordPress already verified. $action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : ''; $action2 = isset( $_REQUEST['action2'] ) ? sanitize_key( wp_unslash( $_REQUEST['action2'] ) ) : ''; $checked = isset( $_POST['checked'] ) && is_array( $_POST['checked'] ) ? count( $_POST['checked'] ) : 0; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only counted. // phpcs:enable WordPress.Security.NonceVerification $bulk = 'activate-selected' === $action || 'activate-selected' === $action2 || $checked > 1; if ( $network_wide || $bulk || ( defined( 'WP_CLI' ) && WP_CLI ) ) { return; } if ( 'pro' !== get_option( self::REDIRECT_OPTION ) ) { update_option( self::REDIRECT_OPTION, 'pro' === $source ? 'pro' : 'core', false ); } } /** * After activation: the free plugin opens the setup wizard (or the Dashboard once setup is done); * Pro always opens the Dashboard. */ public function activation_redirect() { $source = get_option( self::REDIRECT_OPTION ); if ( ! $source ) { return; } if ( wp_doing_ajax() || wp_doing_cron() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || is_network_admin() ) { return; } delete_option( self::REDIRECT_OPTION ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only checks for a bulk activation. if ( isset( $_GET['activate-multi'] ) || ! current_user_can( 'manage_options' ) ) { return; } $page = ( 'pro' !== $source && ! streamsera_get_setting( 'setup_completed', false ) ) ? 'streamsera-setup' : self::SLUG; /** * Filter where StreamSera opens after activation. * * @param string $url Admin URL. * @param string $source 'core' or 'pro'. */ $url = (string) apply_filters( 'streamsera_activation_redirect', admin_url( 'admin.php?page=' . $page ), $source ); wp_safe_redirect( $url ); exit; } /** * Group of each submenu page (Overview · Content · Audience · Setup). A divider is drawn above the * first item of each group. Pages not listed join the group of the item above them. * * @return array slug => group. */ public static function menu_groups() { /** * Filter the admin submenu groups. * * @param array $groups slug => group key. */ return (array) apply_filters( 'streamsera_admin_menu_groups', array( 'streamsera' => 'overview', 'streamsera-stations' => 'content', 'streamsera-players' => 'content', 'streamsera-analytics' => 'audience', 'streamsera-settings' => 'setup', ) ); } /** * Order the submenu by group and mark the first item of each group (runs after add-ons and the * license SDK have added their items). */ public function group_menu() { global $submenu; if ( empty( $submenu[ self::SLUG ] ) || ! is_array( $submenu[ self::SLUG ] ) ) { return; } $groups = self::menu_groups(); $order = array_flip( array_values( array_unique( array_values( $groups ) ) ) ); $items = array_values( $submenu[ self::SLUG ] ); $keyed = array(); $group = null; foreach ( $items as $i => $item ) { $slug = isset( $item[2] ) ? (string) $item[2] : ''; $group = isset( $groups[ $slug ] ) ? $groups[ $slug ] : ( null === $group ? 'overview' : $group ); $keyed[] = array( isset( $order[ $group ] ) ? $order[ $group ] : count( $order ), $i, $group, $item ); } usort( $keyed, static function ( $a, $b ) { return $a[0] === $b[0] ? $a[1] - $b[1] : $a[0] - $b[0]; } ); $out = array(); $prev = null; foreach ( $keyed as $row ) { $item = $row[3]; if ( null !== $prev && $row[2] !== $prev ) { $item[4] = trim( ( isset( $item[4] ) ? $item[4] . ' ' : '' ) . 'ss-menu-sep' ); } $prev = $row[2]; $out[] = $item; } $submenu[ self::SLUG ] = $out; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- reordering our own submenu. } /** * Dividers between submenu groups (every admin screen shows the menu). */ public function menu_styles() { $css = '#adminmenu #toplevel_page_streamsera .wp-submenu li.ss-menu-sep{margin-top:6px;padding-top:6px;border-top:1px solid rgba(255,255,255,.12)}' . '#adminmenu #toplevel_page_streamsera .wp-submenu span.fs-submenu-item.fs-sub:before{content:none;padding:0}'; wp_add_inline_style( 'admin-menu', $css ); } /** * Plugin row links. * * @param array $links Links. * @return array */ public function action_links( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'streamsera' ) . '', '' . esc_html__( 'Dashboard', 'streamsera' ) . '' ); return $links; } /** * Body class on our pages (scopes the admin app's styles). * * @param string $classes Classes. * @return string */ public function body_class( $classes ) { return $this->is_our_page() ? $classes . ' streamsera-admin-page' : $classes; } /** * Whether the current screen is one of ours. * * @return bool */ private function is_our_page() { $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended return $page && ( array_key_exists( $page, self::pages() ) || 0 === strpos( $page, 'streamsera-' ) ); } /** * Mount point. */ public function render() { echo '

' . esc_html__( 'StreamSera', 'streamsera' ) . '

'; } /** * Enqueue the admin app on our pages only. */ public function assets() { if ( ! $this->is_our_page() ) { return; } $asset_file = STREAMSERA_PATH . 'build/admin.asset.php'; if ( ! file_exists( $asset_file ) ) { add_action( 'admin_notices', static function () { echo '

' . esc_html__( 'Some StreamSera files are missing. Please delete the plugin and install it again.', 'streamsera' ) . '

'; } ); return; } $asset = require $asset_file; wp_enqueue_media(); wp_enqueue_script( 'streamsera-admin', STREAMSERA_URL . 'build/admin.js', $asset['dependencies'], $asset['version'], true ); wp_enqueue_style( 'streamsera-admin', STREAMSERA_URL . 'build/admin.css', array( 'wp-components' ), $asset['version'] ); wp_style_add_data( 'streamsera-admin', 'rtl', 'replace' ); // build/admin-rtl.css on right-to-left sites. wp_set_script_translations( 'streamsera-admin', 'streamsera', STREAMSERA_PATH . 'languages' ); $data = array( 'restUrl' => esc_url_raw( rest_url() ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'layouts' => streamsera_layouts(), 'skins' => streamsera_skins(), 'defaults' => streamsera_default_player_config(), 'whiteLabel' => streamsera_white_label(), 'canManage' => current_user_can( apply_filters( 'streamsera_required_cap', 'manage_options', 'rest_manage' ) ), // Users who can't edit stations/players at all (e.g. an add-on's moderator role) list the published ones read-only. 'canEditPosts' => current_user_can( 'edit_posts' ), // Contributors can create stations and players only as drafts (an editor publishes them). 'canPublish' => current_user_can( apply_filters( 'streamsera_required_cap', 'publish_posts', 'stations' ) ), // The Embed dialog's "Add to page" edits or creates pages. 'canEditPages' => current_user_can( 'edit_pages' ), 'version' => STREAMSERA_VERSION, 'adminUrl' => admin_url(), 'siteUrl' => home_url(), 'docsUrl' => 'https://wpsera.com/docs/streamsera/', // Only the pages this user may open (the command palette lists these). 'pages' => self::allowed_pages(), 'defaultVolume' => (int) streamsera_get_setting( 'default_volume', 80 ), // Dashboard: greeting name, quick-tour video and upgrade prompts. 'userName' => wp_get_current_user()->display_name, /** * Filter whether StreamSera Pro is active. While false, the dashboard shows locked Pro * previews and upgrade links (no Pro functionality ships in this plugin). * * @param bool $active Whether Pro is active. */ 'pro' => (bool) apply_filters( 'streamsera_pro_active', defined( 'STREAMSERA_PRO_VERSION' ) ), /** * Filter the "Upgrade to Pro" address. * * @param string $url Upgrade page. */ 'upgradeUrl' => esc_url_raw( (string) apply_filters( 'streamsera_upgrade_url', 'https://wpsera.com/streamsera/pricing/' ) ), /** * Filter the quick-tour video address ("Watch the quick tour" / "Watch video" links; empty: they open the guide). * * @param string $url Video page. */ 'tourUrl' => esc_url_raw( (string) apply_filters( 'streamsera_tour_url', 'https://www.youtube.com/watch?v=1oRzbvgyV8c' ) ), ); /** * Filter the data handed to the admin app (add-ons add their sections). * * @param array $data Bootstrap data. */ $data = apply_filters( 'streamsera_admin_data', $data ); // Settings → Integrations: names, descriptions and fields of every page element (blocks & shortcodes). $data['elements'] = self::element_summaries(); wp_add_inline_script( 'streamsera-admin', 'window.StreamSeraAdmin = ' . wp_json_encode( $data ) . ';', 'before' ); } /** * Page elements for the Blocks & shortcodes reference (no picker queries). * * @return array */ private static function element_summaries() { $out = array(); foreach ( \StreamSera\Frontend\Elements::all() as $id => $el ) { $fields = array(); foreach ( (array) $el['fields'] as $key => $field ) { $options = array(); if ( 'select' === $field['type'] && is_array( $field['options'] ) ) { foreach ( $field['options'] as $value => $label ) { $options[] = array( (string) $value, html_entity_decode( wp_strip_all_tags( (string) $label ), ENT_QUOTES ) ); } } $fields[] = array( 'key' => (string) $key, 'label' => html_entity_decode( wp_strip_all_tags( (string) $field['label'] ), ENT_QUOTES ), 'type' => (string) $field['type'], 'default' => is_scalar( $field['default'] ) ? $field['default'] : '', 'options' => $options, ); } $out[] = array( 'id' => (string) $id, 'label' => html_entity_decode( wp_strip_all_tags( (string) $el['label'] ), ENT_QUOTES ), 'description' => html_entity_decode( wp_strip_all_tags( (string) $el['description'] ), ENT_QUOTES ), 'block' => (string) $el['block'], 'pro' => ! empty( $el['pro'] ), 'fields' => $fields, ); } return $out; } }