Snapshot synced v3.6.1 working state as baseline
Machine-sync snapshot: the working tree carried the full 3.0.0-3.6.1 arc (licensing/FileManager/DatabaseExplorer removal, all new replacement screens and REST controllers) that had not been committed on this machine. Captured here as a safety baseline before starting the Adminify-parity revamp. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
07d77bda57
commit
e2db2ad4b5
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Pages;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* Registers the Appearance-area screens for the "full admin takeover"
|
||||
* initiative: Nav Menus, Themes, Widgets (all real hijacked replacements).
|
||||
*
|
||||
* Theme Editor is deliberately NOT hijacked or replaced — arbitrary
|
||||
* in-browser PHP/CSS file editing is exactly the category of functionality
|
||||
* WordPress.org's Plugin Directory policy excludes (see the removed
|
||||
* File Manager sub-app, now distributed as a separate standalone plugin
|
||||
* instead of being bundled here). Visits to theme-editor.php fall through
|
||||
* to WordPress core's own Theme Editor unmodified, which already respects
|
||||
* DISALLOW_FILE_EDIT itself.
|
||||
*/
|
||||
class AppearancePages
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
ScreenHijack::register(
|
||||
"load-nav-menus.php",
|
||||
"edit_theme_options",
|
||||
"full_admin_takeover",
|
||||
"nav-menus",
|
||||
"aurora-admin-nav-menus-root"
|
||||
);
|
||||
|
||||
ScreenHijack::register(
|
||||
"load-themes.php",
|
||||
"switch_themes",
|
||||
"full_admin_takeover",
|
||||
"themes",
|
||||
"aurora-admin-themes-root",
|
||||
null,
|
||||
null,
|
||||
function () {
|
||||
return [
|
||||
"customizeUrl" => esc_url_raw(admin_url("customize.php")),
|
||||
// Native theme-install.php covers both "browse WordPress.org" and
|
||||
// "Upload Theme" (a button at the top of that same screen) — not
|
||||
// rebuilt here, same reuse-native-flow-for-add pattern as Plugins'
|
||||
// addUrl (plugin-install.php) and Media's newMediaUrl
|
||||
// (media-new.php).
|
||||
"addThemeUrl" => current_user_can("install_themes") ? esc_url_raw(admin_url("theme-install.php")) : "",
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
ScreenHijack::register(
|
||||
"load-widgets.php",
|
||||
"edit_theme_options",
|
||||
"full_admin_takeover",
|
||||
"widgets",
|
||||
"aurora-admin-widgets-root"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Pages;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* Registers every native screen Aurora replaces in place (same URL, no
|
||||
* redirect) via ScreenHijack. Kept as its own bootstrap class rather than
|
||||
* inline calls in AuroraAdmin's constructor since this list is expected to
|
||||
* grow past a dozen entries as more native screens are covered.
|
||||
*
|
||||
* Each of these previously lived in SubApps::PAGES as a registered
|
||||
* "aurora-admin-*" submenu with a matching redirect_native_pages() branch.
|
||||
* Plugins is the one screen that still uses that mechanism — plugins.php
|
||||
* prints output from within its own load hook in a way that can't be
|
||||
* cleanly buffered, confirmed by the old reference plugin's own abandoned
|
||||
* attempt at hijacking it — so it's deliberately not listed here.
|
||||
*/
|
||||
class HijackedNativePages
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
ScreenHijack::register(
|
||||
"load-edit-comments.php",
|
||||
"moderate_comments",
|
||||
"modern_comments",
|
||||
"comments",
|
||||
"aurora-admin-comments-root"
|
||||
);
|
||||
|
||||
// "edit.php" is shared by Posts (the default post type) and every other
|
||||
// post type registered on the site, including Pages — screen_match is
|
||||
// what tells them apart; other custom post types fall through untouched.
|
||||
ScreenHijack::register(
|
||||
"load-edit.php",
|
||||
"edit_posts",
|
||||
"modern_posts",
|
||||
"posts",
|
||||
"aurora-admin-posts-root",
|
||||
function ($screen) {
|
||||
return $screen->post_type === "post";
|
||||
},
|
||||
null,
|
||||
function () {
|
||||
return ["newPostUrl" => esc_url_raw(admin_url("post-new.php"))];
|
||||
}
|
||||
);
|
||||
|
||||
ScreenHijack::register(
|
||||
"load-edit.php",
|
||||
"edit_pages",
|
||||
"modern_pages",
|
||||
"pages",
|
||||
"aurora-admin-pages-root",
|
||||
function ($screen) {
|
||||
return $screen->post_type === "page";
|
||||
},
|
||||
null,
|
||||
function () {
|
||||
return ["newPageUrl" => esc_url_raw(admin_url("post-new.php?post_type=page"))];
|
||||
}
|
||||
);
|
||||
|
||||
ScreenHijack::register(
|
||||
"load-upload.php",
|
||||
"upload_files",
|
||||
"modern_media",
|
||||
"media",
|
||||
"aurora-admin-media-root",
|
||||
null,
|
||||
null,
|
||||
function () {
|
||||
return [
|
||||
"newMediaUrl" => esc_url_raw(admin_url("media-new.php")),
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
ScreenHijack::register(
|
||||
"load-users.php",
|
||||
"list_users",
|
||||
"modern_users",
|
||||
"users",
|
||||
"aurora-admin-users-root",
|
||||
null,
|
||||
null,
|
||||
function () {
|
||||
return ["newUserUrl" => esc_url_raw(admin_url("user-new.php"))];
|
||||
}
|
||||
);
|
||||
|
||||
// media-new.php / user-new.php: Media Upload and Add New User. Free,
|
||||
// gated by the same modern_media / modern_users toggles as their
|
||||
// already-free list-view companions (Media Library / Users) — no
|
||||
// reason to paywall mid-workflow between "browse" and "add".
|
||||
ScreenHijack::register(
|
||||
"load-media-new.php",
|
||||
"upload_files",
|
||||
"modern_media",
|
||||
"media-upload",
|
||||
"aurora-admin-media-upload-root",
|
||||
null,
|
||||
null,
|
||||
function () {
|
||||
return ["libraryUrl" => esc_url_raw(admin_url("upload.php"))];
|
||||
}
|
||||
);
|
||||
|
||||
ScreenHijack::register(
|
||||
"load-user-new.php",
|
||||
"create_users",
|
||||
"modern_users",
|
||||
"add-user",
|
||||
"aurora-admin-add-user-root",
|
||||
null,
|
||||
null,
|
||||
function () {
|
||||
$roles = [];
|
||||
foreach (wp_roles()->roles as $slug => $info) {
|
||||
$roles[] = ["value" => $slug, "label" => translate_user_role($info["name"])];
|
||||
}
|
||||
|
||||
return [
|
||||
"usersUrl" => esc_url_raw(admin_url("users.php")),
|
||||
"roles" => $roles,
|
||||
"defaultRole" => get_option("default_role", "subscriber"),
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
// profile.php: reskins only WordPress's own fields (name, contact
|
||||
// info, bio, password). If any plugin has hooked show_user_profile or
|
||||
// personal_options — the two documented extension points for adding
|
||||
// fields to this exact screen (2FA setup, WooCommerce customer fields,
|
||||
// etc.) — this deliberately does NOT hijack, falling through to the
|
||||
// real native screen instead, since a Vue reskin has no way to host
|
||||
// another plugin's raw HTML/JS output safely. Checked at hijack time
|
||||
// (load-profile.php), which fires after every plugin's own init/
|
||||
// admin_init hooks have already registered their callbacks.
|
||||
ScreenHijack::register(
|
||||
"load-profile.php",
|
||||
"read",
|
||||
"modern_users",
|
||||
"profile",
|
||||
"aurora-admin-profile-root",
|
||||
function () {
|
||||
return !has_action("show_user_profile") && !has_action("personal_options");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Pages;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* Registers the 5 native Settings screens (General/Writing/Reading/
|
||||
* Discussion/Permalinks) via ScreenHijack, part of the "full admin
|
||||
* takeover" initiative (see PROJECT_STATUS.md). Unlike Posts/Pages/Media/
|
||||
* Users/Comments, these are gated on the master `full_admin_takeover`
|
||||
* setting (default off), since they're new ground rather than a migration
|
||||
* of something already shipped.
|
||||
*
|
||||
* All 5 share one Vue entry (native-settings.js / NativeSettings.vue) and
|
||||
* one mount id — only one native Settings screen is ever hijacked per
|
||||
* request, so there's no collision. Each just supplies a different field
|
||||
* schema + current values via its data_callback. Every field except
|
||||
* Permalinks' structure is one of core's own REST-exposed settings
|
||||
* (`/wp/v2/settings`), so the Vue side reads/writes that route directly;
|
||||
* Permalinks has no core REST route for rewrite rules and uses
|
||||
* PermalinksData's own aurora-admin/v1/permalinks route instead.
|
||||
*/
|
||||
class NativeSettingsPages
|
||||
{
|
||||
// Core's REST settings route exposes some fields under a different name
|
||||
// than the wp_options row they actually live in (registered via
|
||||
// register_setting()'s show_in_rest 'name' argument) — reading them back
|
||||
// with get_option() needs the real option name, not the REST one, or the
|
||||
// field just reads back empty.
|
||||
const OPTION_NAME_MAP = [
|
||||
"title" => "blogname",
|
||||
"description" => "blogdescription",
|
||||
"url" => "siteurl",
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
self::register_screen(
|
||||
"load-options-general.php",
|
||||
"general",
|
||||
"General",
|
||||
"Site title, tagline, timezone, and date/time formats.",
|
||||
[
|
||||
["key" => "title", "type" => "text", "label" => "Site Title"],
|
||||
["key" => "description", "type" => "text", "label" => "Tagline"],
|
||||
["key" => "url", "type" => "text", "label" => "Site Address (URL)", "readonly" => true],
|
||||
["key" => "admin_email", "type" => "text", "label" => "Administration Email Address"],
|
||||
["key" => "timezone_string", "type" => "text", "label" => "Timezone", "placeholder" => "e.g. America/New_York"],
|
||||
["key" => "date_format", "type" => "text", "label" => "Date Format"],
|
||||
["key" => "time_format", "type" => "text", "label" => "Time Format"],
|
||||
[
|
||||
"key" => "start_of_week",
|
||||
"type" => "select",
|
||||
"label" => "Week Starts On",
|
||||
"options" => [
|
||||
["value" => "0", "label" => "Sunday"],
|
||||
["value" => "1", "label" => "Monday"],
|
||||
["value" => "2", "label" => "Tuesday"],
|
||||
["value" => "3", "label" => "Wednesday"],
|
||||
["value" => "4", "label" => "Thursday"],
|
||||
["value" => "5", "label" => "Friday"],
|
||||
["value" => "6", "label" => "Saturday"],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
self::register_screen(
|
||||
"load-options-writing.php",
|
||||
"writing",
|
||||
"Writing",
|
||||
"Defaults used when publishing new content.",
|
||||
[
|
||||
// use_smilies is registered in core's REST schema (register_initial_settings())
|
||||
// but was never added here — a real gap, not a deliberate omission.
|
||||
["key" => "use_smilies", "type" => "toggle", "label" => "Convert emoticons like :-) and :-P to graphics on display"],
|
||||
["key" => "default_category", "type" => "select", "label" => "Default Post Category", "options" => self::category_options()],
|
||||
["key" => "default_post_format", "type" => "select", "label" => "Default Post Format", "options" => self::post_format_options()],
|
||||
]
|
||||
);
|
||||
|
||||
self::register_screen(
|
||||
"load-options-reading.php",
|
||||
"reading",
|
||||
"Reading",
|
||||
"What visitors see on the front page and how many posts are listed.",
|
||||
[
|
||||
[
|
||||
"key" => "show_on_front",
|
||||
"type" => "select",
|
||||
"label" => "Homepage Displays",
|
||||
"options" => [
|
||||
["value" => "posts", "label" => "Your latest posts"],
|
||||
["value" => "page", "label" => "A static page"],
|
||||
],
|
||||
],
|
||||
[
|
||||
"key" => "page_on_front",
|
||||
"type" => "select",
|
||||
"label" => "Homepage",
|
||||
"options" => self::page_options(),
|
||||
"showIf" => ["key" => "show_on_front", "equals" => "page"],
|
||||
],
|
||||
[
|
||||
"key" => "page_for_posts",
|
||||
"type" => "select",
|
||||
"label" => "Posts Page",
|
||||
"options" => self::page_options(),
|
||||
"showIf" => ["key" => "show_on_front", "equals" => "page"],
|
||||
],
|
||||
["key" => "posts_per_page", "type" => "text", "label" => "Posts Per Page"],
|
||||
]
|
||||
);
|
||||
|
||||
self::register_screen(
|
||||
"load-options-discussion.php",
|
||||
"discussion",
|
||||
"Discussion",
|
||||
"Default article settings for pings and comments.",
|
||||
[
|
||||
// Saved via DiscussionData's own /discussion-settings route, not
|
||||
// wp/v2/settings — core's REST schema only exposes the first two
|
||||
// of these fields (see DiscussionData.php's docblock for why).
|
||||
["key" => "default_pingback_flag", "type" => "toggle", "label" => "Attempt to notify any blogs linked to from the article"],
|
||||
[
|
||||
"key" => "default_ping_status",
|
||||
"type" => "select",
|
||||
"label" => "Allow link notifications from other blogs (pingbacks and trackbacks)",
|
||||
"options" => [
|
||||
["value" => "open", "label" => "Open"],
|
||||
["value" => "closed", "label" => "Closed"],
|
||||
],
|
||||
],
|
||||
[
|
||||
"key" => "default_comment_status",
|
||||
"type" => "select",
|
||||
"label" => "Allow people to submit comments on new posts",
|
||||
"options" => [
|
||||
["value" => "open", "label" => "Open"],
|
||||
["value" => "closed", "label" => "Closed"],
|
||||
],
|
||||
],
|
||||
|
||||
["key" => "require_name_email", "type" => "toggle", "label" => "Comment author must fill out name and email"],
|
||||
["key" => "comment_registration", "type" => "toggle", "label" => "Users must be registered and logged in to comment"],
|
||||
["key" => "close_comments_for_old_posts", "type" => "toggle", "label" => "Automatically close comments on old posts"],
|
||||
[
|
||||
"key" => "close_comments_days_old",
|
||||
"type" => "text",
|
||||
"label" => "Close comments when post is how many days old",
|
||||
"showIf" => ["key" => "close_comments_for_old_posts", "equals" => true],
|
||||
],
|
||||
["key" => "show_comments_cookies_opt_in", "type" => "toggle", "label" => "Show comments cookies opt-in checkbox"],
|
||||
["key" => "thread_comments", "type" => "toggle", "label" => "Enable threaded (nested) comments"],
|
||||
[
|
||||
"key" => "thread_comments_depth",
|
||||
"type" => "select",
|
||||
"label" => "Number of levels for threaded (nested) comments",
|
||||
"options" => array_map(
|
||||
function ($n) {
|
||||
return ["value" => (string) $n, "label" => (string) $n];
|
||||
},
|
||||
range(2, 10)
|
||||
),
|
||||
"showIf" => ["key" => "thread_comments", "equals" => true],
|
||||
],
|
||||
["key" => "page_comments", "type" => "toggle", "label" => "Break comments into pages"],
|
||||
[
|
||||
"key" => "comments_per_page",
|
||||
"type" => "text",
|
||||
"label" => "Top level comments per page",
|
||||
"showIf" => ["key" => "page_comments", "equals" => true],
|
||||
],
|
||||
[
|
||||
"key" => "default_comments_page",
|
||||
"type" => "select",
|
||||
"label" => "Comments page to display by default",
|
||||
"options" => [
|
||||
["value" => "newest", "label" => "Last page"],
|
||||
["value" => "oldest", "label" => "First page"],
|
||||
],
|
||||
"showIf" => ["key" => "page_comments", "equals" => true],
|
||||
],
|
||||
[
|
||||
"key" => "comment_order",
|
||||
"type" => "select",
|
||||
"label" => "Comments to display at the top of each page",
|
||||
"options" => [
|
||||
["value" => "asc", "label" => "Older"],
|
||||
["value" => "desc", "label" => "Newer"],
|
||||
],
|
||||
],
|
||||
|
||||
["key" => "comments_notify", "type" => "toggle", "label" => "Email me whenever: anyone posts a comment"],
|
||||
["key" => "moderation_notify", "type" => "toggle", "label" => "Email me whenever: a comment is held for moderation"],
|
||||
["key" => "wp_notes_notify", "type" => "toggle", "label" => "Email me whenever: WordPress news and updates", "default" => true],
|
||||
|
||||
["key" => "comment_moderation", "type" => "toggle", "label" => "Comment must be manually approved"],
|
||||
["key" => "comment_previously_approved", "type" => "toggle", "label" => "Comment author must have a previously approved comment"],
|
||||
["key" => "comment_max_links", "type" => "text", "label" => "Hold a comment in the queue if it contains this many links"],
|
||||
[
|
||||
"key" => "moderation_keys",
|
||||
"type" => "textarea",
|
||||
"label" => "Comment Moderation",
|
||||
"description" => "When a comment contains any of these words in its content, author name, URL, email, IP address, or browser's user agent string, it will be held in the moderation queue. One word or IP address per line.",
|
||||
],
|
||||
[
|
||||
"key" => "disallowed_keys",
|
||||
"type" => "textarea",
|
||||
"label" => "Disallowed Comment Keys",
|
||||
"description" => "When a comment contains any of these words in its content, author name, URL, email, IP address, or browser's user agent string, it will be put in the Trash. One word or IP address per line.",
|
||||
],
|
||||
|
||||
["key" => "show_avatars", "type" => "toggle", "label" => "Show Avatars"],
|
||||
[
|
||||
"key" => "avatar_rating",
|
||||
"type" => "select",
|
||||
"label" => "Maximum Rating",
|
||||
"options" => [
|
||||
["value" => "G", "label" => "G — Suitable for all audiences"],
|
||||
["value" => "PG", "label" => "PG — Possibly offensive, usually for audiences 13 and above"],
|
||||
["value" => "R", "label" => "R — Intended for adult audiences above 17"],
|
||||
["value" => "X", "label" => "X — Even more mature than above"],
|
||||
],
|
||||
"showIf" => ["key" => "show_avatars", "equals" => true],
|
||||
],
|
||||
[
|
||||
"key" => "avatar_default",
|
||||
"type" => "select",
|
||||
"label" => "Default Avatar",
|
||||
"options" => [
|
||||
["value" => "mystery", "label" => "Mystery Person"],
|
||||
["value" => "blank", "label" => "Blank"],
|
||||
["value" => "gravatar_default", "label" => "Gravatar Logo"],
|
||||
["value" => "identicon", "label" => "Identicon (Generated)"],
|
||||
["value" => "wavatar", "label" => "Wavatar (Generated)"],
|
||||
["value" => "monsterid", "label" => "MonsterID (Generated)"],
|
||||
["value" => "retro", "label" => "Retro (Generated)"],
|
||||
["value" => "robohash", "label" => "RoboHash (Generated)"],
|
||||
["value" => "initials", "label" => "Initials (Generated)"],
|
||||
["value" => "color", "label" => "Color (Generated)"],
|
||||
],
|
||||
"default" => "mystery",
|
||||
"showIf" => ["key" => "show_avatars", "equals" => true],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
self::register_screen(
|
||||
"load-options-permalink.php",
|
||||
"permalinks",
|
||||
"Permalinks",
|
||||
"The URL structure used for posts and pages.",
|
||||
[["key" => "permalink_structure", "type" => "text", "label" => "Custom Structure"]]
|
||||
);
|
||||
}
|
||||
|
||||
private static function category_options()
|
||||
{
|
||||
$categories = get_categories(["hide_empty" => false]);
|
||||
return array_map(
|
||||
function ($cat) {
|
||||
return ["value" => (string) $cat->term_id, "label" => $cat->name];
|
||||
},
|
||||
$categories
|
||||
);
|
||||
}
|
||||
|
||||
private static function post_format_options()
|
||||
{
|
||||
// get_post_format_strings() doesn't include "Standard" (the '0'/default
|
||||
// value native WP stores when no format is set) — added manually to
|
||||
// match the native Writing screen's own dropdown exactly.
|
||||
$options = [["value" => "0", "label" => "Standard"]];
|
||||
foreach (get_post_format_strings() as $slug => $label) {
|
||||
if ($slug === "standard") {
|
||||
continue;
|
||||
}
|
||||
$options[] = ["value" => $slug, "label" => $label];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private static function page_options()
|
||||
{
|
||||
$options = [["value" => "0", "label" => "— Select —"]];
|
||||
$pages = get_pages();
|
||||
foreach ($pages as $page) {
|
||||
$options[] = ["value" => (string) $page->ID, "label" => $page->post_title ?: "(no title)"];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
private static function register_screen($hook_suffix, $screen, $title, $subtitle, $fields)
|
||||
{
|
||||
ScreenHijack::register(
|
||||
$hook_suffix,
|
||||
"manage_options",
|
||||
"full_admin_takeover",
|
||||
"native-settings",
|
||||
"aurora-admin-native-settings-root",
|
||||
null,
|
||||
null,
|
||||
function () use ($screen, $title, $subtitle, $fields) {
|
||||
$values = [];
|
||||
foreach ($fields as $field) {
|
||||
$option_name = self::OPTION_NAME_MAP[$field["key"]] ?? $field["key"];
|
||||
$value = get_option($option_name, $field["default"] ?? "");
|
||||
// Toggle fields need a real boolean — get_option() returns
|
||||
// whatever's actually stored ("1"/""/true/false depending on how
|
||||
// it was last saved), and a falsy non-bool like "" would
|
||||
// otherwise still satisfy Vue's `!!val` check inconsistently.
|
||||
$values[$field["key"]] = $field["type"] === "toggle" ? (bool) $value : $value;
|
||||
}
|
||||
|
||||
return [
|
||||
"screen" => $screen,
|
||||
"title" => $title,
|
||||
"subtitle" => $subtitle,
|
||||
"fields" => $fields,
|
||||
"values" => $values,
|
||||
];
|
||||
},
|
||||
"admin-takeover"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Pages;
|
||||
|
||||
use AuroraAdmin\Utility\Assets;
|
||||
use AuroraAdmin\Options\Settings;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* Replaces a native WordPress admin screen with an Aurora Vue page without
|
||||
* changing its URL, instead of SubApps' registered-page + redirect approach.
|
||||
*
|
||||
* WordPress fires "load-{hook_suffix}" once a screen is confirmed but before
|
||||
* it prints anything, so the recipe is: check access, buffer everything the
|
||||
* native screen would have printed, throw the buffer away once WordPress
|
||||
* reaches admin_footer, and print Aurora's own mount div in its place. The
|
||||
* URL the browser sees never changes, so there's no separate slug that can
|
||||
* fall out of sync with a plugin's own redirect, a bookmark, or any other
|
||||
* direct visit — the exact bug class SubApps::redirect_native_pages() has to
|
||||
* work around case by case (see its Plugins comment).
|
||||
*
|
||||
* Not every native screen can be hijacked this way — some (plugins.php is
|
||||
* the confirmed case so far) print output from within the load-{page}.php
|
||||
* callback itself or otherwise short-circuit before in_admin_header fires.
|
||||
* Those stay on SubApps' redirect mechanism instead.
|
||||
*/
|
||||
class ScreenHijack
|
||||
{
|
||||
// Set true the moment any registered hijack actually activates for the
|
||||
// current request (all gating checks passed). Shell::is_own_page() reads
|
||||
// this — hijacked screens keep their native URL (no ?page=aurora-admin),
|
||||
// so the shell can't otherwise tell them apart from a real native page
|
||||
// when deciding whether to load native-dark.css's own-page exclusion.
|
||||
private static $hijack_active = false;
|
||||
|
||||
public static function is_hijack_active()
|
||||
{
|
||||
return self::$hijack_active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $hook_suffix "load-{page}.php", e.g. "load-edit.php"
|
||||
* @param string $capability Required capability, checked with current_user_can()
|
||||
* @param string|null $setting_key Settings::get() key gating this hijack
|
||||
* (default true), or null to always hijack once capability passes
|
||||
* @param string $entry Vite entry name (without "src/entries/" or ".js")
|
||||
* @param string $root_id DOM id for the mount div
|
||||
* @param callable|null $screen_match fn(WP_Screen $screen): bool — extra
|
||||
* match beyond the hook itself firing, e.g. distinguishing edit.php's
|
||||
* several post types
|
||||
* @param callable|null $short_circuit fn(WP_Query $query): void, hooked to
|
||||
* pre_get_posts only if provided — for list screens where letting
|
||||
* WordPress's own query run would be wasted work
|
||||
* @param callable|null $data_callback fn(): array — extra data merged into
|
||||
* the mount div's data-aurora-admin JSON, beyond the restUrl/restNonce
|
||||
* defaults every page gets
|
||||
*/
|
||||
public static function register(
|
||||
$hook_suffix,
|
||||
$capability,
|
||||
$setting_key,
|
||||
$entry,
|
||||
$root_id,
|
||||
$screen_match = null,
|
||||
$short_circuit = null,
|
||||
$data_callback = null
|
||||
) {
|
||||
add_action(
|
||||
$hook_suffix,
|
||||
function () use (
|
||||
$capability,
|
||||
$setting_key,
|
||||
$entry,
|
||||
$root_id,
|
||||
$screen_match,
|
||||
$short_circuit,
|
||||
$data_callback
|
||||
) {
|
||||
if (!current_user_can($capability)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($setting_key !== null && !Settings::get($setting_key, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($screen_match !== null) {
|
||||
$screen = get_current_screen();
|
||||
if (!$screen || !$screen_match($screen)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self::$hijack_active = true;
|
||||
|
||||
if ($short_circuit !== null) {
|
||||
add_filter("pre_get_posts", $short_circuit);
|
||||
}
|
||||
|
||||
add_action(
|
||||
"in_admin_header",
|
||||
function () {
|
||||
ob_start();
|
||||
},
|
||||
999
|
||||
);
|
||||
|
||||
add_action(
|
||||
"admin_footer",
|
||||
function () use ($entry, $root_id, $data_callback) {
|
||||
// A screen that exited early (its own wp_die(), a mid-render
|
||||
// redirect, a validation failure taking a different code path
|
||||
// than the initial GET) may never have reached in_admin_header,
|
||||
// or may have already closed its own buffer — calling
|
||||
// ob_end_clean() unconditionally in that case throws a notice
|
||||
// and can corrupt the response. Only discard a buffer that's
|
||||
// actually open.
|
||||
if (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
$data = array_merge(
|
||||
[
|
||||
"restUrl" => esc_url_raw(get_rest_url()),
|
||||
"restNonce" => wp_create_nonce("wp_rest"),
|
||||
],
|
||||
$data_callback !== null ? $data_callback() : []
|
||||
);
|
||||
// The buffer just discarded didn't just hold the native
|
||||
// screen's content — in_admin_header fires before WP prints
|
||||
// <div id="wpbody"><div id="wpbody-content">, and those
|
||||
// wrapper divs' closing tags print (in admin-footer.php)
|
||||
// before this admin_footer callback runs, so both got
|
||||
// captured and thrown away too. Re-print them around the
|
||||
// mount div so the DOM WP itself would have produced still
|
||||
// exists — windowChrome.js's traffic-light bar and the shell's
|
||||
// frame CSS both key off #wpbody-content being there.
|
||||
?>
|
||||
<div id="wpbody" role="main">
|
||||
<div id="wpbody-content">
|
||||
<div
|
||||
id="<?php echo esc_attr($root_id); ?>"
|
||||
data-aurora-admin="<?php echo esc_attr(wp_json_encode($data)); ?>"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
add_action("admin_enqueue_scripts", function () use ($entry) {
|
||||
Assets::enqueue("aurora-admin-" . $entry, "entries/" . $entry . ".js");
|
||||
});
|
||||
},
|
||||
5
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ namespace AuroraAdmin\Pages;
|
||||
|
||||
use AuroraAdmin\Utility\Assets;
|
||||
use AuroraAdmin\Options\Settings;
|
||||
use AuroraAdmin\Licensing\License;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
@@ -74,10 +73,6 @@ class SettingsPage
|
||||
"settings" => Settings::get(),
|
||||
"roles" => $roles,
|
||||
"postTypes" => $post_types,
|
||||
// Real-time result from the license server (see License::status()),
|
||||
// not just whether a key is present — drives the status line under
|
||||
// the License key field (active/invalid/site-limit-reached/etc.).
|
||||
"licenseStatus" => License::status(),
|
||||
];
|
||||
?>
|
||||
<div
|
||||
|
||||
+32
-126
@@ -2,7 +2,6 @@
|
||||
namespace AuroraAdmin\Pages;
|
||||
|
||||
use AuroraAdmin\Utility\Assets;
|
||||
use AuroraAdmin\Licensing\License;
|
||||
use AuroraAdmin\Options\Settings;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
@@ -33,25 +32,17 @@ defined("ABSPATH") || exit();
|
||||
*/
|
||||
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.
|
||||
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-file-manager" => ["title" => "File Manager", "entry" => "file-manager", "feature" => "file-manager"],
|
||||
"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],
|
||||
];
|
||||
public static function pages()
|
||||
{
|
||||
return [
|
||||
"aurora-admin-menu-creator" => ["title" => __("Menu Creator", "aurora-admin"), "entry" => "menu-creator"],
|
||||
"aurora-admin-notices" => ["title" => __("Admin Notices", "aurora-admin"), "entry" => "admin-notices"],
|
||||
"aurora-admin-roles" => ["title" => __("Role Editor", "aurora-admin"), "entry" => "role-editor"],
|
||||
"aurora-admin-activity-log" => ["title" => __("Activity Log", "aurora-admin"), "entry" => "activity-log"],
|
||||
"aurora-admin-plugins" => ["title" => __("Installed Plugins", "aurora-admin"), "entry" => "plugins"],
|
||||
"aurora-admin-bug-report" => ["title" => __("Report a Bug", "aurora-admin"), "entry" => "bug-report"],
|
||||
];
|
||||
}
|
||||
|
||||
private static $hook_suffixes = [];
|
||||
|
||||
@@ -64,12 +55,12 @@ class SubApps
|
||||
|
||||
public static function register_menus()
|
||||
{
|
||||
foreach (self::PAGES as $slug => $page) {
|
||||
foreach (self::pages() as $slug => $page) {
|
||||
$parent = self::parent_slug($slug);
|
||||
$hook = add_submenu_page(
|
||||
$parent,
|
||||
__($page["title"], "aurora-admin"),
|
||||
__($page["title"], "aurora-admin"),
|
||||
$page["title"],
|
||||
$page["title"],
|
||||
self::capability($slug),
|
||||
$slug,
|
||||
[self::class, "render"]
|
||||
@@ -83,18 +74,6 @@ class SubApps
|
||||
if ($slug === "aurora-admin-plugins") {
|
||||
return "plugins.php";
|
||||
}
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -103,34 +82,18 @@ class SubApps
|
||||
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";
|
||||
}
|
||||
|
||||
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");
|
||||
$pages = self::pages();
|
||||
if ($slug === false || !isset($pages[$slug])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$page = $pages[$slug];
|
||||
Assets::enqueue("aurora-admin-" . $page["entry"], "entries/" . $page["entry"] . ".js");
|
||||
}
|
||||
|
||||
@@ -138,6 +101,7 @@ class SubApps
|
||||
{
|
||||
global $pagenow;
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only isset() presence check (no value read), used only to skip pages that already have a ?page= param; no state change.
|
||||
if (isset($_GET["page"]) || wp_doing_ajax()) {
|
||||
return;
|
||||
}
|
||||
@@ -161,82 +125,14 @@ class SubApps
|
||||
exit;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
public static function render()
|
||||
{
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only, routes rendering to the matching page's Vue root by slug; no state change.
|
||||
$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
|
||||
$pages = self::pages();
|
||||
if (!isset($pages[$page])) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -258,7 +154,17 @@ class SubApps
|
||||
"newMediaUrl" => esc_url_raw(admin_url("media-new.php")),
|
||||
"newUserUrl" => esc_url_raw(admin_url("user-new.php")),
|
||||
];
|
||||
$root_id = "aurora-admin-" . self::PAGES[$page]["entry"] . "-root";
|
||||
|
||||
// Report a Bug auto-fills its diagnostic fields from these — saves the
|
||||
// reporter from having to look any of it up themselves.
|
||||
if ($page === "aurora-admin-bug-report") {
|
||||
$data["pluginVersion"] = defined("AURORA_ADMIN_VERSION") ? AURORA_ADMIN_VERSION : "";
|
||||
$data["wpVersion"] = get_bloginfo("version");
|
||||
$data["phpVersion"] = phpversion();
|
||||
$data["theme"] = wp_get_theme()->get("Name");
|
||||
}
|
||||
|
||||
$root_id = "aurora-admin-" . $pages[$page]["entry"] . "-root";
|
||||
?>
|
||||
<div
|
||||
id="<?php echo esc_attr($root_id); ?>"
|
||||
|
||||
Reference in New Issue
Block a user