Initial commit: Aurora Admin plugin

This commit is contained in:
Kris Parish
2026-07-03 02:53:29 -05:00
commit 03cabe2fe4
73 changed files with 9591 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
<?php
namespace AuroraAdmin\MenuCreator;
defined("ABSPATH") || exit();
/**
* Custom admin-menu configurations: hide/rename/reorder WP's native menu
* items per role. Storage + the runtime hook that actually applies a
* published config live here; REST CRUD lives in MenuCreatorData.php.
*/
class MenuCreator
{
const TABLE = "aurora_admin_menus";
const DB_VERSION = "1";
const DB_OPTION = "aurora_admin_menus_db_version";
public function __construct()
{
add_action("init", [self::class, "maybe_install"]);
// Late priority so it runs after all other plugins have registered
// their own menu items — there's nothing left to reorder/hide otherwise.
add_action("admin_menu", [self::class, "apply"], PHP_INT_MAX);
}
public static function table_name()
{
global $wpdb;
return $wpdb->prefix . self::TABLE;
}
public static function maybe_install()
{
if (get_option(self::DB_OPTION) === self::DB_VERSION) {
return;
}
global $wpdb;
$table = self::table_name();
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$table} (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(191) NOT NULL DEFAULT '',
status VARCHAR(20) NOT NULL DEFAULT 'draft',
roles TEXT NULL,
items LONGTEXT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_status (status)
) {$charset};";
require_once ABSPATH . "wp-admin/includes/upgrade.php";
dbDelta($sql);
update_option(self::DB_OPTION, self::DB_VERSION);
}
/**
* Applies every published config whose target roles match the current
* user: hides items marked hidden and relabels items with a custom label.
*/
public static function apply()
{
global $menu, $wpdb;
if (!is_admin() || !function_exists("wp_get_current_user")) {
return;
}
$user = wp_get_current_user();
if (!$user || !$user->exists()) {
return;
}
$table = self::table_name();
$configs = $wpdb->get_results("SELECT roles, items FROM {$table} WHERE status = 'published'");
if (!$configs) {
return;
}
foreach ($configs as $config) {
$roles = json_decode($config->roles ?: "[]", true);
if (!is_array($roles) || !$roles) {
continue;
}
if (!array_intersect((array) $user->roles, $roles)) {
continue;
}
$items = json_decode($config->items ?: "[]", true);
if (!is_array($items)) {
continue;
}
foreach ($items as $item) {
$native_id = isset($item["native_id"]) ? $item["native_id"] : "";
if ($native_id === "") {
continue;
}
foreach ($menu as $key => $entry) {
if (!isset($entry[2]) || $entry[2] !== $native_id) {
continue;
}
if (!empty($item["hidden"])) {
unset($menu[$key]);
continue 2;
}
if (!empty($item["label"])) {
$menu[$key][0] = sanitize_text_field($item["label"]);
}
}
}
}
}
}
+225
View File
@@ -0,0 +1,225 @@
<?php
namespace AuroraAdmin\MenuCreator;
defined("ABSPATH") || exit();
/**
* REST CRUD for saved menu configurations. GET/POST /menus,
* GET/PUT/DELETE /menus/:id. All manage_options-gated.
*/
class MenuCreatorData
{
public function __construct()
{
add_action("rest_api_init", [self::class, "register"]);
}
public static function register()
{
$permission = function () {
return current_user_can("manage_options");
};
register_rest_route("aurora-admin/v1", "/menus", [
[
"methods" => "GET",
"callback" => [self::class, "list_menus"],
"permission_callback" => $permission,
"args" => [
"status" => ["type" => "string", "required" => false],
],
],
[
"methods" => "POST",
"callback" => [self::class, "create"],
"permission_callback" => $permission,
"args" => [
"name" => ["type" => "string", "required" => true],
"status" => ["type" => "string", "required" => false],
"roles" => ["type" => "array", "required" => false],
"items" => ["type" => "array", "required" => false],
],
],
]);
register_rest_route("aurora-admin/v1", "/menus/native-items", [
"methods" => "GET",
"callback" => [self::class, "native_items"],
"permission_callback" => $permission,
]);
register_rest_route("aurora-admin/v1", "/menus/(?P<id>\d+)", [
[
"methods" => "GET",
"callback" => [self::class, "get_one"],
"permission_callback" => $permission,
],
[
"methods" => "PUT",
"callback" => [self::class, "update"],
"permission_callback" => $permission,
"args" => [
"name" => ["type" => "string", "required" => false],
"status" => ["type" => "string", "required" => false],
"roles" => ["type" => "array", "required" => false],
"items" => ["type" => "array", "required" => false],
],
],
[
"methods" => "DELETE",
"callback" => [self::class, "delete"],
"permission_callback" => $permission,
],
]);
}
/**
* The real, current WordPress menu (same source MenuSerializer feeds to
* the sidebar) flattened so the menu editor has something concrete to
* toggle hide/rename on — top-level items plus their submenu items.
*/
public static function native_items()
{
$menu = \AuroraAdmin\Shell\MenuSerializer::serialize();
$flat = [];
foreach ($menu as $item) {
if (!empty($item["separator"]) || empty($item["id"])) {
continue;
}
$flat[] = ["native_id" => $item["id"], "title" => $item["title"], "icon" => $item["icon"]];
foreach ($item["submenu"] ?? [] as $sub) {
if (empty($sub["id"])) {
continue;
}
$flat[] = ["native_id" => $sub["id"], "title" => $item["title"] . " › " . $sub["title"], "icon" => null];
}
}
return new \WP_REST_Response($flat, 200);
}
public static function list_menus($request)
{
global $wpdb;
$table = MenuCreator::table_name();
$status = $request->get_param("status");
if ($status) {
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$table} WHERE status = %s ORDER BY updated_at DESC",
$status
));
} else {
$rows = $wpdb->get_results("SELECT * FROM {$table} ORDER BY updated_at DESC");
}
return new \WP_REST_Response(array_map([self::class, "format"], $rows), 200);
}
public static function get_one($request)
{
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM " . MenuCreator::table_name() . " WHERE id = %d",
(int) $request->get_param("id")
));
if (!$row) {
return new \WP_Error("not_found", "Menu not found", ["status" => 404]);
}
return new \WP_REST_Response(self::format($row), 200);
}
public static function create($request)
{
global $wpdb;
$now = current_time("mysql");
$wpdb->insert(MenuCreator::table_name(), [
"name" => sanitize_text_field($request->get_param("name")),
"status" => self::sanitize_status($request->get_param("status")),
"roles" => wp_json_encode(self::sanitize_roles($request->get_param("roles"))),
"items" => wp_json_encode(self::sanitize_items($request->get_param("items"))),
"created_at" => $now,
"updated_at" => $now,
]);
return self::get_one(new \WP_REST_Request("GET", "", ["id" => $wpdb->insert_id]));
}
public static function update($request)
{
global $wpdb;
$id = (int) $request->get_param("id");
$fields = ["updated_at" => current_time("mysql")];
if ($request->get_param("name") !== null) {
$fields["name"] = sanitize_text_field($request->get_param("name"));
}
if ($request->get_param("status") !== null) {
$fields["status"] = self::sanitize_status($request->get_param("status"));
}
if ($request->get_param("roles") !== null) {
$fields["roles"] = wp_json_encode(self::sanitize_roles($request->get_param("roles")));
}
if ($request->get_param("items") !== null) {
$fields["items"] = wp_json_encode(self::sanitize_items($request->get_param("items")));
}
$wpdb->update(MenuCreator::table_name(), $fields, ["id" => $id]);
return self::get_one($request);
}
public static function delete($request)
{
global $wpdb;
$wpdb->delete(MenuCreator::table_name(), ["id" => (int) $request->get_param("id")]);
return new \WP_REST_Response(["deleted" => true], 200);
}
private static function format($row)
{
return [
"id" => (int) $row->id,
"name" => $row->name,
"status" => $row->status,
"roles" => json_decode($row->roles ?: "[]", true) ?: [],
"items" => json_decode($row->items ?: "[]", true) ?: [],
"createdAt" => $row->created_at,
"updatedAt" => $row->updated_at,
];
}
private static function sanitize_status($status)
{
return $status === "published" ? "published" : "draft";
}
private static function sanitize_roles($roles)
{
if (!is_array($roles)) {
return [];
}
$valid = array_keys(wp_roles()->roles);
return array_values(array_intersect(array_map("sanitize_key", $roles), $valid));
}
private static function sanitize_items($items)
{
if (!is_array($items)) {
return [];
}
$out = [];
foreach ($items as $item) {
if (!is_array($item) || empty($item["native_id"])) {
continue;
}
$out[] = [
"native_id" => sanitize_text_field($item["native_id"]),
"label" => isset($item["label"]) ? sanitize_text_field($item["label"]) : "",
"hidden" => !empty($item["hidden"]),
"order" => isset($item["order"]) ? (int) $item["order"] : 0,
];
}
return $out;
}
}