registry = $registry; } /** * Hook into WordPress. * * @since 1.0.0 * @return void */ public function register() { add_action( 'admin_menu', array( $this, 'menu' ), 60 ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'register_shared_scripts' ), 1 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); add_filter( 'admin_body_class', array( $this, 'body_class' ) ); } /** * Admin URL for the list, a module, or a module tab. * * @since 1.0.0 * @param string $module Module ID. * @param string $tab `settings` or a view slug. * @return string */ public static function url( $module = '', $tab = '' ) { $args = array( 'page' => self::SLUG ); if ( $module ) { $args['module'] = $module; } if ( $tab && 'settings' !== $tab ) { $args['tab'] = $tab; } return add_query_arg( $args, admin_url( 'admin.php' ) ); } /** * The current user's theme: system, light or dark. * * @since 1.0.0 * @return string */ public static function theme() { $theme = (string) get_user_meta( get_current_user_id(), self::THEME_META, true ); return in_array( $theme, array( 'light', 'dark' ), true ) ? $theme : 'system'; } /** * Register the submenu. * * @since 1.0.0 * @return void */ public function menu() { $this->hook = (string) add_submenu_page( 'woocommerce', __( 'Powerups', 'admin-powerups-for-woocommerce' ), __( 'Powerups', 'admin-powerups-for-woocommerce' ), 'manage_woocommerce', self::SLUG, array( $this, 'render' ) ); if ( $this->hook ) { add_action( 'load-' . $this->hook, array( $this, 'guard' ) ); } } /** * Register shared scripts power-ups enqueue by handle, on any admin screen: * `apwc-dev-tools` (JSON viewer, batch runner, copy) for Developer and Performance tools and the * Order meta box. * * @since 1.0.0 * @return void */ public static function register_shared_scripts() { wp_register_script( 'apwc-dev-tools', Assets::url( 'assets/js/dev-tools.js' ), array( 'wp-i18n' ), Assets::ver( 'assets/js/dev-tools.js' ), true ); wp_set_script_translations( 'apwc-dev-tools', 'admin-powerups-for-woocommerce' ); } /** * Refuse a power-up page the current user may not see (admin-only power-ups for store * managers) before anything of it loads: 403, never the settings or a tool. * * @since 1.0.0 * @return void */ public function guard() { if ( $this->is_blocked() ) { wp_die( esc_html__( 'Sorry, you are not allowed to access this power-up.', 'admin-powerups-for-woocommerce' ), '', array( 'response' => 403 ) ); } } /** * Other screens drawn with our panel (e.g. WooCommerce → Email log). * * @var string[] */ private static $screens = array(); /** * Draw another admin screen with our panel: theme class on , panel styles and admin.js. * Call from `admin_menu` with the hook suffix `add_submenu_page()` returned. * * @since 1.0.0 * @param string $hook Hook suffix. * @return void */ public static function add_screen( $hook ) { if ( $hook ) { self::$screens[] = (string) $hook; } } /** * Whether the current request is our screen. * * @since 1.0.0 * @return bool */ private function is_screen() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; return $screen && ( ( $this->hook && $screen->id === $this->hook ) || in_array( $screen->id, self::$screens, true ) ); } /** * Theme class on so the whole content area can take the panel's ground without a flash. * * @since 1.0.0 * @param string $classes Classes. * @return string */ public function body_class( $classes ) { if ( $this->is_screen() ) { $classes .= ' apwc-screen apwc-theme-' . self::theme(); } return $classes; } /** * Current module from the request, if any. * * @since 1.0.0 * @return AbstractModule|null */ private function current_module() { $module = $this->requested_module(); return $module && $module->user_can_access() ? $module : null; } /** * The module named in the request, whether or not the user may see it. * * @since 1.0.0 * @return AbstractModule|null */ private function requested_module() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing. $id = isset( $_GET['module'] ) && is_string( $_GET['module'] ) ? sanitize_key( wp_unslash( $_GET['module'] ) ) : ''; return $id ? $this->registry->get( $id ) : null; } /** * Whether the request names a power-up that exists but the current user may not see. * * @since 1.0.0 * @return bool */ private function is_blocked() { $module = $this->requested_module(); return $module && ! $module->user_can_access(); } /** * Current tab: `settings` or a view slug the module declares (views need the module on). * * @since 1.0.0 * @param AbstractModule $module Module. * @return string */ private function current_tab( AbstractModule $module ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing. $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; // Views need the power-up on and its requirements met (boot() is skipped otherwise). if ( $tab && isset( $module->views()[ $tab ] ) && $module->is_enabled() && '' === (string) $module->unmet_requirement() ) { return $tab; } return 'settings'; } /** * Enqueue assets on our screen only. * * @since 1.0.0 * @param string $hook Hook suffix. * @return void */ public function enqueue( $hook ) { if ( in_array( $hook, self::$screens, true ) ) { wp_enqueue_style( 'apwc-admin', Assets::url( 'assets/css/admin.css' ), array(), Assets::ver( 'assets/css/admin.css' ) ); wp_enqueue_script( 'apwc-admin', Assets::url( 'assets/js/admin.js' ), array( 'wp-api-fetch', 'wp-i18n' ), Assets::ver( 'assets/js/admin.js' ), true ); wp_set_script_translations( 'apwc-admin', 'admin-powerups-for-woocommerce' ); return; } if ( $hook !== $this->hook ) { return; } wp_enqueue_style( 'apwc-admin', Assets::url( 'assets/css/admin.css' ), array(), Assets::ver( 'assets/css/admin.css' ) ); wp_enqueue_script( 'apwc-admin', Assets::url( 'assets/js/admin.js' ), array( 'wp-api-fetch', 'wp-i18n' ), Assets::ver( 'assets/js/admin.js' ), true ); wp_set_script_translations( 'apwc-admin', 'admin-powerups-for-woocommerce' ); wp_enqueue_script( 'apwc-combobox', Assets::url( 'assets/js/combobox.js' ), array( 'wp-api-fetch', 'wp-i18n' ), Assets::ver( 'assets/js/combobox.js' ), true ); wp_set_script_translations( 'apwc-combobox', 'admin-powerups-for-woocommerce' ); // A blocked page runs nothing of the power-up (guard() answers 403 before this). $module = $this->is_blocked() ? null : $this->current_module(); $tab = $module ? $this->current_tab( $module ) : ''; if ( $module ) { // Settings screen wiring runs on or off; views (tabs) only exist while on. $module->admin_screen( $tab ); if ( 'settings' !== $tab ) { $module->enqueue_view_assets( $tab ); } } /** * Enqueue add-on assets on the Powerups screen. * * @since 1.0.0 * @param AbstractModule|null $module Module being viewed, or null on the list. * @param string $tab Current tab ('' on the list). */ do_action( 'apwc_admin_enqueue', $module, $tab ); } /** * Render the page. * * @since 1.0.0 * @return void */ public function render() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You are not allowed to manage power-ups.', 'admin-powerups-for-woocommerce' ) ); } $this->guard(); $theme = self::theme(); $registry = $this->registry; $module = $this->current_module(); if ( $module ) { $tab = $this->current_tab( $module ); include APWC_PATH . 'views/admin/module.php'; return; } // Only what this user may see: admin-only groups never reach store managers. $modules = $this->registry->visible(); $enabled = count( array_filter( $modules, static fn( $m ) => $m->is_enabled() ) ); $labels = Registry::categories(); $labels['other'] = __( 'Other', 'admin-powerups-for-woocommerce' ); $admin_only = Registry::admin_only_categories(); $by_category = array(); foreach ( $modules as $m ) { $by_category[ isset( $labels[ $m->category() ] ) ? $m->category() : 'other' ][] = $m; } // Categories in display order, "Other" last; empty ones are left out. $groups = array(); foreach ( $labels as $key => $label ) { if ( ! empty( $by_category[ $key ] ) ) { $groups[ $key ] = array( 'label' => $label, 'modules' => $by_category[ $key ], // Warning + "Admins only" badge; also for an admin-only power-up filed under "Other". 'admin_only' => in_array( (string) $key, $admin_only, true ) || (bool) array_filter( $by_category[ $key ], static fn( $m ) => $m->is_admin_only() ), ); } } include APWC_PATH . 'views/admin/dashboard.php'; } }