Add real replacement pages for Pages, Media Library, and Users (v2.6.0)
Completes the "skinned vs. replaced" conversion started with Posts — every native list-table screen the user asked about now has a genuine custom Vue page behind it instead of dark-themed CSS over the native markup. All three follow the same pattern as Posts: own REST controller in admin/src/Rest/, own Vue page matching the Comments layout, native screen redirected away (direct visits too, not just the sidebar link), gated by its own Settings toggle defaulting to on. - Pages (PagesData.php + Pages.vue): same as Posts, parent page shown instead of categories. edit.php?post_type=page redirects; other post types untouched. - Media Library (MediaData.php + Media.vue): thumbnail grid with type filters (Images/Audio/Video/Documents/Unattached). Replaces the browse/library experience only — media-new.php (the actual upload screen) is untouched, same reasoning as leaving the block editor alone for Posts/Pages. - Users (UsersData.php + Users.vue): filterable by role. Deleting a user always reassigns their content to whoever performs the deletion rather than exposing WP core's reassign-or-delete-everything choice as a one-click action — deliberately simplified to avoid a surprising, highly destructive default. user-new.php untouched. Verified live: Media's grid renders real thumbnails/dimensions/file sizes with working filters and search; Users shows real accounts with role filters and correctly hides delete for the current user; both redirect correctly from their native URLs. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
56b6674527
commit
204c4365e2
@@ -12,6 +12,9 @@ use AuroraAdmin\Rest\AnalyticsData;
|
||||
use AuroraAdmin\Rest\PluginsData;
|
||||
use AuroraAdmin\Rest\CommentsData;
|
||||
use AuroraAdmin\Rest\PostsData;
|
||||
use AuroraAdmin\Rest\PagesData;
|
||||
use AuroraAdmin\Rest\MediaData;
|
||||
use AuroraAdmin\Rest\UsersData;
|
||||
use AuroraAdmin\Posts\ScheduledDayFilter;
|
||||
use AuroraAdmin\WhiteLabel\WhiteLabel;
|
||||
use AuroraAdmin\Pages\SubApps;
|
||||
@@ -49,6 +52,9 @@ class AuroraAdmin
|
||||
new PluginsData();
|
||||
new CommentsData();
|
||||
new PostsData();
|
||||
new PagesData();
|
||||
new MediaData();
|
||||
new UsersData();
|
||||
new ScheduledDayFilter();
|
||||
new WhiteLabel();
|
||||
new SubApps();
|
||||
|
||||
@@ -47,6 +47,9 @@ class SubApps
|
||||
"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],
|
||||
];
|
||||
|
||||
private static $hook_suffixes = [];
|
||||
@@ -82,6 +85,15 @@ class SubApps
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -93,6 +105,15 @@ class SubApps
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -146,6 +167,41 @@ class SubApps
|
||||
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()
|
||||
@@ -178,6 +234,9 @@ class SubApps
|
||||
"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")),
|
||||
];
|
||||
$root_id = "aurora-admin-" . self::PAGES[$page]["entry"] . "-root";
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Rest;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* REST endpoints for Aurora's custom Media Library page — same list-table
|
||||
* pattern as Posts/Pages/Comments. Deliberately doesn't rebuild the
|
||||
* upload flow (drag-drop, chunked uploads, the grid picker used inside
|
||||
* post editors) — "Add Media File" still opens the native media-new.php,
|
||||
* same way Posts'/Pages' "Add" buttons still open the native, already
|
||||
* themed block editor. This replaces the *library/browse* experience,
|
||||
* not uploading.
|
||||
*/
|
||||
class MediaData
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
add_action("rest_api_init", [self::class, "register"]);
|
||||
}
|
||||
|
||||
public static function register()
|
||||
{
|
||||
register_rest_route("aurora-admin/v1", "/media", [
|
||||
"methods" => "GET",
|
||||
"callback" => [self::class, "list_media"],
|
||||
"permission_callback" => function () {
|
||||
return current_user_can("upload_files");
|
||||
},
|
||||
"args" => [
|
||||
"type" => ["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", "/media/(?P<id>\d+)", [
|
||||
"methods" => "DELETE",
|
||||
"callback" => [self::class, "delete_media"],
|
||||
"permission_callback" => [self::class, "can_delete_route_media"],
|
||||
"args" => [
|
||||
"id" => ["type" => "integer", "required" => true],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public static function can_delete_route_media($request)
|
||||
{
|
||||
$id = (int) $request["id"];
|
||||
return $id && current_user_can("delete_post", $id);
|
||||
}
|
||||
|
||||
public static function list_media($request)
|
||||
{
|
||||
$type = self::query_mime_type($request->get_param("type"));
|
||||
$page = max(1, (int) ($request->get_param("page") ?: 1));
|
||||
$per_page = min(100, max(10, (int) ($request->get_param("perPage") ?: 24)));
|
||||
$search = sanitize_text_field((string) $request->get_param("search"));
|
||||
|
||||
$args = [
|
||||
"post_type" => "attachment",
|
||||
"post_status" => "inherit",
|
||||
"posts_per_page" => $per_page,
|
||||
"paged" => $page,
|
||||
"orderby" => "date",
|
||||
"order" => "DESC",
|
||||
];
|
||||
|
||||
if ($type === "unattached") {
|
||||
$args["post_parent"] = 0;
|
||||
} elseif ($type !== "") {
|
||||
$args["post_mime_type"] = $type;
|
||||
}
|
||||
|
||||
if ($search !== "") {
|
||||
$args["s"] = $search;
|
||||
}
|
||||
|
||||
$query = new \WP_Query($args);
|
||||
|
||||
return new \WP_REST_Response([
|
||||
"items" => array_map([self::class, "serialize_attachment"], $query->posts),
|
||||
"stats" => self::stats(),
|
||||
"pagination" => [
|
||||
"page" => $page,
|
||||
"perPage" => $per_page,
|
||||
"total" => (int) $query->found_posts,
|
||||
"totalPages" => max(1, (int) $query->max_num_pages),
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
|
||||
public static function delete_media($request)
|
||||
{
|
||||
$id = (int) $request["id"];
|
||||
$post = get_post($id);
|
||||
if (!$post || $post->post_type !== "attachment") {
|
||||
return new \WP_Error("aurora_media_invalid", __("Invalid media item.", "aurora-admin"), ["status" => 400]);
|
||||
}
|
||||
|
||||
$result = wp_delete_attachment($id, true);
|
||||
if (!$result) {
|
||||
return new \WP_Error("aurora_media_delete_failed", __("Could not delete the media item.", "aurora-admin"), ["status" => 500]);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response(["success" => true, "message" => __("Media item deleted.", "aurora-admin")], 200);
|
||||
}
|
||||
|
||||
private static function serialize_attachment($post)
|
||||
{
|
||||
$author = get_userdata($post->post_author);
|
||||
$mime = $post->post_mime_type;
|
||||
$is_image = wp_attachment_is_image($post->ID);
|
||||
$meta = wp_get_attachment_metadata($post->ID);
|
||||
$file_path = get_attached_file($post->ID);
|
||||
|
||||
$parent_title = "";
|
||||
$parent_edit_url = "";
|
||||
if ($post->post_parent) {
|
||||
$parent_title = get_the_title($post->post_parent);
|
||||
$parent_edit_url = get_edit_post_link($post->post_parent, "raw") ?: "";
|
||||
}
|
||||
|
||||
return [
|
||||
"id" => (int) $post->ID,
|
||||
"title" => get_the_title($post) ?: basename($file_path),
|
||||
"filename" => basename($file_path),
|
||||
"mimeType" => $mime,
|
||||
"typeLabel" => self::type_label($mime),
|
||||
"isImage" => $is_image,
|
||||
"thumbnailUrl" => $is_image ? (wp_get_attachment_image_url($post->ID, "thumbnail") ?: "") : "",
|
||||
"fileUrl" => wp_get_attachment_url($post->ID) ?: "",
|
||||
"dimensions" => $is_image && !empty($meta["width"]) ? "{$meta['width']} × {$meta['height']}" : "",
|
||||
"fileSize" => !empty($meta["filesize"]) ? size_format($meta["filesize"]) : "",
|
||||
"author" => $author ? $author->display_name : __("Unknown", "aurora-admin"),
|
||||
"attachedToTitle" => $parent_title,
|
||||
"attachedToEditUrl" => $parent_edit_url,
|
||||
"date" => get_the_date("M j, Y", $post),
|
||||
"editUrl" => get_edit_post_link($post->ID, "raw") ?: "",
|
||||
"canDelete" => current_user_can("delete_post", $post->ID),
|
||||
];
|
||||
}
|
||||
|
||||
private static function stats()
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$counts = [
|
||||
"all" => (int) wp_count_posts("attachment")->inherit,
|
||||
"image" => 0,
|
||||
"audio" => 0,
|
||||
"video" => 0,
|
||||
"application" => 0,
|
||||
"unattached" => 0,
|
||||
];
|
||||
|
||||
$rows = $wpdb->get_results(
|
||||
"SELECT post_mime_type, COUNT(*) AS c FROM {$wpdb->posts}
|
||||
WHERE post_type = 'attachment' AND post_status = 'inherit'
|
||||
GROUP BY post_mime_type"
|
||||
);
|
||||
foreach ($rows as $row) {
|
||||
$group = strtok((string) $row->post_mime_type, "/");
|
||||
if (isset($counts[$group])) {
|
||||
$counts[$group] += (int) $row->c;
|
||||
} else {
|
||||
$counts["application"] += (int) $row->c;
|
||||
}
|
||||
}
|
||||
|
||||
$counts["unattached"] = (int) $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$wpdb->posts}
|
||||
WHERE post_type = 'attachment' AND post_status = 'inherit' AND post_parent = 0"
|
||||
);
|
||||
|
||||
return $counts;
|
||||
}
|
||||
|
||||
private static function query_mime_type($type)
|
||||
{
|
||||
switch ($type) {
|
||||
case "image":
|
||||
case "audio":
|
||||
case "video":
|
||||
return $type;
|
||||
case "application":
|
||||
return "application";
|
||||
case "unattached":
|
||||
return "unattached";
|
||||
case "all":
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static function type_label($mime)
|
||||
{
|
||||
$group = strtok((string) $mime, "/");
|
||||
switch ($group) {
|
||||
case "image":
|
||||
return __("Image", "aurora-admin");
|
||||
case "audio":
|
||||
return __("Audio", "aurora-admin");
|
||||
case "video":
|
||||
return __("Video", "aurora-admin");
|
||||
default:
|
||||
return __("Document", "aurora-admin");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Rest;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* REST endpoints for Aurora's custom Pages page — same pattern as
|
||||
* PostsData.php, adapted for the "page" post type (no categories, but
|
||||
* shows the parent page instead).
|
||||
*/
|
||||
class PagesData
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
add_action("rest_api_init", [self::class, "register"]);
|
||||
}
|
||||
|
||||
public static function register()
|
||||
{
|
||||
register_rest_route("aurora-admin/v1", "/pages", [
|
||||
"methods" => "GET",
|
||||
"callback" => [self::class, "list_pages"],
|
||||
"permission_callback" => function () {
|
||||
return current_user_can("edit_pages");
|
||||
},
|
||||
"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", "/pages/trash", [
|
||||
"methods" => "POST",
|
||||
"callback" => [self::class, "trash_page"],
|
||||
"permission_callback" => [self::class, "can_edit_param_page"],
|
||||
"args" => [
|
||||
"id" => ["type" => "integer", "required" => true],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route("aurora-admin/v1", "/pages/restore", [
|
||||
"methods" => "POST",
|
||||
"callback" => [self::class, "restore_page"],
|
||||
"permission_callback" => [self::class, "can_edit_param_page"],
|
||||
"args" => [
|
||||
"id" => ["type" => "integer", "required" => true],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route("aurora-admin/v1", "/pages/(?P<id>\d+)", [
|
||||
"methods" => "DELETE",
|
||||
"callback" => [self::class, "delete_page"],
|
||||
"permission_callback" => [self::class, "can_delete_route_page"],
|
||||
"args" => [
|
||||
"id" => ["type" => "integer", "required" => true],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public static function can_edit_param_page($request)
|
||||
{
|
||||
$id = (int) $request->get_param("id");
|
||||
return $id && current_user_can("edit_page", $id);
|
||||
}
|
||||
|
||||
public static function can_delete_route_page($request)
|
||||
{
|
||||
$id = (int) $request["id"];
|
||||
return $id && current_user_can("delete_page", $id);
|
||||
}
|
||||
|
||||
public static function list_pages($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") ?: 20)));
|
||||
$search = sanitize_text_field((string) $request->get_param("search"));
|
||||
|
||||
$args = [
|
||||
"post_type" => "page",
|
||||
"post_status" => $status,
|
||||
"posts_per_page" => $per_page,
|
||||
"paged" => $page,
|
||||
"orderby" => "title",
|
||||
"order" => "ASC",
|
||||
"ignore_sticky_posts" => true,
|
||||
];
|
||||
|
||||
if ($search !== "") {
|
||||
$args["s"] = $search;
|
||||
}
|
||||
|
||||
$query = new \WP_Query($args);
|
||||
|
||||
return new \WP_REST_Response([
|
||||
"items" => array_map([self::class, "serialize_page"], $query->posts),
|
||||
"stats" => self::stats(),
|
||||
"pagination" => [
|
||||
"page" => $page,
|
||||
"perPage" => $per_page,
|
||||
"total" => (int) $query->found_posts,
|
||||
"totalPages" => max(1, (int) $query->max_num_pages),
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
|
||||
public static function trash_page($request)
|
||||
{
|
||||
$id = (int) $request->get_param("id");
|
||||
$post = get_post($id);
|
||||
if (!$post || $post->post_type !== "page") {
|
||||
return new \WP_Error("aurora_page_invalid", __("Invalid page.", "aurora-admin"), ["status" => 400]);
|
||||
}
|
||||
|
||||
$result = wp_trash_post($id);
|
||||
if (!$result) {
|
||||
return new \WP_Error("aurora_page_trash_failed", __("Could not trash the page.", "aurora-admin"), ["status" => 500]);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response(["success" => true, "message" => __("Page moved to trash.", "aurora-admin")], 200);
|
||||
}
|
||||
|
||||
public static function restore_page($request)
|
||||
{
|
||||
$id = (int) $request->get_param("id");
|
||||
$post = get_post($id);
|
||||
if (!$post || $post->post_type !== "page") {
|
||||
return new \WP_Error("aurora_page_invalid", __("Invalid page.", "aurora-admin"), ["status" => 400]);
|
||||
}
|
||||
|
||||
$result = wp_untrash_post($id);
|
||||
if (!$result) {
|
||||
return new \WP_Error("aurora_page_restore_failed", __("Could not restore the page.", "aurora-admin"), ["status" => 500]);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response(["success" => true, "message" => __("Page restored.", "aurora-admin")], 200);
|
||||
}
|
||||
|
||||
public static function delete_page($request)
|
||||
{
|
||||
$id = (int) $request["id"];
|
||||
$post = get_post($id);
|
||||
if (!$post || $post->post_type !== "page") {
|
||||
return new \WP_Error("aurora_page_invalid", __("Invalid page.", "aurora-admin"), ["status" => 400]);
|
||||
}
|
||||
|
||||
$result = wp_delete_post($id, true);
|
||||
if (!$result) {
|
||||
return new \WP_Error("aurora_page_delete_failed", __("Could not delete the page.", "aurora-admin"), ["status" => 500]);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response(["success" => true, "message" => __("Page deleted.", "aurora-admin")], 200);
|
||||
}
|
||||
|
||||
private static function serialize_page($post)
|
||||
{
|
||||
$author = get_userdata($post->post_author);
|
||||
$status = $post->post_status;
|
||||
$parent_title = $post->post_parent ? get_the_title($post->post_parent) : "";
|
||||
|
||||
return [
|
||||
"id" => (int) $post->ID,
|
||||
"title" => get_the_title($post) ?: __("(No title)", "aurora-admin"),
|
||||
"author" => $author ? $author->display_name : __("Unknown", "aurora-admin"),
|
||||
"parentTitle" => $parent_title,
|
||||
"commentCount" => (int) $post->comment_count,
|
||||
"date" => get_the_date("M j, Y", $post),
|
||||
"dateLabel" =>
|
||||
$status === "publish"
|
||||
? __("Published", "aurora-admin")
|
||||
: ($status === "future"
|
||||
? __("Scheduled", "aurora-admin")
|
||||
: __("Last Modified", "aurora-admin")),
|
||||
"status" => $status,
|
||||
"statusLabel" => self::status_label($status),
|
||||
"editUrl" => get_edit_post_link($post->ID, "raw") ?: "",
|
||||
"viewUrl" => $status === "publish" ? get_permalink($post) : get_preview_post_link($post),
|
||||
"canDelete" => current_user_can("delete_page", $post->ID),
|
||||
];
|
||||
}
|
||||
|
||||
private static function stats()
|
||||
{
|
||||
$counts = wp_count_posts("page");
|
||||
|
||||
return [
|
||||
"all" => (int) $counts->publish + (int) $counts->draft + (int) $counts->pending + (int) $counts->private + (int) $counts->future,
|
||||
"publish" => (int) $counts->publish,
|
||||
"draft" => (int) $counts->draft,
|
||||
"pending" => (int) $counts->pending,
|
||||
"trash" => (int) ($counts->trash ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
private static function query_status($status)
|
||||
{
|
||||
switch ($status) {
|
||||
case "publish":
|
||||
case "draft":
|
||||
case "pending":
|
||||
case "trash":
|
||||
return $status;
|
||||
case "all":
|
||||
default:
|
||||
return ["publish", "draft", "pending", "private", "future"];
|
||||
}
|
||||
}
|
||||
|
||||
private static function status_label($status)
|
||||
{
|
||||
switch ($status) {
|
||||
case "publish":
|
||||
return __("Published", "aurora-admin");
|
||||
case "draft":
|
||||
return __("Draft", "aurora-admin");
|
||||
case "pending":
|
||||
return __("Pending", "aurora-admin");
|
||||
case "future":
|
||||
return __("Scheduled", "aurora-admin");
|
||||
case "private":
|
||||
return __("Private", "aurora-admin");
|
||||
case "trash":
|
||||
return __("Trash", "aurora-admin");
|
||||
default:
|
||||
return ucfirst($status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Rest;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* REST endpoints for Aurora's custom Users page — same list-table pattern
|
||||
* as Posts/Pages/Comments/Media. "Add New User" still opens the native
|
||||
* user-new.php (account creation involves email verification/role
|
||||
* assignment forms not worth rebuilding, same reasoning as leaving
|
||||
* media-new.php/post-new.php alone for Media/Posts/Pages).
|
||||
*
|
||||
* Deleting a user reassigns their content to whichever admin performs the
|
||||
* deletion, rather than presenting WP core's "delete or reassign to
|
||||
* someone else" choice screen — a deliberate simplification, not an
|
||||
* oversight: wp_delete_user() with a null $reassign deletes all of the
|
||||
* user's content too, which is much more destructive and surprising as a
|
||||
* one-click REST action than always reassigning safely.
|
||||
*/
|
||||
class UsersData
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
add_action("rest_api_init", [self::class, "register"]);
|
||||
}
|
||||
|
||||
public static function register()
|
||||
{
|
||||
register_rest_route("aurora-admin/v1", "/users", [
|
||||
"methods" => "GET",
|
||||
"callback" => [self::class, "list_users"],
|
||||
"permission_callback" => function () {
|
||||
return current_user_can("list_users");
|
||||
},
|
||||
"args" => [
|
||||
"role" => ["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", "/users/(?P<id>\d+)", [
|
||||
"methods" => "DELETE",
|
||||
"callback" => [self::class, "delete_user"],
|
||||
"permission_callback" => function ($request) {
|
||||
$id = (int) $request["id"];
|
||||
return $id && current_user_can("delete_user", $id) && $id !== get_current_user_id();
|
||||
},
|
||||
"args" => [
|
||||
"id" => ["type" => "integer", "required" => true],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public static function list_users($request)
|
||||
{
|
||||
$role = sanitize_key((string) $request->get_param("role"));
|
||||
$page = max(1, (int) ($request->get_param("page") ?: 1));
|
||||
$per_page = min(100, max(10, (int) ($request->get_param("perPage") ?: 20)));
|
||||
$search = sanitize_text_field((string) $request->get_param("search"));
|
||||
|
||||
$args = [
|
||||
"number" => $per_page,
|
||||
"offset" => ($page - 1) * $per_page,
|
||||
"orderby" => "display_name",
|
||||
"order" => "ASC",
|
||||
"count_total" => true,
|
||||
];
|
||||
|
||||
if ($role !== "" && $role !== "all") {
|
||||
$args["role"] = $role;
|
||||
}
|
||||
|
||||
if ($search !== "") {
|
||||
$args["search"] = "*{$search}*";
|
||||
$args["search_columns"] = ["user_login", "user_email", "display_name"];
|
||||
}
|
||||
|
||||
$query = new \WP_User_Query($args);
|
||||
|
||||
return new \WP_REST_Response([
|
||||
"items" => array_map([self::class, "serialize_user"], $query->get_results()),
|
||||
"stats" => self::stats(),
|
||||
"pagination" => [
|
||||
"page" => $page,
|
||||
"perPage" => $per_page,
|
||||
"total" => (int) $query->get_total(),
|
||||
"totalPages" => max(1, (int) ceil($query->get_total() / $per_page)),
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
|
||||
public static function delete_user($request)
|
||||
{
|
||||
$id = (int) $request["id"];
|
||||
$user = get_userdata($id);
|
||||
if (!$user) {
|
||||
return new \WP_Error("aurora_user_invalid", __("Invalid user.", "aurora-admin"), ["status" => 400]);
|
||||
}
|
||||
|
||||
require_once ABSPATH . "wp-admin/includes/user.php";
|
||||
$result = wp_delete_user($id, get_current_user_id());
|
||||
if (!$result) {
|
||||
return new \WP_Error("aurora_user_delete_failed", __("Could not delete the user.", "aurora-admin"), ["status" => 500]);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response(["success" => true, "message" => __("User deleted; their content was reassigned to you.", "aurora-admin")], 200);
|
||||
}
|
||||
|
||||
private static function serialize_user($user)
|
||||
{
|
||||
$roles = array_map("translate_user_role", $user->roles);
|
||||
|
||||
return [
|
||||
"id" => (int) $user->ID,
|
||||
"username" => $user->user_login,
|
||||
"displayName" => $user->display_name,
|
||||
"email" => $user->user_email,
|
||||
"avatarUrl" => get_avatar_url($user->ID, ["size" => 64]),
|
||||
"roles" => array_values($roles),
|
||||
"postCount" => (int) count_user_posts($user->ID, "post"),
|
||||
"registeredDate" => mysql2date("M j, Y", $user->user_registered),
|
||||
"editUrl" => get_edit_user_link($user->ID),
|
||||
"canDelete" => current_user_can("delete_user", $user->ID) && $user->ID !== get_current_user_id(),
|
||||
];
|
||||
}
|
||||
|
||||
private static function stats()
|
||||
{
|
||||
$counts = count_users();
|
||||
$by_role = [];
|
||||
foreach ($counts["avail_roles"] as $role => $count) {
|
||||
$by_role[$role] = (int) $count;
|
||||
}
|
||||
|
||||
return [
|
||||
"all" => (int) $counts["total_users"],
|
||||
"byRole" => $by_role,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,18 @@ class MenuSerializer
|
||||
return admin_url("admin.php?page=aurora-admin-posts");
|
||||
}
|
||||
|
||||
if ($slug === "edit.php?post_type=page" && Settings::get("modern_pages", true)) {
|
||||
return admin_url("admin.php?page=aurora-admin-pages");
|
||||
}
|
||||
|
||||
if ($slug === "upload.php" && Settings::get("modern_media", true)) {
|
||||
return admin_url("admin.php?page=aurora-admin-media");
|
||||
}
|
||||
|
||||
if ($slug === "users.php" && Settings::get("modern_users", true)) {
|
||||
return admin_url("admin.php?page=aurora-admin-users");
|
||||
}
|
||||
|
||||
// Already an absolute URL.
|
||||
if (preg_match('#^https?://#', $slug)) {
|
||||
return $slug;
|
||||
|
||||
Reference in New Issue
Block a user