and gets a menu link. */ final class HostedPages { public const META_IN_APP = '_dashbread_in_app'; public const META_POSITION = '_dashbread_menu_position'; public const META_ROLES = '_dashbread_roles'; public const META_ICON = '_dashbread_icon'; public function register(): void { add_action( 'add_meta_boxes', array( $this, 'meta_box' ) ); add_action( 'save_post', array( $this, 'save' ), 10, 2 ); add_action( 'init', array( $this, 'register_meta' ) ); } /** @return string[] */ public static function post_types(): array { $types = get_post_types( array( 'public' => true ), 'names' ); unset( $types['attachment'] ); /** @param string[] $types Post types that can be hosted in the app. */ return (array) apply_filters( 'dashbread_hosted_post_types', array_values( $types ) ); } public function register_meta(): void { foreach ( self::post_types() as $type ) { register_post_meta( $type, self::META_IN_APP, array( 'type' => 'boolean', 'single' => true, 'show_in_rest' => true, 'auth_callback' => static fn() => current_user_can( 'edit_posts' ) ) ); register_post_meta( $type, self::META_POSITION, array( 'type' => 'integer', 'single' => true, 'show_in_rest' => true, 'default' => 50, 'auth_callback' => static fn() => current_user_can( 'edit_posts' ) ) ); register_post_meta( $type, self::META_ICON, array( 'type' => 'string', 'single' => true, 'show_in_rest' => true, 'default' => 'file', 'auth_callback' => static fn() => current_user_can( 'edit_posts' ) ) ); } } public function meta_box(): void { add_meta_box( 'dashbread_hosted', __( 'Dashbread app', 'dashbread' ), array( $this, 'render_box' ), self::post_types(), 'side', 'high' ); } public function render_box( \WP_Post $post ): void { wp_nonce_field( 'dashbread_hosted', 'dashbread_hosted_nonce' ); $in = (bool) get_post_meta( $post->ID, self::META_IN_APP, true ); $pos = (int) ( get_post_meta( $post->ID, self::META_POSITION, true ) ?: 50 ); $icon = (string) ( get_post_meta( $post->ID, self::META_ICON, true ) ?: 'file' ); $roles = array_filter( (array) get_post_meta( $post->ID, self::META_ROLES, true ) ); echo '
'; echo ''; echo '' . esc_html__( 'Icon', 'dashbread' ) . '
' . wp_kses( \Dashbread\Shell\Icons::picker( 'dashbread_icon', $icon ), Kses::ui() ); echo '' . esc_html__( 'Visible to roles (none = any logged-in user)', 'dashbread' ) . '
'; foreach ( wp_roles()->get_names() as $slug => $label ) { echo ''; } if ( $in ) { echo '' . esc_html( '/' . Settings::route() . '/' . $post->post_name ) . '
'; } } public function save( int $post_id, \WP_Post $post ): void { if ( ! isset( $_POST['dashbread_hosted_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['dashbread_hosted_nonce'] ) ), 'dashbread_hosted' ) ) { return; } if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) { return; } update_post_meta( $post_id, self::META_IN_APP, isset( $_POST['dashbread_in_app'] ) ? 1 : 0 ); update_post_meta( $post_id, self::META_POSITION, isset( $_POST['dashbread_menu_position'] ) ? intval( wp_unslash( $_POST['dashbread_menu_position'] ) ) : 50 ); update_post_meta( $post_id, self::META_ICON, isset( $_POST['dashbread_icon'] ) ? sanitize_key( wp_unslash( $_POST['dashbread_icon'] ) ) : 'file' ); $roles = isset( $_POST['dashbread_roles'] ) ? array_map( 'sanitize_key', (array) wp_unslash( $_POST['dashbread_roles'] ) ) : array(); update_post_meta( $post_id, self::META_ROLES, $roles ); } /** Hosted posts the current user may see, ordered by menu position. @return \WP_Post[] */ public static function visible(): array { $posts = get_posts( array( 'post_type' => self::post_types(), 'post_status' => 'publish', 'posts_per_page' => 100, 'meta_key' => self::META_IN_APP, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- bounded (posts_per_page 100), the flag is the only index we have. 'meta_value' => '1', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'no_found_rows' => true, ) ); $user = wp_get_current_user(); $out = array(); foreach ( $posts as $p ) { $roles = array_filter( (array) get_post_meta( $p->ID, self::META_ROLES, true ) ); // no meta reads as '' — that means everyone, not nobody if ( $roles && ! array_intersect( $roles, (array) $user->roles ) ) { continue; } /** Add-ons narrow access further (Pro plans gate hosted pages here). @param bool $visible @param \WP_Post $p @param \WP_User $user */ if ( ! apply_filters( 'dashbread_hosted_visible', true, $p, $user ) ) { continue; } $out[] = $p; } usort( $out, static fn( $a, $b ) => (int) get_post_meta( $a->ID, self::META_POSITION, true ) <=> (int) get_post_meta( $b->ID, self::META_POSITION, true ) ); return $out; } public static function for_route( string $route ): ?\WP_Post { foreach ( self::visible() as $p ) { if ( $p->post_name === $route ) { return $p; } } return null; } }