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
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user