settings = $settings; $this->access = $access; $this->health = $health; $this->pages = $pages; } /** * Hooks into WordPress. * * @return void */ public function register() { add_filter( 'admin_body_class', array( $this, 'body_class' ) ); add_action( 'in_admin_header', array( $this, 'render' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ), 5 ); } /** * A user's theme: light (default), dark or system. * * @param int $user_id User ID. * @return string */ public static function theme( $user_id ) { $theme = (string) get_user_meta( (int) $user_id, self::THEME_META, true ); return in_array( $theme, self::THEMES, true ) ? $theme : 'light'; } /** * Whether a user collapsed the sidebar to its icon rail. * * @param int $user_id User ID. * @return bool */ public static function collapsed( $user_id ) { return 'collapsed' === (string) get_user_meta( (int) $user_id, self::SIDEBAR_META, true ); } /** * The screen object being viewed, or null outside TurtleReport. * * @return AdminPage|null */ public function screen() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only screen detection. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; foreach ( $this->pages as $screen ) { if ( $screen::SLUG === $page ) { return $screen; } } return null; } /** * Slug of the TurtleReport screen being viewed, or '' elsewhere. * * @return string */ public function current() { $screen = $this->screen(); return $screen ? $screen::SLUG : ''; } /** * Which layout the current user gets. * * @return string "sidebar" or "header". */ public function layout() { if ( current_user_can( Capabilities::MANAGE ) ) { return 'sidebar'; } return count( $this->access->visible_sites( get_current_user_id() ) ) > 1 ? 'sidebar' : 'header'; } /** * Body classes on TurtleReport screens: token scope, theme, layout and * TurtleReport sidebar state. Preserve Core's menu state by default. * * @param string $classes Space-separated classes. * @return string */ public function body_class( $classes ) { if ( '' === $this->current() ) { return $classes; } $user_id = get_current_user_id(); $layout = $this->layout(); $add = array( 'tly-shell', 'tly-theme-' . self::theme( $user_id ), 'tly-layout-' . $layout ); if ( 'sidebar' === $layout ) { if ( self::collapsed( $user_id ) ) { $add[] = 'tly-side-collapsed'; } /** * Opt into folding the WordPress admin menu on TurtleReport screens. * Core's user preference and responsive state are preserved by default. * * @param bool $fold Default false. */ if ( apply_filters( 'turtlereport_fold_admin_menu', false ) ) { $add[] = 'folded'; } } return $classes . ' ' . implode( ' ', $add ) . ' '; } /** * Shared styles and the shell script. * * @return void */ public function enqueue() { if ( '' === $this->current() ) { return; } wp_enqueue_style( 'turtlereport-tokens', TURTLEREPORT_URL . 'assets/css/tokens.css', array(), AdminPage::asset_version( 'assets/css/tokens.css' ) ); wp_enqueue_style( 'turtlereport-shell', TURTLEREPORT_URL . 'assets/css/shell.css', array( 'turtlereport-tokens' ), AdminPage::asset_version( 'assets/css/shell.css' ) ); AdminAssets::enqueue( 'shell', 'turtlereport-shell' ); } /** * Outputs the sidebar (or slim header) before the screen's content. * * @return void */ public function render() { $screen = $this->screen(); if ( ! $screen ) { return; } View::render( 'shell', $this->view_data( $screen ) ); } /** * Everything the shell template needs. * * @param AdminPage $screen Current screen. * @return array */ public function view_data( AdminPage $screen ) { $user_id = get_current_user_id(); $sites = $this->reports( $screen ); $title = $screen->nav_title(); foreach ( $sites as $item ) { if ( $item['active'] ) { $title = $item['name']; } } return array( 'layout' => $this->layout(), // White-label: the agency's name when set, otherwise TurtleReport. 'brand_name' => '' !== (string) $this->settings->get( 'brand_name' ) ? (string) $this->settings->get( 'brand_name' ) : __( 'TurtleReport', 'turtlereport' ), 'brand_logo' => $this->settings->logo_url(), 'home' => ReportsPage::url(), 'sites' => $sites, 'filter' => count( $sites ) >= self::FILTER_FROM, 'manage' => $this->manage( $screen ), 'guide' => $this->guide( $screen ), 'add_site' => current_user_can( Capabilities::MANAGE ) ? SitesPage::url( array( 'action' => 'new' ) ) : '', 'theme' => self::theme( $user_id ), 'collapsed' => self::collapsed( $user_id ), 'title' => $title, ); } /** * Reports the user can open, for the switcher. * * @param AdminPage $screen Current screen. * @return array[] Items with id, name, label, initial, url and active. */ public function reports( AdminPage $screen ) { $sites = $this->access->visible_sites( get_current_user_id() ); if ( ! $sites ) { return array(); } $active = ''; if ( ReportsPage::SLUG === $screen::SLUG ) { // Same rule as the report screen: the requested site if visible, else the first (A–Z). // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only choice of a visible site. $requested = isset( $_GET['site'] ) ? sanitize_key( wp_unslash( $_GET['site'] ) ) : ''; $active = isset( $sites[ $requested ] ) ? $requested : (string) key( $sites ); } $out = array(); foreach ( $sites as $site ) { $name = $site->display_name(); $out[] = array( 'id' => $site->id, 'name' => $name, 'label' => $site->site_label(), 'initial' => Text::upper( Text::substr( $name, 0, 1 ) ), 'url' => ReportsPage::url( array( 'site' => $site->id ) ), 'active' => $site->id === $active, ); } return $out; } /** * Manage links for administrators. * * @param AdminPage $screen Current screen. * @return array[] Items with key, label, url, active, badge and alert. */ public function manage( AdminPage $screen ) { $items = array(); foreach ( $this->pages as $page ) { if ( $page instanceof ReportsPage || $page instanceof GuidePage || ! current_user_can( $page->capability() ) ) { continue; } $item = array( 'key' => str_replace( 'turtlereport-', '', $page::SLUG ), 'label' => $page->nav_title(), 'url' => $page::url(), 'active' => $page::SLUG === $screen::SLUG, 'badge' => '', 'alert' => '', ); if ( $page instanceof DeliveriesPage ) { $waiting = $page->attention(); if ( $waiting ) { /* translators: %d: number of scheduled report emails that need attention. */ $item['alert'] = sprintf( _n( '%d report email needs attention', '%d report emails need attention', $waiting, 'turtlereport' ), $waiting ); } } if ( $page instanceof ConnectionsPage && $this->health ) { $failing = count( $this->health->failing() ); if ( $failing ) { /* translators: %d: number of connections that stopped working. */ $item['alert'] = sprintf( _n( '%d connection needs attention', '%d connections need attention', $failing, 'turtlereport' ), $failing ); } } $items[] = $item; } return $items; } /** * The setup guide link, if the user may open it. * * @param AdminPage $screen Current screen. * @return array|null Item with label, url and active. */ private function guide( AdminPage $screen ) { foreach ( $this->pages as $page ) { if ( $page instanceof GuidePage && current_user_can( $page->capability() ) ) { return array( 'label' => $page->nav_title(), 'url' => $page::url(), 'active' => $page::SLUG === $screen::SLUG, ); } } return null; } }