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:
co-authored by
Claude Sonnet 5
parent
b0b0a9999a
commit
8153d4397c
@@ -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
@@ -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"])) : "";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user