Files
aurora-wordpress-admin/admin/src/Pages/SubApps.php
T

250 lines
9.0 KiB
PHP
Raw Normal View History

2026-07-03 02:53:29 -05:00
<?php
namespace AuroraAdmin\Pages;
use AuroraAdmin\Utility\Assets;
use AuroraAdmin\Licensing\License;
use AuroraAdmin\Options\Settings;
2026-07-03 02:53:29 -05:00
defined("ABSPATH") || exit();
/**
* 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()).
2026-07-03 02:53:29 -05:00
*/
class SubApps
{
// "feature" ties a page to a License::PAID_FEATURES key — null means the
// page is free. Locked pages still register normally (real capability,
// real URL) but render() swaps in an upgrade prompt instead of the real
// feature when unlicensed, and maybe_enqueue() loads that prompt's tiny
// JS bundle instead of the real (much heavier) feature bundle.
2026-07-03 02:53:29 -05:00
const PAGES = [
"aurora-admin-menu-creator" => ["title" => "Menu Creator", "entry" => "menu-creator", "feature" => null],
"aurora-admin-notices" => ["title" => "Admin Notices", "entry" => "admin-notices", "feature" => null],
"aurora-admin-roles" => ["title" => "Role Editor", "entry" => "role-editor", "feature" => "role-editor"],
"aurora-admin-activity-log" => ["title" => "Activity Log", "entry" => "activity-log", "feature" => "activity-log"],
"aurora-admin-database" => ["title" => "Database Explorer", "entry" => "database-explorer", "feature" => "database-explorer"],
"aurora-admin-plugins" => ["title" => "Installed Plugins", "entry" => "plugins", "feature" => null],
"aurora-admin-comments" => ["title" => "Comments", "entry" => "comments", "feature" => null],
"aurora-admin-posts" => ["title" => "Posts", "entry" => "posts", "feature" => null],
"aurora-admin-pages" => ["title" => "Pages", "entry" => "pages", "feature" => null],
"aurora-admin-media" => ["title" => "Media Library", "entry" => "media", "feature" => null],
"aurora-admin-users" => ["title" => "Users", "entry" => "users", "feature" => null],
2026-07-03 02:53:29 -05:00
];
private static $hook_suffixes = [];
public function __construct()
{
add_action("admin_menu", [self::class, "register_menus"]);
add_action("admin_enqueue_scripts", [self::class, "maybe_enqueue"]);
add_action("admin_init", [self::class, "redirect_native_pages"]);
2026-07-03 02:53:29 -05:00
}
public static function register_menus()
{
foreach (self::PAGES as $slug => $page) {
$parent = self::parent_slug($slug);
2026-07-03 02:53:29 -05:00
$hook = add_submenu_page(
$parent,
2026-07-03 02:53:29 -05:00
__($page["title"], "aurora-admin"),
__($page["title"], "aurora-admin"),
self::capability($slug),
2026-07-03 02:53:29 -05:00
$slug,
[self::class, "render"]
);
self::$hook_suffixes[$slug] = $hook;
}
}
private static function parent_slug($slug)
2026-07-03 02:53:29 -05:00
{
if ($slug === "aurora-admin-plugins") {
return "plugins.php";
2026-07-03 02:53:29 -05:00
}
if ($slug === "aurora-admin-posts") {
return "edit.php";
}
if ($slug === "aurora-admin-pages") {
return "edit.php?post_type=page";
}
if ($slug === "aurora-admin-media") {
return "upload.php";
}
if ($slug === "aurora-admin-users") {
return "users.php";
}
return "aurora-admin";
}
private static function capability($slug)
{
if ($slug === "aurora-admin-plugins") {
return "activate_plugins";
}
if ($slug === "aurora-admin-comments" || $slug === "aurora-admin-posts") {
return "edit_posts";
}
if ($slug === "aurora-admin-pages") {
return "edit_pages";
}
if ($slug === "aurora-admin-media") {
return "upload_files";
}
if ($slug === "aurora-admin-users") {
return "list_users";
}
return "manage_options";
2026-07-03 02:53:29 -05:00
}
public static function maybe_enqueue($hook_suffix)
{
$slug = array_search($hook_suffix, self::$hook_suffixes, true);
if ($slug === false || !isset(self::PAGES[$slug])) {
return;
}
$page = self::PAGES[$slug];
if ($page["feature"] && !License::is_feature_available($page["feature"])) {
Assets::enqueue("aurora-admin-locked-feature", "entries/locked-feature.js");
return;
}
Assets::enqueue("aurora-admin-" . $page["entry"], "entries/" . $page["entry"] . ".js");
2026-07-03 02:53:29 -05:00
}
public static function redirect_native_pages()
{
global $pagenow;
if (isset($_GET["page"]) || wp_doing_ajax()) {
return;
}
// Settings > Comments > "Modern comments page" — previously always
// redirected regardless of this setting; default true so existing
// installs keep their current behavior until explicitly turned off.
if (
$pagenow === "edit-comments.php" &&
current_user_can("edit_posts") &&
Settings::get("modern_comments", true)
) {
wp_safe_redirect(admin_url("admin.php?page=aurora-admin-comments"));
exit;
}
// Settings > Posts > "Modern posts table". Only the default post-list
// view (edit.php with no post_type, or post_type=post) redirects —
// edit.php?post_type=page (Pages) and other custom post types are
// untouched, since only Posts has a replacement page.
$post_type = isset($_GET["post_type"]) ? sanitize_key(wp_unslash($_GET["post_type"])) : "post";
if (
$pagenow === "edit.php" &&
$post_type === "post" &&
current_user_can("edit_posts") &&
Settings::get("modern_posts", true)
) {
wp_safe_redirect(admin_url("admin.php?page=aurora-admin-posts"));
exit;
}
// Settings > Pages > "Modern pages table".
if (
$pagenow === "edit.php" &&
$post_type === "page" &&
current_user_can("edit_pages") &&
Settings::get("modern_pages", true)
) {
wp_safe_redirect(admin_url("admin.php?page=aurora-admin-pages"));
exit;
}
// Settings > Media > "Modern media library". upload.php serves both
// the default grid view and ?mode=list — both redirect the same way.
// media-new.php (the upload/drag-drop screen) is a separate file,
// untouched, same as post-new.php for Posts/Pages.
if (
$pagenow === "upload.php" &&
current_user_can("upload_files") &&
Settings::get("modern_media", true)
) {
wp_safe_redirect(admin_url("admin.php?page=aurora-admin-media"));
exit;
}
// Settings > Users > "Modern users table". user-new.php (account
// creation) is a separate file, untouched.
if (
$pagenow === "users.php" &&
current_user_can("list_users") &&
Settings::get("modern_users", true)
) {
wp_safe_redirect(admin_url("admin.php?page=aurora-admin-users"));
exit;
}
}
2026-07-03 02:53:29 -05:00
public static function render()
{
$page = isset($_GET["page"]) ? sanitize_text_field(wp_unslash($_GET["page"])) : "";
if (!isset(self::PAGES[$page])) {
return;
}
$feature = self::PAGES[$page]["feature"];
if ($feature && !License::is_feature_available($feature)) {
$data = ["feature" => License::feature_label($feature)];
?>
<div data-aurora-locked-feature="<?php echo esc_attr(wp_json_encode($data)); ?>"></div>
<?php
return;
}
2026-07-03 02:53:29 -05:00
$roles = [];
foreach (wp_roles()->roles as $slug => $info) {
$roles[] = ["value" => $slug, "label" => translate_user_role($info["name"])];
}
global $wpdb;
$data = [
"restUrl" => esc_url_raw(get_rest_url()),
"restNonce" => wp_create_nonce("wp_rest"),
"userName" => wp_get_current_user()->display_name,
"roles" => $roles,
"tablePrefix" => $wpdb->prefix,
"newPostUrl" => esc_url_raw(admin_url("post-new.php")),
"newPageUrl" => esc_url_raw(admin_url("post-new.php?post_type=page")),
"newMediaUrl" => esc_url_raw(admin_url("media-new.php")),
"newUserUrl" => esc_url_raw(admin_url("user-new.php")),
2026-07-03 02:53:29 -05:00
];
$root_id = "aurora-admin-" . self::PAGES[$page]["entry"] . "-root";
?>
<div
id="<?php echo esc_attr($root_id); ?>"
data-aurora-admin="<?php echo esc_attr(wp_json_encode($data)); ?>"
></div>
<?php
}
}