Fix sub-app permissions bug, add Comments app, support Site Editor

- Root-cause fix for "Sorry, you are not allowed to access this page" on
  Menu Creator/Activity Log/Role Editor: remove_submenu_page() was
  corrupting WP core's user_can_access_admin_page() capability lookup.
  Sub-apps now stay registered in $submenu; MenuSerializer filters them
  out of the sidebar instead.
- Add a 6th sub-app, Comments, with a redirect from native edit-comments.php.
- Extend the window-chrome/taskbar framing to the Site Editor
  (site-editor.php), including a fix for a CSS position:fixed
  over-constraint bug that let its frame run past the viewport.
- Rewrite the taskbar from a single sessionStorage slot to a list so
  multiple minimized windows are each independently restorable.
- Extend native-dark.css coverage to the block editor's own chrome.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
nixpanel
2026-07-03 18:48:25 -05:00
co-authored by Claude Sonnet 5
parent b0b0a9999a
commit 8153d4397c
16 changed files with 1510 additions and 194 deletions
+2
View File
@@ -9,6 +9,7 @@ use AuroraAdmin\Rest\DashboardData;
use AuroraAdmin\Analytics\Analytics;
use AuroraAdmin\Rest\AnalyticsData;
use AuroraAdmin\Rest\PluginsData;
use AuroraAdmin\Rest\CommentsData;
use AuroraAdmin\Posts\ScheduledDayFilter;
use AuroraAdmin\WhiteLabel\WhiteLabel;
use AuroraAdmin\Pages\SubApps;
@@ -41,6 +42,7 @@ class AuroraAdmin
new Analytics();
new AnalyticsData();
new PluginsData();
new CommentsData();
new ScheduledDayFilter();
new WhiteLabel();
new SubApps();
+57 -15
View File
@@ -6,15 +6,28 @@ use AuroraAdmin\Utility\Assets;
defined("ABSPATH") || exit();
/**
* Registers Aurora's 5 secondary full-page apps (Menu Creator, Admin
* Notices, Role Editor, Activity Log, Database Explorer). Each is a real WP
* admin page (proper capability check, working URL, its own hook_suffix for
* asset gating) registered as a submenu of "aurora-admin" — then
* immediately hidden from WordPress's own submenu via remove_submenu_page(),
* the standard register-but-hide trick. This keeps them out of the
* WP-native menu list (MenuSerializer never sees them, nothing to filter)
* while still being reachable, permission-checked pages; they're presented
* instead in the sidebar's dedicated Aurora nav section (see Shell::aurora_nav()).
* Registers Aurora's secondary full-page apps (Menu Creator, Admin Notices,
* Role Editor, Activity Log, Database Explorer, Plugins, Comments). Each is
* a real WP admin page (proper capability check, working URL, its own
* hook_suffix for asset gating) registered as a submenu of "aurora-admin"
* (or "plugins.php" for the Plugins page).
*
* These stay registered in WordPress's own $submenu — they are NOT hidden
* via remove_submenu_page(). An earlier version did call it (the standard
* "register but hide" trick), but that turned out to corrupt WP core's own
* access-control bookkeeping: user_can_access_admin_page() partly relies on
* $submenu still containing the entry to resolve the page's required
* capability, so removing it caused every user (including admins) to be
* denied with "Sorry, you are not allowed to access this page" — confirmed
* by reproducing the bug, disabling the remove_submenu_page() call, and
* watching it resolve immediately. Since Aurora already hides WordPress's
* entire native admin menu chrome (replaced by its own Vue sidebar), a
* still-registered-but-native-menu-invisible entry has no visible effect on
* its own — MenuSerializer::serialize() is what keeps these out of the
* sidebar's own WP-native-menu section (skips slugs prefixed
* "aurora-admin-", plus the "aurora-admin" top-level slug itself), so they
* aren't duplicated there. They're presented instead in the sidebar's
* dedicated Aurora nav section (see Shell::aurora_nav()).
*/
class SubApps
{
@@ -25,6 +38,7 @@ class SubApps
"aurora-admin-activity-log" => ["title" => "Activity Log", "entry" => "activity-log"],
"aurora-admin-database" => ["title" => "Database Explorer", "entry" => "database-explorer"],
"aurora-admin-plugins" => ["title" => "Installed Plugins", "entry" => "plugins"],
"aurora-admin-comments" => ["title" => "Comments", "entry" => "comments"],
];
private static $hook_suffixes = [];
@@ -32,19 +46,19 @@ class SubApps
public function __construct()
{
add_action("admin_menu", [self::class, "register_menus"]);
add_action("admin_menu", [self::class, "hide_from_native_submenu"], 999);
add_action("admin_enqueue_scripts", [self::class, "maybe_enqueue"]);
add_action("admin_init", [self::class, "redirect_native_pages"]);
}
public static function register_menus()
{
foreach (self::PAGES as $slug => $page) {
$parent = $slug === "aurora-admin-plugins" ? "plugins.php" : "aurora-admin";
$parent = self::parent_slug($slug);
$hook = add_submenu_page(
$parent,
__($page["title"], "aurora-admin"),
__($page["title"], "aurora-admin"),
$slug === "aurora-admin-plugins" ? "activate_plugins" : "manage_options",
self::capability($slug),
$slug,
[self::class, "render"]
);
@@ -52,11 +66,23 @@ class SubApps
}
}
public static function hide_from_native_submenu()
private static function parent_slug($slug)
{
foreach (array_keys(self::PAGES) as $slug) {
remove_submenu_page($slug === "aurora-admin-plugins" ? "plugins.php" : "aurora-admin", $slug);
if ($slug === "aurora-admin-plugins") {
return "plugins.php";
}
return "aurora-admin";
}
private static function capability($slug)
{
if ($slug === "aurora-admin-plugins") {
return "activate_plugins";
}
if ($slug === "aurora-admin-comments") {
return "edit_posts";
}
return "manage_options";
}
public static function maybe_enqueue($hook_suffix)
@@ -69,6 +95,22 @@ class SubApps
Assets::enqueue("aurora-admin-" . self::PAGES[$slug]["entry"], "entries/" . self::PAGES[$slug]["entry"] . ".js");
}
public static function redirect_native_pages()
{
global $pagenow;
if ($pagenow !== "edit-comments.php" || !current_user_can("edit_posts")) {
return;
}
if (isset($_GET["page"]) || wp_doing_ajax()) {
return;
}
wp_safe_redirect(admin_url("admin.php?page=aurora-admin-comments"));
exit;
}
public static function render()
{
$page = isset($_GET["page"]) ? sanitize_text_field(wp_unslash($_GET["page"])) : "";
+208
View File
@@ -0,0 +1,208 @@
<?php
namespace AuroraAdmin\Rest;
defined("ABSPATH") || exit();
/**
* REST endpoints for Aurora's custom Comments page.
*/
class CommentsData
{
public function __construct()
{
add_action("rest_api_init", [self::class, "register"]);
}
public static function register()
{
register_rest_route("aurora-admin/v1", "/comments", [
"methods" => "GET",
"callback" => [self::class, "list_comments"],
"permission_callback" => function () {
return current_user_can("moderate_comments");
},
"args" => [
"status" => ["type" => "string", "required" => false],
"search" => ["type" => "string", "required" => false],
"page" => ["type" => "integer", "required" => false],
"perPage" => ["type" => "integer", "required" => false],
],
]);
register_rest_route("aurora-admin/v1", "/comments/status", [
"methods" => "POST",
"callback" => [self::class, "update_status"],
"permission_callback" => function () {
return current_user_can("moderate_comments");
},
"args" => [
"id" => ["type" => "integer", "required" => true],
"status" => ["type" => "string", "required" => true],
],
]);
register_rest_route("aurora-admin/v1", "/comments/(?P<id>\d+)", [
"methods" => "DELETE",
"callback" => [self::class, "delete_comment"],
"permission_callback" => function () {
return current_user_can("moderate_comments");
},
"args" => [
"id" => ["type" => "integer", "required" => true],
],
]);
}
public static function list_comments($request)
{
$status = self::query_status($request->get_param("status"));
$page = max(1, (int) ($request->get_param("page") ?: 1));
$per_page = min(100, max(10, (int) ($request->get_param("perPage") ?: 50)));
$search = sanitize_text_field((string) $request->get_param("search"));
$args = [
"status" => $status,
"number" => $per_page,
"offset" => ($page - 1) * $per_page,
"orderby" => "comment_date_gmt",
"order" => "DESC",
"type" => "comment",
];
if ($search !== "") {
$args["search"] = $search;
}
$query = new \WP_Comment_Query();
$comments = $query->query($args);
$total = (int) $query->found_comments;
return new \WP_REST_Response([
"items" => array_map([self::class, "serialize_comment"], $comments),
"stats" => self::stats(),
"pagination" => [
"page" => $page,
"perPage" => $per_page,
"total" => $total,
"totalPages" => max(1, (int) ceil($total / $per_page)),
],
], 200);
}
public static function update_status($request)
{
$id = (int) $request->get_param("id");
$status = self::action_status($request->get_param("status"));
if (!$id || !$status || !get_comment($id)) {
return new \WP_Error("aurora_comment_status_invalid", __("Invalid comment action.", "aurora-admin"), ["status" => 400]);
}
$result = wp_set_comment_status($id, $status);
if (!$result) {
return new \WP_Error("aurora_comment_status_failed", __("Could not update the comment.", "aurora-admin"), ["status" => 500]);
}
return new \WP_REST_Response(["success" => true, "message" => __("Comment updated.", "aurora-admin")], 200);
}
public static function delete_comment($request)
{
$id = (int) $request->get_param("id");
if (!$id || !get_comment($id)) {
return new \WP_Error("aurora_comment_delete_invalid", __("Invalid comment.", "aurora-admin"), ["status" => 400]);
}
$result = wp_delete_comment($id, true);
if (!$result) {
return new \WP_Error("aurora_comment_delete_failed", __("Could not delete the comment.", "aurora-admin"), ["status" => 500]);
}
return new \WP_REST_Response(["success" => true, "message" => __("Comment deleted.", "aurora-admin")], 200);
}
private static function serialize_comment($comment)
{
$post_id = (int) $comment->comment_post_ID;
$status = wp_get_comment_status($comment);
return [
"id" => (int) $comment->comment_ID,
"author" => $comment->comment_author ?: __("Anonymous", "aurora-admin"),
"email" => $comment->comment_author_email,
"avatar" => get_avatar_url($comment, ["size" => 64]),
"content" => wp_trim_words(wp_strip_all_tags($comment->comment_content), 28),
"rawContent" => $comment->comment_content,
"postId" => $post_id,
"postTitle" => get_the_title($post_id) ?: __("(No title)", "aurora-admin"),
"postEditUrl" => get_edit_post_link($post_id, "raw") ?: "",
"date" => mysql2date("M j, Y", $comment->comment_date),
"relativeDate" => sprintf(__("%s ago", "aurora-admin"), human_time_diff(strtotime($comment->comment_date_gmt), current_time("timestamp", true))),
"status" => $status,
"statusLabel" => self::status_label($status),
"editUrl" => admin_url("comment.php?action=editcomment&c=" . (int) $comment->comment_ID),
"canEdit" => current_user_can("edit_comment", (int) $comment->comment_ID),
];
}
private static function stats()
{
$counts = wp_count_comments();
$approved = isset($counts->approved) ? (int) $counts->approved : 0;
$pending = isset($counts->moderated) ? (int) $counts->moderated : 0;
$spam = isset($counts->spam) ? (int) $counts->spam : 0;
$trash = isset($counts->trash) ? (int) $counts->trash : 0;
return [
"all" => $approved + $pending + $spam,
"approved" => $approved,
"pending" => $pending,
"spam" => $spam,
"trash" => $trash,
];
}
private static function query_status($status)
{
switch ($status) {
case "approved":
return "approve";
case "pending":
return "hold";
case "spam":
case "trash":
return $status;
default:
return "all";
}
}
private static function action_status($status)
{
switch ($status) {
case "approve":
case "hold":
case "spam":
case "trash":
return $status;
default:
return "";
}
}
private static function status_label($status)
{
switch ($status) {
case "approved":
return __("Approved", "aurora-admin");
case "unapproved":
return __("Pending", "aurora-admin");
case "spam":
return __("Spam", "aurora-admin");
case "trash":
return __("Trash", "aurora-admin");
default:
return ucfirst($status);
}
}
}
+1 -1
View File
@@ -141,7 +141,7 @@ class DashboardData
"spam" => (int) $counts->spam,
"total" => (int) $counts->approved,
"latest" => $latest,
"listUrl" => admin_url("edit-comments.php"),
"listUrl" => admin_url("admin.php?page=aurora-admin-comments"),
];
}
+14
View File
@@ -78,6 +78,16 @@ class MenuSerializer
$sub_title = self::clean_title(isset($sub[0]) ? $sub[0] : "");
$sub_slug = isset($sub[2]) ? $sub[2] : "";
// Aurora's own sub-apps (Menu Creator, Comments, etc.) are
// presented in the sidebar's dedicated Aurora nav section
// instead — skip them here so they aren't duplicated. Most are
// parented under "aurora-admin" itself, already skipped above
// wholesale, but "aurora-admin-plugins" is parented under the
// real "plugins.php" menu and needs this explicit check.
if (strpos($sub_slug, "aurora-admin-") === 0) {
continue;
}
$entry["submenu"][] = [
"id" => $sub_slug,
"title" => $sub_title["title"],
@@ -136,6 +146,10 @@ class MenuSerializer
return admin_url("plugins.php?page=aurora-admin-plugins");
}
if ($slug === "edit-comments.php") {
return admin_url("admin.php?page=aurora-admin-comments");
}
// Already an absolute URL.
if (preg_match('#^https?://#', $slug)) {
return $slug;
+12 -3
View File
@@ -34,8 +34,17 @@ class Shell
}
/**
* Whether the shell should run on the current request. Skipped on a few
* full-screen native experiences that own the whole viewport.
* Whether the shell should run on the current request. Skipped on the
* Customizer (its own full-screen live-preview UI with a fundamentally
* different structure Aurora doesn't attempt to frame). The Site Editor
* used to be excluded here too — it's now handled instead: it turns out
* to be built on the same @wordpress/interface package as the block
* editor (post.php/post-new.php both get body.block-editor-page too),
* so shell-frame.css/windowChrome.js's existing block-editor handling
* covers it once #site-editor (its actual position:fixed, full-viewport
* root — unlike the post editor, its .interface-interface-skeleton is
* merely position:relative, nested inside #site-editor) is framed the
* same way .interface-interface-skeleton is everywhere else.
*/
private static function is_enabled()
{
@@ -44,7 +53,7 @@ class Shell
}
$screen = function_exists("get_current_screen") ? get_current_screen() : null;
if ($screen && in_array($screen->id, ["site-editor", "customize"], true)) {
if ($screen && in_array($screen->id, ["customize"], true)) {
return false;
}