Author SHA1 Message Date
NixReaper 19831cd2f3 Trim uixpress references from changelog and project status docs 2026-08-29 21:59:23 -05:00
NixReaperandClaude Opus 4.8 faf9f35992 Expand login customizer: background, colors, custom CSS
Extend Settings > Login (beyond the existing style toggle + logo) toward
Loginfy parity: background image or color, form-panel background, button/
focus-accent color, and a custom login CSS field — all dependent on
"Style login page" and all falling back to the original fixed dark theme
when unset. LoginStyle.php sanitizes colors via sanitize_hex_color(),
esc_url()s the background image, and appends custom CSS (</style> stripped).
Verified on wp-login.php.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 14:49:03 -05:00
NixReaperandClaude Opus 4.8 f5c48bced9 Add Code settings group: custom admin CSS/JS + header/footer scripts
New Settings > Code group (Custom CSS & JS + Header & footer scripts) driven
by admin/src/Code/CodeInjection.php:
- Admin CSS  → <style> in the admin <head> (</style> stripped to prevent breakout)
- Admin JS   → <script> in the admin footer (</script> neutralized)
- Header scripts → printed verbatim in the front-end <head>
- Footer scripts → printed verbatim before the front-end </body>

Header/footer scripts are raw admin-authored markup by design (analytics,
meta, etc.), settable only by manage_options users on the settings page.
SettingsField gains a `mono` flag for monospace textareas (no CodeMirror
dependency). All four injection points verified on PHP 8.5.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 14:44:10 -05:00
8 changed files with 213 additions and 13 deletions
+1 -2
View File
@@ -1053,8 +1053,7 @@ and closed the one remaining documented security gap.
- First phase of the "full admin takeover" initiative: Posts, Pages, Media - First phase of the "full admin takeover" initiative: Posts, Pages, Media
Library, Users, and Comments no longer redirect to a separate Library, Users, and Comments no longer redirect to a separate
`admin.php?page=...` URL — they now hijack their native screens in place `admin.php?page=...` URL — they now hijack their native screens in place
(`ScreenHijack.php`, registered via `HijackedNativePages.php`), the same (`ScreenHijack.php`, registered via `HijackedNativePages.php`). WordPress's
approach the original uixpress reference plugin used. WordPress's
`load-{page}.php` hook fires before the native screen prints anything, so `load-{page}.php` hook fires before the native screen prints anything, so
Aurora buffers and discards the native output and mounts its own Vue page Aurora buffers and discards the native output and mounts its own Vue page
in its place — the URL never changes, so there's no separate slug that can in its place — the URL never changes, so there's no separate slug that can
+2 -2
View File
@@ -287,8 +287,8 @@ URL with no rewrite needed.
**Plugins is the one exception, deliberately left on the old redirect **Plugins is the one exception, deliberately left on the old redirect
mechanism** — `plugins.php` prints output in a way that can't be cleanly mechanism** — `plugins.php` prints output in a way that can't be cleanly
buffered (confirmed by the old uixpress reference plugin's own abandoned buffered (confirmed via a prior abandoned attempt at hijacking it the same
attempt at hijacking it the same way). It still has a real way). It still has a real
`aurora-admin-plugins` submenu under `plugins.php` and its own `aurora-admin-plugins` submenu under `plugins.php` and its own
`redirect_native_pages()` branch in `SubApps.php`. `redirect_native_pages()` branch in `SubApps.php`.
+2
View File
@@ -47,6 +47,7 @@ use AuroraAdmin\Media\SvgUploads;
use AuroraAdmin\Media\MediaFolders; use AuroraAdmin\Media\MediaFolders;
use AuroraAdmin\ImageFormats\ImageFormats; use AuroraAdmin\ImageFormats\ImageFormats;
use AuroraAdmin\Security\Security; use AuroraAdmin\Security\Security;
use AuroraAdmin\Code\CodeInjection;
use AuroraAdmin\Rest\MediaFolderData; use AuroraAdmin\Rest\MediaFolderData;
use AuroraAdmin\Rest\BugReportData; use AuroraAdmin\Rest\BugReportData;
use AuroraAdmin\Rest\CompanionPluginData; use AuroraAdmin\Rest\CompanionPluginData;
@@ -99,6 +100,7 @@ class AuroraAdmin
new MediaFolders(); new MediaFolders();
new ImageFormats(); new ImageFormats();
new Security(); new Security();
new CodeInjection();
new MediaFolderData(); new MediaFolderData();
new BugReportData(); new BugReportData();
new CompanionPluginData(); new CompanionPluginData();
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace AuroraAdmin\Code;
use AuroraAdmin\Options\Settings;
defined("ABSPATH") || exit();
/**
* Custom code injection (Settings > Code).
*
* Four admin-authored fields, only settable by manage_options users on the
* settings page:
* - Admin CSS → wrapped in a <style> in the admin <head>
* - Admin JS → wrapped in a <script> in the admin footer
* - Header scripts → printed raw in the front-end <head>
* - Footer scripts → printed raw before the front-end </body>
*
* Header/footer scripts are printed verbatim (they're meant to hold full
* <script>/<meta> tags — analytics, verification, etc.), the same trusted-
* admin model as any "insert headers and footers" tool. Admin CSS/JS is the
* user's own code too, but is wrapped in the right tag for them and guarded
* against accidentally breaking out of that tag.
*/
class CodeInjection
{
public function __construct()
{
add_action("admin_head", [self::class, "admin_css"], 999);
add_action("admin_print_footer_scripts", [self::class, "admin_js"], 999);
add_action("wp_head", [self::class, "header_scripts"], 999);
add_action("wp_footer", [self::class, "footer_scripts"], 999);
}
public static function admin_css()
{
$css = trim((string) Settings::get("custom_admin_css", ""));
if ($css === "") {
return;
}
// Keep the CSS from breaking out of its own <style> element.
$css = str_ireplace("</style>", "", $css);
echo "\n<style id=\"aurora-custom-admin-css\">\n" . $css . "\n</style>\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- admin-authored stylesheet, output raw inside a <style> tag by design; </style> stripped above to prevent breakout.
}
public static function admin_js()
{
$js = trim((string) Settings::get("custom_admin_js", ""));
if ($js === "") {
return;
}
$js = str_ireplace("</script>", "<\\/script>", $js);
echo "\n<script id=\"aurora-custom-admin-js\">\n" . $js . "\n</script>\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- admin-authored script, output raw inside a <script> tag by design.
}
public static function header_scripts()
{
$code = trim((string) Settings::get("header_scripts", ""));
if ($code === "") {
return;
}
echo "\n" . $code . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- admin-authored header markup (scripts/meta), printed verbatim by design.
}
public static function footer_scripts()
{
$code = trim((string) Settings::get("footer_scripts", ""));
if ($code === "") {
return;
}
echo "\n" . $code . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- admin-authored footer markup (scripts), printed verbatim by design.
}
}
+24 -9
View File
@@ -42,10 +42,25 @@ class LoginStyle
} }
$logo = trim((string) Settings::get("login_logo", "")); $logo = trim((string) Settings::get("login_logo", ""));
$bg_image = trim((string) Settings::get("login_bg_image", ""));
// Colors fall back to the original fixed dark theme when unset; invalid
// values are dropped by sanitize_hex_color (returns null) then fall back.
$bg_color = sanitize_hex_color((string) Settings::get("login_bg_color", "")) ?: "";
$form_bg = sanitize_hex_color((string) Settings::get("login_form_bg", "")) ?: "#1c2333";
$accent = sanitize_hex_color((string) Settings::get("login_button_color", "")) ?: "#4f7cff";
$custom_css = str_ireplace("</style>", "", (string) Settings::get("login_custom_css", ""));
if ($bg_image) {
$background = "url(" . esc_url($bg_image) . ") center / cover no-repeat";
} elseif ($bg_color) {
$background = $bg_color;
} else {
$background = "linear-gradient(135deg, #1b2333 0%, #0f1420 100%)";
}
?> ?>
<style> <style>
body.login { body.login {
background: linear-gradient(135deg, #1b2333 0%, #0f1420 100%); background: <?php echo $background; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- composed from a sanitize_hex_color() color or esc_url()'d image above. ?>;
} }
body.login #login h1 a { body.login #login h1 a {
<?php if ($logo): ?> <?php if ($logo): ?>
@@ -64,7 +79,7 @@ class LoginStyle
body.login form#loginform, body.login form#loginform,
body.login form#registerform, body.login form#registerform,
body.login form#lostpasswordform { body.login form#lostpasswordform {
background: #1c2333; background: <?php echo esc_attr($form_bg); ?>;
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px; border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35); box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
@@ -84,8 +99,8 @@ class LoginStyle
body.login input[type="text"]:focus, body.login input[type="text"]:focus,
body.login input[type="password"]:focus, body.login input[type="password"]:focus,
body.login input[type="email"]:focus { body.login input[type="email"]:focus {
border-color: #4f7cff; border-color: <?php echo esc_attr($accent); ?>;
box-shadow: 0 0 0 1px #4f7cff; box-shadow: 0 0 0 1px <?php echo esc_attr($accent); ?>;
} }
body.login .forgetmenot label { body.login .forgetmenot label {
color: #9fb0d0; color: #9fb0d0;
@@ -99,23 +114,23 @@ class LoginStyle
color: #fff; color: #fff;
} }
body.login .wp-core-ui .button-primary { body.login .wp-core-ui .button-primary {
background: #4f7cff; background: <?php echo esc_attr($accent); ?>;
border-color: #4f7cff; border-color: <?php echo esc_attr($accent); ?>;
box-shadow: none; box-shadow: none;
text-shadow: none; text-shadow: none;
} }
body.login .wp-core-ui .button-primary:hover, body.login .wp-core-ui .button-primary:hover,
body.login .wp-core-ui .button-primary:focus { body.login .wp-core-ui .button-primary:focus {
background: #3b63e0; filter: brightness(0.92);
border-color: #3b63e0;
} }
body.login #login_error, body.login #login_error,
body.login .message, body.login .message,
body.login .success { body.login .success {
background: #232b40; background: #232b40;
border-left-color: #4f7cff; border-left-color: <?php echo esc_attr($accent); ?>;
color: #e2e8f4; color: #e2e8f4;
} }
<?php echo $custom_css; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- admin-authored login CSS, output raw by design; </style> stripped above. ?>
</style> </style>
<?php <?php
} }
@@ -138,6 +138,7 @@ const installCompanion = async () => {
<textarea <textarea
v-else-if="field.type === 'textarea'" v-else-if="field.type === 'textarea'"
class="field__input field__textarea" class="field__input field__textarea"
:class="{ 'field__textarea--mono': field.mono }"
:placeholder="field.placeholder || ''" :placeholder="field.placeholder || ''"
:rows="field.rows || 6" :rows="field.rows || 6"
:value="val || ''" :value="val || ''"
@@ -346,6 +347,11 @@ const installCompanion = async () => {
} }
.field__input:focus { outline: none; border-color: var(--aurora-accent); } .field__input:focus { outline: none; border-color: var(--aurora-accent); }
.field__textarea { resize: vertical; font-family: inherit; line-height: 1.5; } .field__textarea { resize: vertical; font-family: inherit; line-height: 1.5; }
.field__textarea--mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
font-size: 0.8125rem;
tab-size: 2;
}
.field__color { display: flex; align-items: center; gap: 10px; } .field__color { display: flex; align-items: center; gap: 10px; }
.field__color input[type='color'] { .field__color input[type='color'] {
+105
View File
@@ -150,6 +150,48 @@ export const settingsGroups = [
type: 'image', type: 'image',
label: 'Login logo', label: 'Login logo',
description: 'Replace the WordPress logo on the login screen.', description: 'Replace the WordPress logo on the login screen.',
dependsOn: 'style_login',
},
{
key: 'login_bg_image',
type: 'image',
label: 'Background image',
description: 'Full-screen background for the login page. Overrides the background color below.',
dependsOn: 'style_login',
},
{
key: 'login_bg_color',
type: 'color',
label: 'Background color',
description: 'Solid background color for the login page (used when no background image is set).',
default: '#0f1420',
dependsOn: 'style_login',
},
{
key: 'login_form_bg',
type: 'color',
label: 'Form background',
description: 'Background color of the login form panel.',
default: '#1c2333',
dependsOn: 'style_login',
},
{
key: 'login_button_color',
type: 'color',
label: 'Button color',
description: 'Color of the primary “Log In” button and the field focus accent.',
default: '#4f7cff',
dependsOn: 'style_login',
},
{
key: 'login_custom_css',
type: 'textarea',
mono: true,
rows: 8,
label: 'Custom login CSS',
description: 'Extra CSS applied to the login screen, after Aurora’s own styles.',
placeholder: 'body.login { … }',
dependsOn: 'style_login',
}, },
], ],
}, },
@@ -388,6 +430,69 @@ export const settingsGroups = [
}, },
], ],
}, },
{
id: 'code',
title: 'Code',
icon: 'dashicons-editor-code',
categories: [
{
id: 'custom-code',
title: 'Custom CSS & JS',
subtitle: 'Inject your own CSS and JavaScript into the WordPress admin',
icon: 'dashicons-admin-customizer',
fields: [
{
key: 'custom_admin_css',
type: 'textarea',
mono: true,
rows: 10,
label: 'Admin CSS',
description:
'CSS added to every wp-admin page, inside a <style> tag in the admin <head>. Enter CSS only — no <style> tags needed.',
placeholder: '.wrap { max-width: 1200px; }',
},
{
key: 'custom_admin_js',
type: 'textarea',
mono: true,
rows: 10,
label: 'Admin JavaScript',
description:
'JavaScript added to the admin footer, inside a <script> tag. Enter JS only — no <script> tags needed.',
placeholder: "console.log('Hello from Aurora');",
},
],
},
{
id: 'header-footer',
title: 'Header & footer scripts',
subtitle: 'Inject raw code into your site’s front-end <head> and footer',
icon: 'dashicons-editor-code',
fields: [
{
key: 'header_scripts',
type: 'textarea',
mono: true,
rows: 8,
label: 'Header scripts',
description:
'Raw HTML/JS printed in the front-end <head> — analytics snippets, verification meta tags, etc. Include the full <script>/<meta> tags.',
placeholder: '<!-- e.g. analytics -->\n<script>…</script>',
},
{
key: 'footer_scripts',
type: 'textarea',
mono: true,
rows: 8,
label: 'Footer scripts',
description:
'Raw HTML/JS printed just before </body> on the front end. Include the full <script> tags.',
placeholder: '<script>…</script>',
},
],
},
],
},
{ {
id: 'add-ons', id: 'add-ons',
title: 'Add-ons', title: 'Add-ons',
+1
View File
@@ -61,6 +61,7 @@ require AURORA_ADMIN_PATH . "admin/src/Media/SvgUploads.php";
require AURORA_ADMIN_PATH . "admin/src/Media/MediaFolders.php"; require AURORA_ADMIN_PATH . "admin/src/Media/MediaFolders.php";
require AURORA_ADMIN_PATH . "admin/src/ImageFormats/ImageFormats.php"; require AURORA_ADMIN_PATH . "admin/src/ImageFormats/ImageFormats.php";
require AURORA_ADMIN_PATH . "admin/src/Security/Security.php"; require AURORA_ADMIN_PATH . "admin/src/Security/Security.php";
require AURORA_ADMIN_PATH . "admin/src/Code/CodeInjection.php";
require AURORA_ADMIN_PATH . "admin/src/Rest/MediaFolderData.php"; require AURORA_ADMIN_PATH . "admin/src/Rest/MediaFolderData.php";
require AURORA_ADMIN_PATH . "admin/src/Rest/BugReportData.php"; require AURORA_ADMIN_PATH . "admin/src/Rest/BugReportData.php";
require AURORA_ADMIN_PATH . "admin/src/Rest/CompanionPluginData.php"; require AURORA_ADMIN_PATH . "admin/src/Rest/CompanionPluginData.php";