'GET', 'permission_callback' => array( $this, 'can_manage' ), 'callback' => array( $this, 'bootstrap' ) ) ); register_rest_route( self::NS, '/admin/settings', array( 'methods' => 'POST', 'permission_callback' => array( $this, 'can_manage' ), 'callback' => array( $this, 'save_settings' ) ) ); register_rest_route( self::NS, '/admin/hosted', array( array( 'methods' => 'GET', 'permission_callback' => array( $this, 'can_manage' ), 'callback' => array( $this, 'hosted' ), 'args' => array( 'search' => array( 'type' => 'string' ), 'all' => array( 'type' => 'boolean' ) ) ), array( 'methods' => 'POST', 'permission_callback' => array( $this, 'can_manage' ), 'callback' => array( $this, 'save_hosted' ), 'args' => array( 'id' => array( 'type' => 'integer', 'required' => true ) ) ), ) ); register_rest_route( self::NS, '/admin/broadcast', array( 'methods' => 'POST', 'permission_callback' => array( $this, 'can_manage' ), 'callback' => array( $this, 'broadcast' ) ) ); register_rest_route( self::NS, '/admin/dismiss', array( 'methods' => 'POST', 'permission_callback' => array( $this, 'can_manage' ), 'callback' => array( $this, 'dismiss' ), 'args' => array( 'key' => array( 'type' => 'string', 'required' => true ) ) ) ); } /** Last login per user, so the dashboard can show "last seen" and "logins today" without a log table. */ public function track_login( string $login, \WP_User $user ): void { update_user_meta( $user->ID, 'dashbread_last_login', time() ); } public function bootstrap(): \WP_REST_Response { return new \WP_REST_Response( array( 'settings' => $this->settings_payload(), 'stats' => $this->stats(), 'status' => $this->status(), 'members' => $this->recent_members(), 'hosted' => $this->hosted_list( '', false ), 'roles' => $this->roles(), 'icons' => Icons::SUGGESTED, 'pages' => $this->registered_pages(), 'dismissed'=> (array) get_user_meta( get_current_user_id(), 'dashbread_admin_dismissed', true ), ) ); } private function settings_payload(): array { $s = Settings::all(); unset( $s['menus'] ); $s['logo_url'] = $s['logo_id'] ? (string) wp_get_attachment_image_url( (int) $s['logo_id'], 'medium' ) : ''; return $s; } public function save_settings( \WP_REST_Request $r ): \WP_REST_Response { $in = (array) $r->get_json_params(); $out = ( new \Dashbread\Core\Admin() )->sanitize( $in ); update_option( Settings::OPTION, $out ); if ( get_option( 'dashbread_flush_needed' ) ) { \Dashbread\Core\Router::add_rewrite_rules(); flush_rewrite_rules( false ); update_option( 'dashbread_flush_needed', 0 ); } return new \WP_REST_Response( array( 'settings' => $this->settings_payload(), 'status' => $this->status() ) ); } private function stats(): array { global $wpdb; $counts = count_users(); $week_ago = gmdate( 'Y-m-d H:i:s', time() - WEEK_IN_SECONDS ); $today = strtotime( 'today' ); return array( 'members' => (int) $counts['total_users'], 'new_week' => (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE user_registered >= %s', $wpdb->users, $week_ago ) ), // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- admin-only dashboard stat, no core API for a date-bounded user count. 'logins_today' => (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE meta_key = %s AND meta_value >= %d', $wpdb->usermeta, 'dashbread_last_login', $today ) ), // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- admin-only dashboard stat. 'notif_week' => (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE created_at >= %s', Store::table(), $week_ago ) ), // phpcs:ignore WordPress.DB -- Dashbread's own notifications table; admin-only stat. 'notif_unread' => (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE read_at IS NULL', Store::table() ) ), // phpcs:ignore WordPress.DB -- Dashbread's own notifications table; admin-only stat. 'hosted' => count( $this->hosted_list( '', false ) ), 'delete_requests'=> (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE meta_key = %s', $wpdb->usermeta, 'dashbread_delete_requested' ) ), // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- admin-only dashboard stat. ); } private function status(): array { $s = Settings::all(); $conflict = get_page_by_path( $s['route'] ); $locations = get_nav_menu_locations(); $assigned = 0; foreach ( array( 'dashbread_sidebar', 'dashbread_header', 'dashbread_account' ) as $loc ) { if ( ! empty( $locations[ $loc ] ) ) { $assigned++; } } $sidebars = (array) get_option( 'sidebars_widgets', array() ); $theme = wp_get_theme(); $switch = class_exists( 'user_switching' ) || function_exists( 'switch_to_user' ); return array( 'version' => DASHBREAD_VERSION, 'pro' => Features::pro_active() ? ( defined( 'DASHBREAD_PRO_VERSION' ) ? DASHBREAD_PRO_VERSION : '' ) : '', 'app_url' => Settings::app_url(), 'home' => home_url( '/' ), 'route' => $s['route'], 'route_conflict' => $conflict ? get_edit_post_link( $conflict->ID, 'raw' ) : '', 'permalinks' => Settings::pretty_permalinks(), 'theme' => $theme->get( 'Name' ), 'menus_assigned' => $assigned, 'widgets' => count( (array) ( $sidebars['dashbread-dashboard'] ?? array() ) ), 'user_switching' => $switch, 'php' => PHP_VERSION, 'wp' => get_bloginfo( 'version' ), 'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG, 'urls' => array( 'menus' => admin_url( 'nav-menus.php' ), 'widgets' => admin_url( 'widgets.php' ), 'users' => admin_url( 'users.php' ), 'plugins' => admin_url( 'plugin-install.php?s=user+switching&tab=search&type=term' ), 'permalinks'=> admin_url( 'options-permalink.php' ), 'new_page' => admin_url( 'post-new.php?post_type=page' ), ), ); } private function recent_members(): array { $out = array(); foreach ( get_users( array( 'number' => 8, 'orderby' => 'registered', 'order' => 'DESC' ) ) as $u ) { $last = (int) get_user_meta( $u->ID, 'dashbread_last_login', true ); $out[] = array( 'id' => $u->ID, 'name' => $u->display_name, 'email' => $u->user_email, 'role' => isset( wp_roles()->get_names()[ $u->roles[0] ?? '' ] ) ? translate_user_role( wp_roles()->get_names()[ $u->roles[0] ] ) : (string) ( $u->roles[0] ?? '' ), 'avatar' => get_avatar_url( $u->ID, array( 'size' => 48 ) ), 'joined' => mysql2date( 'M j', $u->user_registered ), 'last_seen' => $last ? sprintf( /* translators: %s: human-readable time difference */ __( '%s ago', 'dashbread' ), human_time_diff( $last ) ) : __( 'Never', 'dashbread' ), 'delete_requested' => (bool) get_user_meta( $u->ID, 'dashbread_delete_requested', true ), 'edit_url' => get_edit_user_link( $u->ID ), 'switch_url'=> function_exists( 'switch_to_user' ) && class_exists( 'user_switching' ) ? \user_switching::switch_to_url( $u ) : '', ); } return $out; } private function roles(): array { $out = array(); foreach ( wp_roles()->get_names() as $slug => $label ) { $out[] = array( 'slug' => $slug, 'label' => translate_user_role( $label ) ); } return $out; } private function registered_pages(): array { $out = array(); foreach ( Registry::instance()->pages() as $id => $p ) { $out[] = array( 'id' => $id, 'title' => (string) $p['title'], 'path' => (string) $p['path'], 'icon' => (string) $p['icon'] ); } return $out; } /** Hosted posts (all statuses) or, with $all, every eligible published post matching a search. */ private function hosted_list( string $search, bool $all ): array { $args = array( 'post_type' => HostedPages::post_types(), 'post_status' => $all ? 'publish' : array( 'publish', 'draft', 'private', 'pending' ), 'posts_per_page' => $all ? 30 : 200, 'orderby' => $all ? 'date' : 'title', 'order' => $all ? 'DESC' : 'ASC', 'no_found_rows' => true, ); if ( $all ) { $args['s'] = $search; } else { $args['meta_key'] = HostedPages::META_IN_APP; // phpcs:ignore WordPress.DB.SlowDBQuery $args['meta_value'] = '1'; // phpcs:ignore WordPress.DB.SlowDBQuery } $out = array(); foreach ( get_posts( $args ) as $p ) { $out[] = $this->hosted_row( $p ); } if ( ! $all ) { usort( $out, static fn( $a, $b ) => $a['position'] <=> $b['position'] ?: strcasecmp( $a['title'], $b['title'] ) ); } return $out; } private function hosted_row( \WP_Post $p ): array { return array( 'id' => $p->ID, 'title' => get_the_title( $p ) ?: __( '(no title)', 'dashbread' ), 'type' => (string) ( get_post_type_object( $p->post_type )->labels->singular_name ?? $p->post_type ), 'status' => $p->post_status, 'slug' => $p->post_name, 'in_app' => (bool) get_post_meta( $p->ID, HostedPages::META_IN_APP, true ), 'position' => (int) ( get_post_meta( $p->ID, HostedPages::META_POSITION, true ) ?: 50 ), 'icon' => Icons::resolve( (string) ( get_post_meta( $p->ID, HostedPages::META_ICON, true ) ?: 'file' ) ), 'roles' => array_values( array_filter( (array) get_post_meta( $p->ID, HostedPages::META_ROLES, true ) ) ), 'app_url' => Settings::app_url( $p->post_name ), 'edit_url' => get_edit_post_link( $p->ID, 'raw' ), ); } public function hosted( \WP_REST_Request $r ): \WP_REST_Response { return new \WP_REST_Response( array( 'items' => $this->hosted_list( (string) $r->get_param( 'search' ), (bool) $r->get_param( 'all' ) ) ) ); } public function save_hosted( \WP_REST_Request $r ): \WP_REST_Response|\WP_Error { $id = (int) $r->get_param( 'id' ); $p = get_post( $id ); if ( ! $p || ! current_user_can( 'edit_post', $id ) ) { return new \WP_Error( 'dashbread_not_found', __( 'Post not found.', 'dashbread' ), array( 'status' => 404 ) ); } $in = (array) $r->get_json_params(); if ( array_key_exists( 'in_app', $in ) ) { update_post_meta( $id, HostedPages::META_IN_APP, empty( $in['in_app'] ) ? 0 : 1 ); } if ( array_key_exists( 'position', $in ) ) { update_post_meta( $id, HostedPages::META_POSITION, (int) $in['position'] ); } if ( array_key_exists( 'icon', $in ) ) { update_post_meta( $id, HostedPages::META_ICON, Icons::resolve( (string) $in['icon'] ) ); } if ( array_key_exists( 'roles', $in ) ) { update_post_meta( $id, HostedPages::META_ROLES, array_map( 'sanitize_key', (array) $in['roles'] ) ); } return new \WP_REST_Response( array( 'item' => $this->hosted_row( get_post( $id ) ) ) ); } public function broadcast( \WP_REST_Request $r ): \WP_REST_Response|\WP_Error { $in = (array) $r->get_json_params(); $title = sanitize_text_field( (string) ( $in['title'] ?? '' ) ); if ( '' === $title ) { return new \WP_Error( 'dashbread_title', __( 'A title is required.', 'dashbread' ), array( 'status' => 400 ) ); } $role = sanitize_key( (string) ( $in['role'] ?? '' ) ); $args = array( 'fields' => 'ID', 'number' => 10000 ); if ( '' !== $role ) { $args['role'] = $role; } $ids = array_map( 'intval', (array) get_users( $args ) ); Notifications::notify_many( $ids, 'broadcast', array( 'title' => $title, 'body' => sanitize_textarea_field( (string) ( $in['body'] ?? '' ) ), 'url' => esc_url_raw( (string) ( $in['url'] ?? '' ) ), ) ); return new \WP_REST_Response( array( 'sent' => count( $ids ), 'queued' => count( $ids ) > 50 ) ); } public function dismiss( \WP_REST_Request $r ): \WP_REST_Response { $uid = get_current_user_id(); $keys = (array) get_user_meta( $uid, 'dashbread_admin_dismissed', true ); $keys[] = sanitize_key( (string) $r->get_param( 'key' ) ); $keys = array_values( array_unique( array_filter( $keys, 'strlen' ) ) ); update_user_meta( $uid, 'dashbread_admin_dismissed', $keys ); return new \WP_REST_Response( array( 'dismissed' => $keys ) ); } }