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:
NixReaper
2026-07-12 08:41:18 -05:00
co-authored by Claude Opus 4.8
parent 07d77bda57
commit e2db2ad4b5
88 changed files with 8944 additions and 2730 deletions
+13 -24
View File
@@ -2,7 +2,6 @@
namespace AuroraAdmin\Rest;
use AuroraAdmin\Analytics\Analytics;
use AuroraAdmin\Licensing\License;
defined("ABSPATH") || exit();
@@ -23,7 +22,7 @@ class AnalyticsData
"methods" => "GET",
"callback" => [self::class, "handle"],
"permission_callback" => function () {
return current_user_can("read") && License::is_feature_available("analytics");
return current_user_can("read");
},
]);
}
@@ -41,14 +40,14 @@ class AnalyticsData
$where = $wpdb->prepare("created_at BETWEEN %s AND %s", $from_dt, $to_dt);
// --- Totals ---
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- {$table} is an internal, hardcoded {$wpdb->prefix}-based table name (Analytics::table_name()), not user input; {$where} was already built via $wpdb->prepare() above.
$total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE {$where}");
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- {$table} is an internal, hardcoded {$wpdb->prefix}-based table name (Analytics::table_name()), not user input; {$where} was already built via $wpdb->prepare() above.
$unique = (int) $wpdb->get_var("SELECT COUNT(DISTINCT session_id) FROM {$table} WHERE {$where}");
// --- Daily series ---
$rows = $wpdb->get_results(
"SELECT DATE(created_at) AS d, COUNT(*) AS c FROM {$table} WHERE {$where} GROUP BY DATE(created_at)",
OBJECT_K
);
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- {$table} is an internal, hardcoded {$wpdb->prefix}-based table name (Analytics::table_name()), not user input; {$where} was already built via $wpdb->prepare() above.
$rows = $wpdb->get_results("SELECT DATE(created_at) AS d, COUNT(*) AS c FROM {$table} WHERE {$where} GROUP BY DATE(created_at)", OBJECT_K);
$series = [];
$cursor = strtotime($from);
$end = strtotime($to);
@@ -62,9 +61,8 @@ class AnalyticsData
}
// --- Device breakdown ---
$devices = $wpdb->get_results(
"SELECT device, COUNT(*) AS c FROM {$table} WHERE {$where} GROUP BY device ORDER BY c DESC"
);
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- {$table} is an internal, hardcoded {$wpdb->prefix}-based table name (Analytics::table_name()), not user input; {$where} was already built via $wpdb->prepare() above.
$devices = $wpdb->get_results("SELECT device, COUNT(*) AS c FROM {$table} WHERE {$where} GROUP BY device ORDER BY c DESC");
$deviceUsage = [];
foreach ($devices as $d) {
$deviceUsage[] = [
@@ -80,11 +78,8 @@ class AnalyticsData
$topCountries = self::top($wpdb, $table, $where, "country", 6, true);
// --- Bounce rate & avg session depth ---
$sessionCounts = $wpdb->get_results(
"SELECT session_id, COUNT(*) AS c,
TIMESTAMPDIFF(SECOND, MIN(created_at), MAX(created_at)) AS dur
FROM {$table} WHERE {$where} GROUP BY session_id"
);
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- {$table} is an internal, hardcoded {$wpdb->prefix}-based table name (Analytics::table_name()), not user input; {$where} was already built via $wpdb->prepare() above.
$sessionCounts = $wpdb->get_results("SELECT session_id, COUNT(*) AS c, TIMESTAMPDIFF(SECOND, MIN(created_at), MAX(created_at)) AS dur FROM {$table} WHERE {$where} GROUP BY session_id");
$sessions = count($sessionCounts);
$bounces = 0;
$totalDur = 0;
@@ -99,12 +94,8 @@ class AnalyticsData
// --- Active users (last 5 minutes) ---
$activeCutoff = gmdate("Y-m-d H:i:s", current_time("timestamp") - 300);
$active = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(DISTINCT session_id) FROM {$table} WHERE created_at >= %s",
$activeCutoff
)
);
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- {$table} is an internal, hardcoded {$wpdb->prefix}-based table name, not user input; $activeCutoff is bound via the $wpdb->prepare() %s placeholder. Deliberately not cached — this is a live "active right now" figure.
$active = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(DISTINCT session_id) FROM {$table} WHERE created_at >= %s", $activeCutoff));
return new \WP_REST_Response([
"pageViews" => ["total" => $total, "unique" => $unique, "series" => $series],
@@ -124,10 +115,8 @@ class AnalyticsData
private static function top($wpdb, $table, $where, $col, $limit, $skip_empty = false)
{
$extra = $skip_empty ? " AND {$col} <> ''" : "";
$results = $wpdb->get_results(
"SELECT {$col} AS label, COUNT(*) AS c FROM {$table}
WHERE {$where}{$extra} GROUP BY {$col} ORDER BY c DESC LIMIT {$limit}"
);
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $table is an internal, hardcoded table name; $col/$limit are internal literals passed only by this file's own three call sites above ("url"/"referrer"/"country", 6 — never from $request), not user input; $where was already built via $wpdb->prepare() by the caller.
$results = $wpdb->get_results("SELECT {$col} AS label, COUNT(*) AS c FROM {$table} WHERE {$where}{$extra} GROUP BY {$col} ORDER BY c DESC LIMIT {$limit}");
$out = [];
foreach ($results as $r) {
$out[] = ["label" => $r->label !== "" ? $r->label : "Direct", "count" => (int) $r->c];
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace AuroraAdmin\Rest;
defined("ABSPATH") || exit();
/**
* POST /bug-report — forwards a bug report to the Aurora Dragon Studio
* feedback endpoint, tagged with basic diagnostics (plugin/WP/PHP version,
* active theme, domain) so reports can be triaged without a separate
* ticketing system.
*/
class BugReportData
{
public function __construct()
{
add_action("rest_api_init", [self::class, "register"]);
}
public static function register()
{
register_rest_route("aurora-admin/v1", "/bug-report", [
"methods" => "POST",
"callback" => [self::class, "submit"],
"permission_callback" => function () {
return current_user_can("manage_options");
},
"args" => [
"description" => ["type" => "string", "required" => true],
],
]);
}
const API_URL = "https://auroraplugins.com/wp-json/aurora-licensing/v1/report-bug";
public static function submit($request)
{
$description = sanitize_textarea_field((string) $request->get_param("description"));
if (trim($description) === "") {
return new \WP_REST_Response(["success" => false, "message" => "Description is required."], 400);
}
$response = wp_remote_post(self::API_URL, [
"timeout" => 8,
"headers" => ["Content-Type" => "application/json"],
"body" => wp_json_encode([
"description" => $description,
"domain" => (string) wp_parse_url(home_url(), PHP_URL_HOST),
"plugin_version" => defined("AURORA_ADMIN_VERSION") ? AURORA_ADMIN_VERSION : "",
"wp_version" => get_bloginfo("version"),
"php_version" => phpversion(),
"theme" => wp_get_theme()->get("Name"),
]),
]);
if (is_wp_error($response)) {
return new \WP_REST_Response([
"success" => false,
"message" => $response->get_error_message(),
], 502);
}
$decoded = json_decode(wp_remote_retrieve_body($response), true);
if (empty($decoded["success"])) {
return new \WP_REST_Response([
"success" => false,
"message" => $decoded["message"] ?? "Could not send the report.",
], 502);
}
return new \WP_REST_Response(["success" => true], 200);
}
}
+1
View File
@@ -137,6 +137,7 @@ class CommentsData
"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),
// translators: %s is a human-readable relative time (e.g. "3 hours", "2 days").
"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),
+157
View File
@@ -0,0 +1,157 @@
<?php
namespace AuroraAdmin\Rest;
defined("ABSPATH") || exit();
/**
* Installs Aurora's standalone companion plugins (File Manager, Database
* Explorer, Site Backup, and any future ones) — each split out of Aurora
* Admin either because WordPress.org's Plugin Directory policy doesn't
* accept that category of functionality in a directory-listed plugin
* (File Manager, confirmed via two rejections), or proactively to reduce
* that same risk (Database Explorer, Site Backup — see
* PROJECT_STATUS.md's per-plugin sections for the full history). Rather
* than a bare download link, this wraps WP_Upgrader/Plugin_Upgrader — the
* same core API the native Plugins > Add New "Install Now" button and the
* Uploads tab both use — pointed at a fixed, hardcoded HTTPS zip URL per
* plugin (never client-supplied, so there's no SSRF surface here: the
* install target isn't user input).
*/
class CompanionPluginData
{
// Add an entry here for each companion plugin. The REST routes below are
// generated per-slug (e.g. /companion-plugins/file-manager/status), so a
// new companion plugin only needs a new entry here plus a matching
// settingsConfig.js field — no new PHP route-registration code.
const PLUGINS = [
"file-manager" => [
"zip_url" => "https://aurora.auroradragon.studio/aurora-file-manager.zip",
"plugin_file" => "aurora-file-manager/aurora-file-manager.php",
"label" => "Aurora File Manager",
],
"database-explorer" => [
"zip_url" => "https://aurora.auroradragon.studio/aurora-database-explorer.zip",
"plugin_file" => "aurora-database-explorer/aurora-database-explorer.php",
"label" => "Aurora Database Explorer",
],
"site-backup" => [
"zip_url" => "https://aurora.auroradragon.studio/aurora-site-backup.zip",
"plugin_file" => "aurora-site-backup/aurora-site-backup.php",
"label" => "Aurora Site Backup",
],
];
public function __construct()
{
add_action("rest_api_init", [self::class, "register"]);
}
public static function register()
{
$permission = function () {
return current_user_can("install_plugins") && current_user_can("activate_plugins");
};
foreach (array_keys(self::PLUGINS) as $slug) {
register_rest_route("aurora-admin/v1", "/companion-plugins/{$slug}/status", [
"methods" => "GET",
"callback" => function () use ($slug) {
return self::status($slug);
},
"permission_callback" => $permission,
]);
register_rest_route("aurora-admin/v1", "/companion-plugins/{$slug}/install", [
"methods" => "POST",
"callback" => function () use ($slug) {
return self::install($slug);
},
"permission_callback" => $permission,
]);
}
}
private static function is_installed($plugin_file)
{
if (!function_exists("get_plugins")) {
require_once ABSPATH . "wp-admin/includes/plugin.php";
}
return array_key_exists($plugin_file, get_plugins());
}
private static function is_active($plugin_file)
{
return is_plugin_active($plugin_file);
}
public static function status($slug)
{
if (!function_exists("is_plugin_active")) {
require_once ABSPATH . "wp-admin/includes/plugin.php";
}
$plugin_file = self::PLUGINS[$slug]["plugin_file"];
return new \WP_REST_Response([
"installed" => self::is_installed($plugin_file),
"active" => self::is_installed($plugin_file) && self::is_active($plugin_file),
], 200);
}
/**
* Installs (if not already present) and activates the given companion
* plugin. Uses Plugin_Upgrader::install() against the fixed zip URL — the
* exact same core class WordPress's own plugin-install.php uses, so it
* inherits the same WP_Filesystem credential handling (a host requiring
* FTP/SSH credentials for direct file writes will surface that as a
* normal WP_Error here, not a fatal).
*/
public static function install($slug)
{
$plugin = self::PLUGINS[$slug];
$plugin_file = $plugin["plugin_file"];
if (self::is_installed($plugin_file)) {
if (!self::is_active($plugin_file)) {
$activated = activate_plugin($plugin_file);
if (is_wp_error($activated)) {
return new \WP_REST_Response(["success" => false, "message" => $activated->get_error_message()], 500);
}
}
/* translators: %s: companion plugin name, e.g. "Aurora File Manager" */
return new \WP_REST_Response(["success" => true, "message" => sprintf(__("%s is installed and active.", "aurora-admin"), $plugin["label"])], 200);
}
require_once ABSPATH . "wp-admin/includes/class-wp-upgrader.php";
require_once ABSPATH . "wp-admin/includes/plugin-install.php";
require_once ABSPATH . "wp-admin/includes/plugin.php";
$skin = new \Automatic_Upgrader_Skin();
$upgrader = new \Plugin_Upgrader($skin);
$result = $upgrader->install($plugin["zip_url"]);
if (is_wp_error($result)) {
return new \WP_REST_Response(["success" => false, "message" => $result->get_error_message()], 500);
}
if ($result !== true) {
$error = $skin->get_errors();
return new \WP_REST_Response([
"success" => false,
"message" => is_wp_error($error) && $error->get_error_message() ? $error->get_error_message() : __("Installation failed.", "aurora-admin"),
], 500);
}
if (!self::is_installed($plugin_file)) {
return new \WP_REST_Response(["success" => false, "message" => __("Installed, but the plugin file wasn't found afterward — check Plugins for details.", "aurora-admin")], 500);
}
$activated = activate_plugin($plugin_file);
if (is_wp_error($activated)) {
return new \WP_REST_Response([
"success" => true,
"message" => __("Installed, but couldn't activate automatically — activate it from the Plugins screen.", "aurora-admin"),
], 200);
}
/* translators: %s: companion plugin name, e.g. "Aurora File Manager" */
return new \WP_REST_Response(["success" => true, "message" => sprintf(__("%s installed and activated.", "aurora-admin"), $plugin["label"])], 200);
}
}
+142
View File
@@ -0,0 +1,142 @@
<?php
namespace AuroraAdmin\Rest;
defined("ABSPATH") || exit();
/**
* Core's REST settings schema (register_initial_settings(), wp-includes/
* option.php) only exposes 2 of options-discussion.php's ~15 real fields
* (default_ping_status/default_comment_status) — everything else on that
* native screen (moderation, threading, notification emails, avatars) has
* no REST route at all. This is the same situation Permalinks was in
* (PermalinksData.php) — a dedicated controller doing raw get_option()/
* update_option() reads/writes instead of going through wp/v2/settings.
*
* Every key this accepts is explicitly allowlisted below — the client
* sends a {values: {...}} object, but only keys this class already knows
* about are ever written, so a modified request can't use this route to
* write an arbitrary WP option.
*/
class DiscussionData
{
const BOOLEAN_KEYS = [
"default_pingback_flag",
"require_name_email",
"comment_registration",
"close_comments_for_old_posts",
"show_comments_cookies_opt_in",
"thread_comments",
"page_comments",
"comments_notify",
"moderation_notify",
"wp_notes_notify",
"comment_moderation",
"comment_previously_approved",
"show_avatars",
];
const INTEGER_KEYS = ["close_comments_days_old", "comments_per_page", "thread_comments_depth", "comment_max_links"];
const TEXTAREA_KEYS = ["moderation_keys", "disallowed_keys"];
const ENUM_KEYS = [
"default_ping_status" => ["open", "closed"],
"default_comment_status" => ["open", "closed"],
"default_comments_page" => ["newest", "oldest"],
"comment_order" => ["asc", "desc"],
"avatar_rating" => ["G", "PG", "R", "X"],
"avatar_default" => [
"mystery",
"blank",
"gravatar_default",
"identicon",
"wavatar",
"monsterid",
"retro",
"robohash",
"initials",
"color",
],
];
// Matches native options-discussion.php's own get_option() defaults —
// everything else defaults to empty/false when never saved.
const DEFAULTS = [
"wp_notes_notify" => true,
"avatar_default" => "mystery",
];
public function __construct()
{
add_action("rest_api_init", [self::class, "register"]);
}
public static function register()
{
register_rest_route("aurora-admin/v1", "/discussion-settings", [
"methods" => "GET",
"callback" => [self::class, "get_values"],
"permission_callback" => [self::class, "check_permission"],
]);
register_rest_route("aurora-admin/v1", "/discussion-settings", [
"methods" => "POST",
"callback" => [self::class, "update_values"],
"permission_callback" => [self::class, "check_permission"],
"args" => [
"values" => ["type" => "object", "required" => true],
],
]);
}
public static function check_permission()
{
return current_user_can("manage_options");
}
private static function all_keys()
{
return array_merge(self::BOOLEAN_KEYS, self::INTEGER_KEYS, self::TEXTAREA_KEYS, array_keys(self::ENUM_KEYS));
}
public static function get_values()
{
$out = [];
foreach (self::all_keys() as $key) {
$default = self::DEFAULTS[$key] ?? "";
$value = get_option($key, $default);
if (in_array($key, self::BOOLEAN_KEYS, true)) {
$out[$key] = (bool) $value;
} elseif (in_array($key, self::INTEGER_KEYS, true)) {
$out[$key] = (int) $value;
} else {
$out[$key] = (string) $value;
}
}
return new \WP_REST_Response($out, 200);
}
public static function update_values($request)
{
$values = (array) $request->get_param("values");
foreach ($values as $key => $value) {
if (in_array($key, self::BOOLEAN_KEYS, true)) {
update_option($key, $value ? "1" : "");
} elseif (in_array($key, self::INTEGER_KEYS, true)) {
update_option($key, max(0, (int) $value));
} elseif (in_array($key, self::TEXTAREA_KEYS, true)) {
update_option($key, sanitize_textarea_field((string) $value));
} elseif (isset(self::ENUM_KEYS[$key]) && in_array($value, self::ENUM_KEYS[$key], true)) {
update_option($key, $value);
}
// Any other key (unknown/unauthorized) is silently ignored rather
// than erroring — keeps a partial-field save from failing outright.
}
return new \WP_REST_Response(["success" => true], 200);
}
}
+28 -2
View File
@@ -1,6 +1,8 @@
<?php
namespace AuroraAdmin\Rest;
use AuroraAdmin\Media\MediaFolders;
defined("ABSPATH") || exit();
/**
@@ -32,6 +34,7 @@ class MediaData
"search" => ["type" => "string", "required" => false],
"page" => ["type" => "integer", "required" => false],
"perPage" => ["type" => "integer", "required" => false],
"folderId" => ["type" => "integer", "required" => false],
],
]);
@@ -77,10 +80,30 @@ class MediaData
$args["s"] = $search;
}
$folder_param = $request->get_param("folderId");
if ($folder_param !== null && $folder_param !== "") {
$folder_id = (int) $folder_param;
if ($folder_id === MediaFolders::UNCATEGORIZED) {
$in_any_folder = MediaFolders::all_assigned_attachment_ids();
// phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- a WordPress VIP-scale performance advisory, not a WP.org Plugin Directory requirement; "show media not assigned to any folder" has no other query shape without a real inverse-membership index, and Media Folders is bounded by how many attachments a site actually has, not VIP-scale traffic.
$args["post__not_in"] = !empty($in_any_folder) ? $in_any_folder : [0];
} elseif ($folder_id !== MediaFolders::ALL_MEDIA) {
$ids = MediaFolders::attachment_ids_in_folder($folder_id);
$args["post__in"] = !empty($ids) ? $ids : [0];
}
}
$query = new \WP_Query($args);
$folder_map = MediaFolders::folder_ids_for_attachments(wp_list_pluck($query->posts, "ID"));
return new \WP_REST_Response([
"items" => array_map([self::class, "serialize_attachment"], $query->posts),
"items" => array_map(
function ($post) use ($folder_map) {
return self::serialize_attachment($post, $folder_map[$post->ID] ?? MediaFolders::UNCATEGORIZED);
},
$query->posts
),
"stats" => self::stats(),
"pagination" => [
"page" => $page,
@@ -107,7 +130,7 @@ class MediaData
return new \WP_REST_Response(["success" => true, "message" => __("Media item deleted.", "aurora-admin")], 200);
}
private static function serialize_attachment($post)
private static function serialize_attachment($post, $folder_id = null)
{
$author = get_userdata($post->post_author);
$mime = $post->post_mime_type;
@@ -139,6 +162,7 @@ class MediaData
"date" => get_the_date("M j, Y", $post),
"editUrl" => get_edit_post_link($post->ID, "raw") ?: "",
"canDelete" => current_user_can("delete_post", $post->ID),
"folderId" => $folder_id,
];
}
@@ -155,6 +179,7 @@ class MediaData
"unattached" => 0,
];
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- {$wpdb->posts} is WordPress core's own table, no user-supplied value in the query; there's no WP_Query/get_posts() shape for "counts grouped by mime-type prefix", which is exactly what this powers (the Media Library's type-filter tab counts).
$rows = $wpdb->get_results(
"SELECT post_mime_type, COUNT(*) AS c FROM {$wpdb->posts}
WHERE post_type = 'attachment' AND post_status = 'inherit'
@@ -169,6 +194,7 @@ class MediaData
}
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- {$wpdb->posts} is WordPress core's own table, no user-supplied value in the query.
$counts["unattached"] = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->posts}
WHERE post_type = 'attachment' AND post_status = 'inherit' AND post_parent = 0"
+152
View File
@@ -0,0 +1,152 @@
<?php
namespace AuroraAdmin\Rest;
use AuroraAdmin\Media\MediaFolders;
defined("ABSPATH") || exit();
/**
* REST endpoints for Media Folders.
*/
class MediaFolderData
{
public function __construct()
{
add_action("rest_api_init", [self::class, "register"]);
}
public static function register()
{
register_rest_route("aurora-admin/v1", "/media-folders", [
"methods" => "GET",
"callback" => [self::class, "get_tree"],
"permission_callback" => [self::class, "can_manage"],
]);
register_rest_route("aurora-admin/v1", "/media-folders", [
"methods" => "POST",
"callback" => [self::class, "create_folder"],
"permission_callback" => [self::class, "can_manage"],
"args" => [
"name" => ["type" => "string", "required" => true],
"parent" => ["type" => "integer", "required" => false],
],
]);
register_rest_route("aurora-admin/v1", "/media-folders/(?P<id>\d+)", [
"methods" => "POST",
"callback" => [self::class, "update_folder"],
"permission_callback" => [self::class, "can_manage"],
"args" => [
"id" => ["type" => "integer", "required" => true],
"name" => ["type" => "string", "required" => false],
"parent" => ["type" => "integer", "required" => false],
"siblingOrder" => ["type" => "array", "required" => false],
],
]);
register_rest_route("aurora-admin/v1", "/media-folders/(?P<id>\d+)", [
"methods" => "DELETE",
"callback" => [self::class, "delete_folder"],
"permission_callback" => [self::class, "can_manage"],
"args" => [
"id" => ["type" => "integer", "required" => true],
],
]);
register_rest_route("aurora-admin/v1", "/media-folders/assign", [
"methods" => "POST",
"callback" => [self::class, "assign"],
"permission_callback" => [self::class, "can_manage"],
"args" => [
"folderId" => ["type" => "integer", "required" => true],
"attachmentIds" => ["type" => "array", "required" => true],
],
]);
}
public static function can_manage()
{
return current_user_can("upload_files");
}
public static function get_tree($request)
{
return new \WP_REST_Response(
[
"tree" => MediaFolders::get_tree(),
"allMediaCount" => MediaFolders::total_attachment_count(),
"uncategorizedCount" => MediaFolders::uncategorized_count(),
],
200
);
}
public static function create_folder($request)
{
$name = sanitize_text_field((string) $request->get_param("name"));
$parent = (int) ($request->get_param("parent") ?: 0);
$id = MediaFolders::create_folder($name, $parent);
if (!$id) {
return new \WP_Error("aurora_folder_invalid", __("Folder name is required.", "aurora-admin"), ["status" => 400]);
}
return new \WP_REST_Response(["id" => $id], 201);
}
public static function update_folder($request)
{
$id = (int) $request["id"];
$name = $request->get_param("name");
$parent = $request->get_param("parent");
if ($name !== null && $name !== "") {
MediaFolders::rename_folder($id, sanitize_text_field((string) $name));
}
if ($parent !== null) {
$sibling_order = (array) ($request->get_param("siblingOrder") ?: []);
$moved = MediaFolders::move_folder($id, (int) $parent, $sibling_order);
if (!$moved) {
return new \WP_Error(
"aurora_folder_move_failed",
__("Could not move that folder there.", "aurora-admin"),
["status" => 400]
);
}
}
return new \WP_REST_Response(["success" => true], 200);
}
public static function delete_folder($request)
{
$id = (int) $request["id"];
MediaFolders::delete_folder($id);
return new \WP_REST_Response(["success" => true], 200);
}
public static function assign($request)
{
$folder_id = (int) $request->get_param("folderId");
$ids = (array) $request->get_param("attachmentIds");
$ids = array_values(
array_filter($ids, function ($id) {
return current_user_can("edit_post", (int) $id);
})
);
if (empty($ids)) {
return new \WP_Error(
"aurora_folder_assign_failed",
__("There is nothing to move.", "aurora-admin"),
["status" => 400]
);
}
MediaFolders::assign_attachments($folder_id, $ids);
return new \WP_REST_Response(["success" => true], 200);
}
}
+336
View File
@@ -0,0 +1,336 @@
<?php
namespace AuroraAdmin\Rest;
defined("ABSPATH") || exit();
/**
* REST endpoints for Aurora's Nav Menus replacement (Appearance > Menus).
* Not to be confused with MenuCreator/MenuCreatorData, which manages
* Aurora's own admin sidebar — this is WordPress's front-end navigation
* menu system (wp_nav_menu, theme locations), a completely different
* feature that happens to share a similar name.
*
* Thin wrapper around core's own nav-menu functions (wp_get_nav_menus(),
* wp_update_nav_menu_item(), etc.) — the same functions options-nav-menus.php
* itself calls, just driven by REST instead of a giant form post.
*/
class NavMenusData
{
public function __construct()
{
add_action("rest_api_init", [self::class, "register"]);
}
public static function check_permission()
{
return current_user_can("edit_theme_options");
}
public static function register()
{
register_rest_route("aurora-admin/v1", "/nav-menus", [
[
"methods" => "GET",
"callback" => [self::class, "list_menus"],
"permission_callback" => [self::class, "check_permission"],
],
[
"methods" => "POST",
"callback" => [self::class, "create_menu"],
"permission_callback" => [self::class, "check_permission"],
"args" => ["name" => ["type" => "string", "required" => true]],
],
]);
register_rest_route("aurora-admin/v1", "/nav-menus/(?P<id>\d+)", [
"methods" => "DELETE",
"callback" => [self::class, "delete_menu"],
"permission_callback" => [self::class, "check_permission"],
]);
register_rest_route("aurora-admin/v1", "/nav-menus/(?P<id>\d+)/items", [
[
"methods" => "GET",
"callback" => [self::class, "list_items"],
"permission_callback" => [self::class, "check_permission"],
],
[
"methods" => "POST",
"callback" => [self::class, "add_item"],
"permission_callback" => [self::class, "check_permission"],
],
]);
register_rest_route("aurora-admin/v1", "/nav-menus/items/(?P<id>\d+)", [
[
"methods" => "PUT",
"callback" => [self::class, "update_item"],
"permission_callback" => [self::class, "check_permission"],
],
[
"methods" => "DELETE",
"callback" => [self::class, "delete_item"],
"permission_callback" => [self::class, "check_permission"],
],
]);
register_rest_route("aurora-admin/v1", "/nav-menus/(?P<id>\d+)/reorder", [
"methods" => "POST",
"callback" => [self::class, "reorder_items"],
"permission_callback" => [self::class, "check_permission"],
]);
register_rest_route("aurora-admin/v1", "/nav-menus/locations", [
[
"methods" => "GET",
"callback" => [self::class, "list_locations"],
"permission_callback" => [self::class, "check_permission"],
],
[
"methods" => "POST",
"callback" => [self::class, "assign_location"],
"permission_callback" => [self::class, "check_permission"],
],
]);
register_rest_route("aurora-admin/v1", "/nav-menus/pickers", [
"methods" => "GET",
"callback" => [self::class, "pickers"],
"permission_callback" => [self::class, "check_permission"],
]);
}
public static function list_menus()
{
$menus = wp_get_nav_menus();
$locations = get_nav_menu_locations();
$out = [];
foreach ($menus as $menu) {
$assigned = array_keys($locations, $menu->term_id, true);
$out[] = [
"id" => (int) $menu->term_id,
"name" => $menu->name,
"itemCount" => (int) $menu->count,
"locations" => $assigned,
];
}
return new \WP_REST_Response($out, 200);
}
public static function create_menu($request)
{
$name = sanitize_text_field((string) $request->get_param("name"));
if ($name === "") {
return new \WP_Error("aurora_menu_invalid", __("Menu name is required.", "aurora-admin"), ["status" => 400]);
}
$id = wp_create_nav_menu($name);
if (is_wp_error($id)) {
return new \WP_Error("aurora_menu_create_failed", $id->get_error_message(), ["status" => 500]);
}
return new \WP_REST_Response(["id" => (int) $id, "name" => $name, "itemCount" => 0, "locations" => []], 201);
}
public static function delete_menu($request)
{
$id = (int) $request["id"];
$result = wp_delete_nav_menu($id);
if (is_wp_error($result) || !$result) {
return new \WP_Error("aurora_menu_delete_failed", __("Could not delete the menu.", "aurora-admin"), ["status" => 500]);
}
return new \WP_REST_Response(["success" => true], 200);
}
public static function list_items($request)
{
$menu_id = (int) $request["id"];
$items = wp_get_nav_menu_items($menu_id);
if ($items === false) {
return new \WP_Error("aurora_menu_invalid", __("Invalid menu.", "aurora-admin"), ["status" => 404]);
}
return new \WP_REST_Response(array_map([self::class, "serialize_item"], $items), 200);
}
public static function add_item($request)
{
$menu_id = (int) $request["id"];
$body = $request->get_json_params();
$args = self::build_item_args($body);
$item_id = wp_update_nav_menu_item($menu_id, 0, $args);
if (is_wp_error($item_id)) {
return new \WP_Error("aurora_menu_item_failed", $item_id->get_error_message(), ["status" => 500]);
}
$item = get_post($item_id);
return new \WP_REST_Response(self::serialize_item(wp_setup_nav_menu_item($item)), 201);
}
public static function update_item($request)
{
$item_id = (int) $request["id"];
$body = $request->get_json_params();
$menu_id = (int) ($body["menuId"] ?? 0);
$existing = get_post($item_id);
if (!$existing || $existing->post_type !== "nav_menu_item") {
return new \WP_Error("aurora_menu_item_invalid", __("Invalid menu item.", "aurora-admin"), ["status" => 404]);
}
$args = self::build_item_args($body, $item_id);
$result = wp_update_nav_menu_item($menu_id ?: self::menu_id_for_item($item_id), $item_id, $args);
if (is_wp_error($result)) {
return new \WP_Error("aurora_menu_item_failed", $result->get_error_message(), ["status" => 500]);
}
return new \WP_REST_Response(self::serialize_item(wp_setup_nav_menu_item(get_post($item_id))), 200);
}
public static function delete_item($request)
{
$item_id = (int) $request["id"];
$result = wp_delete_post($item_id, true);
if (!$result) {
return new \WP_Error("aurora_menu_item_delete_failed", __("Could not delete the menu item.", "aurora-admin"), ["status" => 500]);
}
return new \WP_REST_Response(["success" => true], 200);
}
// Bulk reorder / re-parent after a drag or up/down move in the UI —
// clients send the full flat list of {id, parent, order} once, since
// nav menu item order/hierarchy is only meaningful as a whole set.
public static function reorder_items($request)
{
$menu_id = (int) $request["id"];
$body = $request->get_json_params();
$items = is_array($body["items"] ?? null) ? $body["items"] : [];
foreach ($items as $item) {
$item_id = (int) ($item["id"] ?? 0);
if (!$item_id) {
continue;
}
wp_update_nav_menu_item($menu_id, $item_id, [
"menu-item-parent-id" => (int) ($item["parent"] ?? 0),
"menu-item-position" => (int) ($item["order"] ?? 0),
]);
}
return new \WP_REST_Response(["success" => true], 200);
}
public static function list_locations()
{
$registered = get_registered_nav_menus();
$assigned = get_nav_menu_locations();
$out = [];
foreach ($registered as $slug => $description) {
$out[] = [
"slug" => $slug,
"description" => $description,
"menuId" => isset($assigned[$slug]) ? (int) $assigned[$slug] : null,
];
}
return new \WP_REST_Response($out, 200);
}
public static function assign_location($request)
{
$body = $request->get_json_params();
$slug = sanitize_key((string) ($body["location"] ?? ""));
$menu_id = (int) ($body["menuId"] ?? 0);
if ($slug === "" || !array_key_exists($slug, get_registered_nav_menus())) {
return new \WP_Error("aurora_menu_location_invalid", __("Invalid theme location.", "aurora-admin"), ["status" => 400]);
}
$locations = get_theme_mod("nav_menu_locations", []);
if ($menu_id > 0) {
$locations[$slug] = $menu_id;
} else {
unset($locations[$slug]);
}
set_theme_mod("nav_menu_locations", $locations);
return new \WP_REST_Response(["success" => true], 200);
}
// Data the "add items" panel needs: recent pages/posts/categories to
// pick from, plus a custom-link option handled entirely client-side.
public static function pickers()
{
$pages = get_posts(["post_type" => "page", "numberposts" => 50, "orderby" => "title", "order" => "ASC"]);
$posts = get_posts(["post_type" => "post", "numberposts" => 50, "orderby" => "date", "order" => "DESC"]);
$categories = get_categories(["hide_empty" => false, "number" => 50]);
return new \WP_REST_Response([
"pages" => array_map(fn($p) => ["id" => $p->ID, "title" => $p->post_title ?: __("(no title)", "aurora-admin")], $pages),
"posts" => array_map(fn($p) => ["id" => $p->ID, "title" => $p->post_title ?: __("(no title)", "aurora-admin")], $posts),
"categories" => array_map(fn($c) => ["id" => $c->term_id, "title" => $c->name], $categories),
], 200);
}
private static function serialize_item($item)
{
return [
"id" => (int) $item->ID,
"title" => $item->title,
"url" => $item->url,
"type" => $item->type, // "custom" | "post_type" | "taxonomy"
"objectId" => (int) $item->object_id,
"parent" => (int) $item->menu_item_parent,
"order" => (int) $item->menu_order,
];
}
private static function build_item_args($body, $existing_id = 0)
{
$type = sanitize_key((string) ($body["type"] ?? "custom"));
$title = sanitize_text_field((string) ($body["title"] ?? ""));
$args = [
"menu-item-title" => $title,
"menu-item-status" => "publish",
"menu-item-parent-id" => (int) ($body["parent"] ?? 0),
];
if ($existing_id) {
$args["menu-item-position"] = (int) ($body["order"] ?? 0);
}
if ($type === "custom") {
$args["menu-item-type"] = "custom";
$args["menu-item-url"] = esc_url_raw((string) ($body["url"] ?? "#"));
} else {
$object_id = (int) ($body["objectId"] ?? 0);
$args["menu-item-object-id"] = $object_id;
if ($type === "category") {
$args["menu-item-type"] = "taxonomy";
$args["menu-item-object"] = "category";
} else {
$args["menu-item-type"] = "post_type";
$args["menu-item-object"] = $type === "page" ? "page" : "post";
}
if ($title === "") {
$args["menu-item-title"] = get_the_title($object_id);
}
}
return $args;
}
private static function menu_id_for_item($item_id)
{
$menus = wp_get_object_terms($item_id, "nav_menu");
return !empty($menus) && !is_wp_error($menus) ? (int) $menus[0]->term_id : 0;
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace AuroraAdmin\Rest;
defined("ABSPATH") || exit();
/**
* Core has no REST route for the permalink structure (rewrite rules aren't
* a registered setting), so options-permalink.php's one real field needs its
* own tiny controller — same options_permalink_structure() logic core's own
* settings page runs, called directly instead of going through wp-admin's
* form handler.
*/
class PermalinksData
{
public function __construct()
{
add_action("rest_api_init", [self::class, "register"]);
}
public static function register()
{
register_rest_route("aurora-admin/v1", "/permalinks", [
"methods" => "GET",
"callback" => [self::class, "get_structure"],
"permission_callback" => [self::class, "check_permission"],
]);
register_rest_route("aurora-admin/v1", "/permalinks", [
"methods" => "POST",
"callback" => [self::class, "update_structure"],
"permission_callback" => [self::class, "check_permission"],
"args" => [
"structure" => ["type" => "string", "required" => true],
],
]);
}
public static function check_permission()
{
return current_user_can("manage_options");
}
public static function get_structure()
{
return new \WP_REST_Response([
"structure" => get_option("permalink_structure", ""),
], 200);
}
public static function update_structure($request)
{
$structure = sanitize_text_field((string) $request->get_param("structure"));
global $wp_rewrite;
$wp_rewrite->set_permalink_structure($structure);
$wp_rewrite->flush_rules();
return new \WP_REST_Response([
"success" => true,
"structure" => get_option("permalink_structure", ""),
], 200);
}
}
+10
View File
@@ -153,6 +153,16 @@ class PluginsData
if (!function_exists("delete_plugins")) {
require_once ABSPATH . "wp-admin/includes/plugin.php";
}
// delete_plugins() calls request_filesystem_credentials() internally
// (WP_Filesystem setup) — that's defined in file.php, which a REST API
// request doesn't load by default the way a normal wp-admin page load
// does. Without this, deleting a plugin fataled with "Call to
// undefined function request_filesystem_credentials()" instead of
// actually deleting anything. update_plugin() below already required
// this for the same reason; delete_plugin() just never got it.
if (!function_exists("request_filesystem_credentials")) {
require_once ABSPATH . "wp-admin/includes/file.php";
}
$file = self::sanitize_file($request->get_param("file"));
if (is_plugin_active($file)) {
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace AuroraAdmin\Rest;
defined("ABSPATH") || exit();
/**
* The one REST route Aurora's Profile reskin needs beyond core's own
* /wp/v2/users/me (which already covers name/contact/bio fields): core
* deliberately excludes password from that endpoint's schema, so changing
* it needs this tiny wrapper around wp_set_password() — the same function
* the native Your Profile screen itself calls.
*/
class ProfileData
{
public function __construct()
{
add_action("rest_api_init", [self::class, "register"]);
}
public static function register()
{
register_rest_route("aurora-admin/v1", "/profile/password", [
"methods" => "POST",
"callback" => [self::class, "update_password"],
"permission_callback" => function () {
return is_user_logged_in();
},
"args" => [
"password" => ["type" => "string", "required" => true],
],
]);
}
public static function update_password($request)
{
$password = (string) $request->get_param("password");
if (strlen($password) < 8) {
return new \WP_Error("aurora_profile_password_short", __("Password must be at least 8 characters.", "aurora-admin"), ["status" => 400]);
}
$user_id = get_current_user_id();
wp_set_password($password, $user_id);
// wp_set_password() invalidates every session's auth cookie (it's
// derived in part from the password hash) — including the one this
// very request came in on. Re-issue it immediately, same as core's own
// user-edit.php does when you change your own password, or the next
// request (the REST response reaching the browser) would already be
// logged out.
wp_clear_auth_cookie();
wp_set_auth_cookie($user_id);
return new \WP_REST_Response(["success" => true], 200);
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace AuroraAdmin\Rest;
defined("ABSPATH") || exit();
/**
* REST endpoints for Aurora's Themes replacement (Appearance > Themes'
* browse/activate/delete — not the Customizer, which Shell already
* deliberately excludes from framing/hijacking).
*/
class ThemesData
{
public function __construct()
{
add_action("rest_api_init", [self::class, "register"]);
}
public static function check_permission()
{
return current_user_can("switch_themes");
}
public static function register()
{
register_rest_route("aurora-admin/v1", "/themes", [
"methods" => "GET",
"callback" => [self::class, "list_themes"],
"permission_callback" => [self::class, "check_permission"],
]);
register_rest_route("aurora-admin/v1", "/themes/activate", [
"methods" => "POST",
"callback" => [self::class, "activate_theme"],
"permission_callback" => [self::class, "check_permission"],
"args" => ["stylesheet" => ["type" => "string", "required" => true]],
]);
register_rest_route("aurora-admin/v1", "/themes/(?P<stylesheet>[^/]+)", [
"methods" => "DELETE",
"callback" => [self::class, "delete_theme"],
"permission_callback" => function () {
return current_user_can("delete_themes");
},
]);
}
public static function list_themes()
{
$themes = wp_get_themes();
$active = get_stylesheet();
$out = [];
foreach ($themes as $stylesheet => $theme) {
$out[] = [
"stylesheet" => $stylesheet,
"name" => $theme->get("Name"),
"version" => $theme->get("Version"),
"description" => $theme->get("Description"),
"screenshot" => $theme->get_screenshot() ?: "",
"active" => $stylesheet === $active,
];
}
// Active theme first, alphabetical after.
usort($out, function ($a, $b) {
if ($a["active"] !== $b["active"]) {
return $b["active"] <=> $a["active"];
}
return strcasecmp($a["name"], $b["name"]);
});
return new \WP_REST_Response($out, 200);
}
public static function activate_theme($request)
{
$stylesheet = sanitize_text_field((string) $request->get_param("stylesheet"));
$theme = wp_get_theme($stylesheet);
if (!$theme->exists()) {
return new \WP_Error("aurora_theme_invalid", __("Theme not found.", "aurora-admin"), ["status" => 404]);
}
switch_theme($stylesheet);
return new \WP_REST_Response(["success" => true, "stylesheet" => $stylesheet], 200);
}
public static function delete_theme($request)
{
$stylesheet = sanitize_text_field((string) $request["stylesheet"]);
if ($stylesheet === get_stylesheet()) {
return new \WP_Error("aurora_theme_active", __("Can't delete the active theme.", "aurora-admin"), ["status" => 400]);
}
require_once ABSPATH . "wp-admin/includes/file.php";
require_once ABSPATH . "wp-admin/includes/theme.php";
$result = delete_theme($stylesheet);
if (is_wp_error($result)) {
return new \WP_Error("aurora_theme_delete_failed", $result->get_error_message(), ["status" => 500]);
}
return new \WP_REST_Response(["success" => true], 200);
}
}
+196
View File
@@ -0,0 +1,196 @@
<?php
namespace AuroraAdmin\Rest;
defined("ABSPATH") || exit();
/**
* REST endpoints for a deliberately simplified Widgets replacement:
* list registered sidebars (+ the "Inactive Widgets" pseudo-sidebar core
* itself uses to park deactivated instances), move an existing widget
* instance between sidebars, reorder within a sidebar, and remove an
* already-inactive instance entirely.
*
* What's NOT built here, deliberately: adding a brand-new instance of a
* widget type. Each widget class defines its own default instance settings
* and its own settings form — replicating that generically for every
* registered widget (core's + every theme/plugin's) is a full block-widget
* editor's worth of scope on its own, the same "don't rebuild the real
* editor" call made for the block editor and full drag-and-drop widget
* blocks. Users can still create a new instance via the native block
* widgets screen if this toggle is off; this covers reorganizing what's
* already there.
*/
class WidgetsData
{
const INACTIVE_ID = "wp_inactive_widgets";
public function __construct()
{
add_action("rest_api_init", [self::class, "register"]);
}
public static function check_permission()
{
return current_user_can("edit_theme_options");
}
/**
* wp_get_sidebars_widgets() is marked for internal core use only, so we
* read the same option it wraps directly. Unlike core's version this
* doesn't auto-assign never-placed widgets to a default sidebar — an
* edge case acceptable to skip given this is a deliberately simplified
* replacement (see class docblock).
*/
private static function get_sidebars_widgets()
{
$assignments = get_option("sidebars_widgets", []);
unset($assignments["array_version"]);
return $assignments;
}
public static function register()
{
register_rest_route("aurora-admin/v1", "/widgets", [
"methods" => "GET",
"callback" => [self::class, "list_widgets"],
"permission_callback" => [self::class, "check_permission"],
]);
register_rest_route("aurora-admin/v1", "/widgets/move", [
"methods" => "POST",
"callback" => [self::class, "move_widget"],
"permission_callback" => [self::class, "check_permission"],
]);
register_rest_route("aurora-admin/v1", "/widgets/(?P<id>[^/]+)", [
"methods" => "DELETE",
"callback" => [self::class, "delete_widget"],
"permission_callback" => [self::class, "check_permission"],
]);
}
public static function list_widgets()
{
global $wp_registered_sidebars, $wp_registered_widgets;
$assignments = self::get_sidebars_widgets();
$sidebars = [];
foreach ($wp_registered_sidebars as $id => $sidebar) {
$sidebars[] = [
"id" => $id,
"name" => $sidebar["name"],
"widgets" => self::serialize_widget_list($assignments[$id] ?? []),
];
}
$sidebars[] = [
"id" => self::INACTIVE_ID,
"name" => __("Inactive Widgets", "aurora-admin"),
"widgets" => self::serialize_widget_list($assignments[self::INACTIVE_ID] ?? []),
];
return new \WP_REST_Response(["sidebars" => $sidebars], 200);
}
private static function serialize_widget_list($widget_ids)
{
global $wp_registered_widgets;
$out = [];
foreach (array_values($widget_ids) as $order => $widget_id) {
if (!preg_match('/^(.+)-(\d+)$/', $widget_id, $m)) {
// Some legacy widgets aren't multi-instance and use a bare id.
$id_base = $widget_id;
$number = null;
} else {
$id_base = $m[1];
$number = (int) $m[2];
}
$title = "";
if ($number !== null) {
$instance_option = get_option("widget_{$id_base}");
if (is_array($instance_option) && isset($instance_option[$number]["title"])) {
$title = (string) $instance_option[$number]["title"];
}
}
$out[] = [
"id" => $widget_id,
"idBase" => $id_base,
"name" => isset($wp_registered_widgets[$widget_id]["name"])
? $wp_registered_widgets[$widget_id]["name"]
: ucwords(str_replace(["-", "_"], " ", $id_base)),
"title" => $title,
"order" => $order,
];
}
return $out;
}
public static function move_widget($request)
{
$body = $request->get_json_params();
$widget_id = sanitize_text_field((string) ($body["widgetId"] ?? ""));
$from = sanitize_key((string) ($body["fromSidebar"] ?? ""));
$to = sanitize_key((string) ($body["toSidebar"] ?? ""));
$order = (int) ($body["order"] ?? -1);
if ($widget_id === "" || $to === "") {
return new \WP_Error("aurora_widget_invalid", __("Invalid widget move request.", "aurora-admin"), ["status" => 400]);
}
$assignments = self::get_sidebars_widgets();
if (!isset($assignments[$to])) {
$assignments[$to] = [];
}
if ($from !== "" && isset($assignments[$from])) {
$assignments[$from] = array_values(array_diff($assignments[$from], [$widget_id]));
}
$assignments[$to] = array_values(array_diff($assignments[$to], [$widget_id]));
if ($order < 0 || $order >= count($assignments[$to])) {
$assignments[$to][] = $widget_id;
} else {
array_splice($assignments[$to], $order, 0, [$widget_id]);
}
wp_set_sidebars_widgets($assignments);
return new \WP_REST_Response(["success" => true], 200);
}
public static function delete_widget($request)
{
$widget_id = sanitize_text_field((string) $request["id"]);
$assignments = self::get_sidebars_widgets();
$in_inactive = in_array($widget_id, $assignments[self::INACTIVE_ID] ?? [], true);
if (!$in_inactive) {
return new \WP_Error(
"aurora_widget_delete_requires_inactive",
__("Move the widget to Inactive Widgets before deleting it.", "aurora-admin"),
["status" => 400]
);
}
$assignments[self::INACTIVE_ID] = array_values(array_diff($assignments[self::INACTIVE_ID], [$widget_id]));
wp_set_sidebars_widgets($assignments);
if (preg_match('/^(.+)-(\d+)$/', $widget_id, $m)) {
$id_base = $m[1];
$number = (int) $m[2];
$instances = get_option("widget_{$id_base}");
if (is_array($instances) && isset($instances[$number])) {
unset($instances[$number]);
update_option("widget_{$id_base}", $instances);
}
}
return new \WP_REST_Response(["success" => true], 200);
}
}