From b0d1a0ecbdad2544d629522528a1b879524cf686 Mon Sep 17 00:00:00 2001
From: Nol de Roos <108540791+nolderoos@users.noreply.github.com>
Date: Tue, 12 May 2026 00:42:26 +0200
Subject: [PATCH 1/7] =?UTF-8?q?feat:=20v1.1=20beta=20=E2=80=94=20login=20s?=
=?UTF-8?q?tyling=20settings=20+=20iframe=20defenses?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
includes/Admin/Settings.php | 110 ++++++++++++++++
includes/Frontend/LoginScreen.php | 5 +
includes/Installer.php | 4 +
includes/helpers.php | 4 +
readme.txt | 4 +
templates/login-shell.php | 28 ++++-
tests/phpunit/Model/SettingsSanitizeTest.php | 124 +++++++++++++++++++
tests/phpunit/stubs/wp-functions.php | 27 ++++
8 files changed, 305 insertions(+), 1 deletion(-)
create mode 100644 tests/phpunit/Model/SettingsSanitizeTest.php
diff --git a/includes/Admin/Settings.php b/includes/Admin/Settings.php
index 8a8d95a..9c4b19e 100644
--- a/includes/Admin/Settings.php
+++ b/includes/Admin/Settings.php
@@ -28,6 +28,8 @@ final class Settings {
private const MAX_LINK_USES_PRESETS = [ 1, 2, 3, 5, 10 ];
+ private const FONT_STACK_KEYS = [ 'system', 'sans-modern', 'serif', 'mono', 'rounded' ];
+
// Keyed by extension regex (wp_check_filetype_and_ext format); membership checks hit values.
private const LOGO_MIMES = [
'png' => 'image/png',
@@ -199,6 +201,10 @@ private static function render_section_branding( bool $weak_salts ): void {
+
+
+
+
+
+
+
+
+
+ __( 'System default', 'magicauth' ),
+ 'sans-modern' => __( 'Modern sans (Inter)', 'magicauth' ),
+ 'serif' => __( 'Serif (Georgia)', 'magicauth' ),
+ 'mono' => __( 'Monospace', 'magicauth' ),
+ 'rounded' => __( 'Rounded', 'magicauth' ),
+ ];
+ ?>
+
+
+
+
+
+
+
+
+ '',
'logo_attachment_id' => 0,
'brand_color' => '#2271b1',
+ 'page_color' => '#eeeeee',
+ 'card_radius' => 6,
+ 'card_width' => 480,
+ 'font_stack' => 'system',
'agency_credit_name' => '',
'agency_credit_url' => '',
'agency_credit_icon_id' => 0,
diff --git a/includes/helpers.php b/includes/helpers.php
index e328bbc..4d2983d 100644
--- a/includes/helpers.php
+++ b/includes/helpers.php
@@ -30,6 +30,10 @@ function magicauth_get_settings(): array {
'company_name' => '',
'logo_attachment_id' => 0,
'brand_color' => '#2271b1',
+ 'page_color' => '#eeeeee',
+ 'card_radius' => 6,
+ 'card_width' => 480,
+ 'font_stack' => 'system',
'agency_credit_name' => '',
'agency_credit_url' => '',
'agency_credit_icon_id' => 0,
diff --git a/readme.txt b/readme.txt
index 9914330..0a15db5 100644
--- a/readme.txt
+++ b/readme.txt
@@ -74,5 +74,9 @@ MagicAuth registers a WordPress privacy exporter and eraser. Personal data store
== Changelog ==
+= 1.1.0 =
+* Branding: page background color, card corner radius, card max width, and font family are now admin-controllable from Settings > MagicAuth.
+* Security: `wp-login.php` now sends `X-Frame-Options: DENY` and `Content-Security-Policy: frame-ancestors 'none'` to refuse framing entirely.
+
= 1.0.0 =
* Initial public release.
diff --git a/templates/login-shell.php b/templates/login-shell.php
index a1f118d..fa6ff76 100644
--- a/templates/login-shell.php
+++ b/templates/login-shell.php
@@ -19,17 +19,43 @@
defined( 'ABSPATH' ) || exit;
?>
' or a future maintainer can break the ' as a block-terminator regardless of CSS
+ // quoting. Pin a static assertion so a future maintainer adding e.g.
+ // 'fancy' => 'Helvetica' cannot silently break the
assertEqualsWithDelta( 21.0, magicauth_contrast_ratio( '#000', '#ffffff' ), 0.01 );
+ }
+
+ public function test_identical_colors_yield_1(): void {
+ $this->assertEqualsWithDelta( 1.0, magicauth_contrast_ratio( '#abcdef', '#abcdef' ), 0.001 );
+ }
+
+ public function test_wp_blue_on_white_is_aa_normal(): void {
+ $r = magicauth_contrast_ratio( '#2271b1', '#ffffff' );
+ $this->assertGreaterThanOrEqual( 4.5, $r );
+ $this->assertLessThan( 7.0, $r );
+ }
+
+ public function test_three_digit_hex_normalizes_like_six(): void {
+ $this->assertEqualsWithDelta(
+ magicauth_contrast_ratio( '#abc', '#ffffff' ),
+ magicauth_contrast_ratio( '#aabbcc', '#ffffff' ),
+ 0.01
+ );
+ }
+
+ public function test_symmetric(): void {
+ $a = magicauth_contrast_ratio( '#123456', '#abcdef' );
+ $b = magicauth_contrast_ratio( '#abcdef', '#123456' );
+ $this->assertEqualsWithDelta( $a, $b, 0.001 );
+ }
+
+ public function test_garbage_input_returns_1(): void {
+ $this->assertSame( 1.0, magicauth_contrast_ratio( 'pirate', '#ffffff' ) );
+ }
+
+ public function test_alpha_hex_rejected(): void {
+ // 8-char form is not a valid 3/6-digit hex → 1.0 sentinel.
+ $this->assertSame( 1.0, magicauth_contrast_ratio( '#ff000080', '#ffffff' ) );
+ }
+
+ public function test_evaluate_thresholds(): void {
+ $this->assertSame( 'fail', magicauth_contrast_evaluate( 2.0, 'normal' ) );
+ $this->assertSame( 'warn', magicauth_contrast_evaluate( 4.0, 'normal' ) );
+ $this->assertSame( 'pass', magicauth_contrast_evaluate( 5.0, 'normal' ) );
+ $this->assertSame( 'pass', magicauth_contrast_evaluate( 3.5, 'ui' ) );
+ $this->assertSame( 'pass', magicauth_contrast_evaluate( 3.5, 'large' ) );
+ }
+}
diff --git a/tests/phpunit/Model/SettingsSanitizeTest.php b/tests/phpunit/Model/SettingsSanitizeTest.php
index 539861c..1a399bd 100644
--- a/tests/phpunit/Model/SettingsSanitizeTest.php
+++ b/tests/phpunit/Model/SettingsSanitizeTest.php
@@ -102,6 +102,151 @@ public function test_defaults_returned_for_missing_keys(): void {
$this->assertSame( 'system', $settings['font_stack'] );
}
+ public function test_link_color_accepts_empty(): void {
+ $out = $this->sanitize( [ 'link_color' => '' ] );
+ $this->assertSame( '', $out['link_color'] );
+ $this->assertEmpty( $this->settings_error_codes(), 'empty link_color should produce no notice' );
+ }
+
+ public function test_link_color_accepts_high_contrast_hex(): void {
+ $out = $this->sanitize( [ 'link_color' => '#0044aa' ] );
+ $this->assertSame( '#0044aa', $out['link_color'] );
+ $this->assertEmpty( $this->settings_error_codes(), 'AA+ link_color should be silent' );
+ }
+
+ public function test_link_color_rejects_invalid_hex(): void {
+ $out = $this->sanitize( [ 'link_color' => 'pirate' ] );
+ // Default link_color is empty; invalid input restores previous value.
+ $this->assertSame( '', $out['link_color'] );
+ $this->assertContains( 'magicauth_link_color_invalid', $this->settings_error_codes() );
+ }
+
+ public function test_link_color_rejects_unreadable_below_floor(): void {
+ // #bbbbbb on #ffffff ≈ 1.9:1 — well under the 2.5 hard floor.
+ $out = $this->sanitize( [ 'link_color' => '#bbbbbb' ] );
+ $this->assertSame( '', $out['link_color'], 'fail verdict must restore previous (empty default)' );
+ $this->assertContains( 'magicauth_link_color_unreadable', $this->settings_error_codes() );
+ }
+
+ public function test_link_color_saves_with_warning_in_warn_band(): void {
+ // #888888 on #ffffff ≈ 3.5:1 — between the 2.5 floor and AA 4.5.
+ $out = $this->sanitize( [ 'link_color' => '#888888' ] );
+ $this->assertSame( '#888888', $out['link_color'], 'warn verdict must still save' );
+ $this->assertContains( 'magicauth_link_color_low_contrast', $this->settings_error_codes() );
+ }
+
+ public function test_link_color_array_input_is_ignored(): void {
+ $out = $this->sanitize( [ 'link_color' => [ '#ff0000' ] ] );
+ $this->assertSame( '', $out['link_color'] );
+ }
+
+ public function test_color_mode_round_trips_allowlist(): void {
+ foreach ( [ 'light', 'dark', 'auto' ] as $mode ) {
+ $out = $this->sanitize( [ 'color_mode' => $mode ] );
+ $this->assertSame( $mode, $out['color_mode'], "mode '{$mode}' should round-trip" );
+ }
+ }
+
+ public function test_color_mode_rejects_unknown(): void {
+ $out = $this->sanitize( [ 'color_mode' => 'midnight' ] );
+ $this->assertSame( 'light', $out['color_mode'] );
+ }
+
+ public function test_color_mode_rejects_array_input(): void {
+ $out = $this->sanitize( [ 'color_mode' => [ 'dark' ] ] );
+ // Default survives via is_scalar guard.
+ $this->assertSame( 'light', $out['color_mode'] );
+ }
+
+ public function test_brand_dark_contrast_warning_fires_for_low_contrast(): void {
+ $out = $this->sanitize( [
+ 'brand_color' => '#444444',
+ 'color_mode' => 'dark',
+ ] );
+ $this->assertSame( '#444444', $out['brand_color'] );
+ $this->assertSame( 'dark', $out['color_mode'] );
+ $this->assertContains( 'magicauth_brand_dark_contrast', $this->settings_error_codes() );
+ }
+
+ public function test_brand_dark_contrast_silent_in_light_mode(): void {
+ // Same low-contrast brand, but light mode — no dark-surface check applies.
+ $out = $this->sanitize( [
+ 'brand_color' => '#444444',
+ 'color_mode' => 'light',
+ ] );
+ $this->assertNotContains( 'magicauth_brand_dark_contrast', $this->settings_error_codes() );
+ }
+
+ public function test_background_id_zero_clears(): void {
+ $out = $this->sanitize( [ 'background_attachment_id' => 0 ] );
+ $this->assertSame( 0, $out['background_attachment_id'] );
+ $this->assertEmpty( $this->settings_error_codes(), 'zero should be silent' );
+ }
+
+ public function test_background_id_rejects_non_image_attachment(): void {
+ $path = $this->make_temp_file( '.png', $this->one_pixel_png_bytes() );
+ try {
+ magicauth_test_register_attachment_file( 42, $path, false /* is_image = false */ );
+ $out = $this->sanitize( [ 'background_attachment_id' => 42 ] );
+ $this->assertSame( 0, $out['background_attachment_id'] );
+ $this->assertContains( 'magicauth_bg_not_image', $this->settings_error_codes() );
+ } finally {
+ @unlink( $path );
+ }
+ }
+
+ public function test_background_id_accepts_valid_png(): void {
+ $path = $this->make_temp_file( '.png', $this->one_pixel_png_bytes() );
+ try {
+ magicauth_test_register_attachment_file( 7, $path, true );
+ $out = $this->sanitize( [ 'background_attachment_id' => 7 ] );
+ $this->assertSame( 7, $out['background_attachment_id'] );
+ $this->assertEmpty( $this->settings_error_codes(), 'valid PNG should be silent' );
+ } finally {
+ @unlink( $path );
+ }
+ }
+
+ public function test_background_id_rejects_svg(): void {
+ $path = $this->make_temp_file( '.svg', '' );
+ try {
+ magicauth_test_register_attachment_file( 11, $path, true );
+ $out = $this->sanitize( [ 'background_attachment_id' => 11 ] );
+ $this->assertSame( 0, $out['background_attachment_id'] );
+ $this->assertContains( 'magicauth_bg_bad_ext', $this->settings_error_codes() );
+ } finally {
+ @unlink( $path );
+ }
+ }
+
+ public function test_background_id_returns_zero_when_path_missing(): void {
+ // Image flag set but no path registered → get_attached_file() returns false.
+ global $magicauth_test_state;
+ $magicauth_test_state['attachment_is_image'][ 99 ] = true;
+ $out = $this->sanitize( [ 'background_attachment_id' => 99 ] );
+ $this->assertSame( 0, $out['background_attachment_id'] );
+ }
+
+ private function make_temp_file( string $extension, string $bytes ): string {
+ $path = sys_get_temp_dir() . '/magicauth-bg-test-' . uniqid( '', true ) . $extension;
+ file_put_contents( $path, $bytes );
+ return $path;
+ }
+
+ private function one_pixel_png_bytes(): string {
+ // Minimal 1×1 transparent PNG — finfo identifies as image/png.
+ return base64_decode(
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
+ true
+ ) ?: '';
+ }
+
+ /** @return list */
+ private function settings_error_codes(): array {
+ $errors = $GLOBALS['magicauth_test_state']['settings_errors'] ?? [];
+ return array_map( static fn ( $e ) => (string) ( $e['code'] ?? '' ), $errors );
+ }
+
public function test_font_stacks_in_shell_template_contain_no_html_meta_chars(): void {
// HTML parsers treat '' as a block-terminator regardless of CSS
// quoting. Pin a static assertion so a future maintainer adding e.g.
diff --git a/tests/phpunit/stubs/wp-functions.php b/tests/phpunit/stubs/wp-functions.php
index ace9f1a..94208cd 100644
--- a/tests/phpunit/stubs/wp-functions.php
+++ b/tests/phpunit/stubs/wp-functions.php
@@ -48,6 +48,9 @@ function update_option( string $option, $value, $autoload = null ): bool {
unset( $autoload );
global $magicauth_test_state;
$magicauth_test_state['options'][ $option ] = $value;
+ if ( 'magicauth_settings' === $option && function_exists( 'magicauth_invalidate_settings_cache' ) ) {
+ magicauth_invalidate_settings_cache();
+ }
return true;
}
}
@@ -59,6 +62,9 @@ function add_option( string $option, $value ): bool {
return false;
}
$magicauth_test_state['options'][ $option ] = $value;
+ if ( 'magicauth_settings' === $option && function_exists( 'magicauth_invalidate_settings_cache' ) ) {
+ magicauth_invalidate_settings_cache();
+ }
return true;
}
}
@@ -67,6 +73,9 @@ function add_option( string $option, $value ): bool {
function delete_option( string $option ): bool {
global $magicauth_test_state;
unset( $magicauth_test_state['options'][ $option ] );
+ if ( 'magicauth_settings' === $option && function_exists( 'magicauth_invalidate_settings_cache' ) ) {
+ magicauth_invalidate_settings_cache();
+ }
return true;
}
}
@@ -656,6 +665,37 @@ function wp_get_attachment_image_src( int $id, string $size = 'thumbnail' ) {
}
}
+if ( ! function_exists( 'wp_attachment_is_image' ) ) {
+ function wp_attachment_is_image( int $id ): bool {
+ global $magicauth_test_state;
+ return ! empty( $magicauth_test_state['attachment_is_image'][ $id ] );
+ }
+}
+
+if ( ! function_exists( 'get_attached_file' ) ) {
+ function get_attached_file( int $id ) {
+ global $magicauth_test_state;
+ return $magicauth_test_state['attachment_paths'][ $id ] ?? false;
+ }
+}
+
+if ( ! function_exists( 'wp_check_filetype_and_ext' ) ) {
+ // Simplified: match by file extension against the allowlist regex keys.
+ // Real WP also peeks at content via finfo; we leave that to the caller's
+ // own finfo block, since duplicating WP's logic here adds little value.
+ function wp_check_filetype_and_ext( string $file, string $filename, array $mimes = [] ): array {
+ unset( $file );
+ $ext = strtolower( (string) pathinfo( $filename, PATHINFO_EXTENSION ) );
+ foreach ( $mimes as $regex => $mime ) {
+ $alternatives = explode( '|', $regex );
+ if ( in_array( $ext, $alternatives, true ) ) {
+ return [ 'ext' => $ext, 'type' => $mime, 'proper_filename' => false ];
+ }
+ }
+ return [ 'ext' => false, 'type' => false, 'proper_filename' => false ];
+ }
+}
+
if ( ! function_exists( 'magicauth_test_register_attachment' ) ) {
function magicauth_test_register_attachment( int $id, string $url ): void {
global $magicauth_test_state;
@@ -663,6 +703,19 @@ function magicauth_test_register_attachment( int $id, string $url ): void {
}
}
+if ( ! function_exists( 'magicauth_test_register_attachment_file' ) ) {
+ /**
+ * Test helper: bind an attachment ID to an on-disk path so sanitize_background()
+ * sees a real file (finfo needs bytes). Marks the attachment as an image by
+ * default; pass $is_image=false to simulate a non-image attachment.
+ */
+ function magicauth_test_register_attachment_file( int $id, string $path, bool $is_image = true ): void {
+ global $magicauth_test_state;
+ $magicauth_test_state['attachment_paths'][ $id ] = $path;
+ $magicauth_test_state['attachment_is_image'][ $id ] = $is_image;
+ }
+}
+
if ( ! function_exists( 'magicauth_test_register_user' ) ) {
/**
* Test helper: register a stub WP_User in the in-memory user table.
@@ -729,14 +782,19 @@ function add_settings_error( string $setting, string $code, string $message, str
function magicauth_test_reset_state(): void {
global $magicauth_test_state, $wpdb;
$magicauth_test_state = [
- 'options' => [],
- 'usermeta' => [],
- 'transients' => [],
- 'users' => [],
- 'actions' => [],
- 'filters' => [],
- 'attachments' => [],
+ 'options' => [],
+ 'usermeta' => [],
+ 'transients' => [],
+ 'users' => [],
+ 'actions' => [],
+ 'filters' => [],
+ 'attachments' => [],
+ 'attachment_paths' => [],
+ 'attachment_is_image' => [],
];
+ if ( function_exists( 'magicauth_invalidate_settings_cache' ) ) {
+ magicauth_invalidate_settings_cache();
+ }
if ( isset( $wpdb ) && method_exists( $wpdb, 'truncate_magicauth_table' ) ) {
$wpdb->truncate_magicauth_table();
}
From f21d9ed19f787600764dac6788a63da140d226b8 Mon Sep 17 00:00:00 2001
From: r00bbert <280805750+r00bbert@users.noreply.github.com>
Date: Tue, 12 May 2026 14:11:45 +0200
Subject: [PATCH 3/7] fix: split branding and appearance
---
assets/css/magicauth-admin.css | 14 --------------
includes/Admin/Settings.php | 14 +++++++++++---
2 files changed, 11 insertions(+), 17 deletions(-)
diff --git a/assets/css/magicauth-admin.css b/assets/css/magicauth-admin.css
index 36db28f..242fc93 100644
--- a/assets/css/magicauth-admin.css
+++ b/assets/css/magicauth-admin.css
@@ -413,20 +413,6 @@ body.admin-bar .magicauth-admin .magicauth-topbar__bar { top: 32px; }
border-radius: 4px;
}
-/* Sub-card subheading inside .magicauth-card */
-.magicauth-admin .magicauth-card__subhead {
- margin: 0 0 16px;
- font-size: 13px;
- font-weight: 600;
- color: var(--tx-muted);
- text-transform: uppercase;
- letter-spacing: 0.04em;
-}
-
-.magicauth-admin .magicauth-card + .magicauth-card {
- margin-top: 16px;
-}
-
/* Cards. Namespaced to avoid colliding with WP core `.card` (caps at 520px). */
.magicauth-admin .magicauth-card {
background: var(--ettic-surface);
diff --git a/includes/Admin/Settings.php b/includes/Admin/Settings.php
index 47691fa..b1bbd08 100644
--- a/includes/Admin/Settings.php
+++ b/includes/Admin/Settings.php
@@ -178,6 +178,7 @@ public static function render_page(): void {
+
@@ -211,18 +212,25 @@ private static function render_section_branding( bool $weak_salts ): void {
-
-
+
+
+
+
+
+
+
-
From 5ec6e8fe8c2f1254c97c4989d5c5e0bb6aee266a Mon Sep 17 00:00:00 2001
From: r00bbert <280805750+r00bbert@users.noreply.github.com>
Date: Tue, 12 May 2026 14:12:38 +0200
Subject: [PATCH 4/7] fix: link color follows brand when blank
---
assets/js/magicauth-admin.js | 44 ++++++++++++++++++++++++++++++++++++
includes/Admin/Settings.php | 2 +-
2 files changed, 45 insertions(+), 1 deletion(-)
diff --git a/assets/js/magicauth-admin.js b/assets/js/magicauth-admin.js
index 6424c47..67b59ce 100644
--- a/assets/js/magicauth-admin.js
+++ b/assets/js/magicauth-admin.js
@@ -12,6 +12,7 @@
function init() {
initMediaPickers();
initColorPickers();
+ initColorFollowers();
initRowDirtyMarks();
initDirtyTracking();
initCharCounters();
@@ -107,6 +108,49 @@
} );
}
+ // Follower color pickers: when the text input is blank, the swatch mirrors
+ // a source picker (e.g. link_color follows brand_color). Typing a hex or
+ // picking a swatch color writes to the text input, which disconnects.
+ function initColorFollowers() {
+ var HEX_RE = /^#?[0-9a-fA-F]{6}$/;
+ var normalize = function ( v ) { return v.charAt( 0 ) === '#' ? v : '#' + v; };
+
+ document.querySelectorAll( '.magicauth-admin .magicauth-color[data-magicauth-color-follows]' ).forEach( function ( follower ) {
+ var sourceKey = follower.getAttribute( 'data-magicauth-color-follows' );
+ if ( ! sourceKey ) {
+ return;
+ }
+ var sourceText = document.querySelector( 'input[name="magicauth_settings[' + sourceKey + ']"]' );
+ var swatch = follower.querySelector( 'input[type="color"]' );
+ var text = follower.querySelector( 'input[type="text"]' );
+ if ( ! sourceText || ! swatch || ! text ) {
+ return;
+ }
+
+ function isConnected() { return '' === text.value.trim(); }
+ function mirrorFromSource() {
+ var v = sourceText.value.trim();
+ if ( HEX_RE.test( v ) ) {
+ swatch.value = normalize( v );
+ }
+ }
+
+ // Cleared text → snap swatch back to source.
+ text.addEventListener( 'input', function () {
+ if ( isConnected() ) {
+ mirrorFromSource();
+ }
+ } );
+
+ // Source moved → mirror only while connected.
+ sourceText.addEventListener( 'input', function () {
+ if ( isConnected() ) {
+ mirrorFromSource();
+ }
+ } );
+ } );
+ }
+
// Per-row dirty mark — auto-injected, opacity toggled via .is-dirty
function initRowDirtyMarks() {
document.querySelectorAll( '.magicauth-admin .magicauth-row' ).forEach( function ( row ) {
diff --git a/includes/Admin/Settings.php b/includes/Admin/Settings.php
index 47691fa..f29015d 100644
--- a/includes/Admin/Settings.php
+++ b/includes/Admin/Settings.php
@@ -870,7 +870,7 @@ public static function field_link_color(): void {
-
+
From ad87fbc62d52f571274a466cbc09c3727f86ba80 Mon Sep 17 00:00:00 2001
From: r00bbert <280805750+r00bbert@users.noreply.github.com>
Date: Tue, 12 May 2026 14:13:11 +0200
Subject: [PATCH 5/7] fix: match diagnostics row padding
---
assets/css/magicauth-admin.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/assets/css/magicauth-admin.css b/assets/css/magicauth-admin.css
index 36db28f..c5b676f 100644
--- a/assets/css/magicauth-admin.css
+++ b/assets/css/magicauth-admin.css
@@ -885,7 +885,7 @@ body.admin-bar .magicauth-admin .magicauth-topbar__bar { top: 32px; }
align-items: center;
justify-content: space-between;
gap: 16px;
- padding: 14px 8px;
+ padding: 14px;
position: relative;
}
From 124fdc189e86c0a317ddcd4b1a85daa31d53ae97 Mon Sep 17 00:00:00 2001
From: Nol de Roos <108540791+nolderoos@users.noreply.github.com>
Date: Tue, 12 May 2026 14:59:33 +0200
Subject: [PATCH 6/7] fix: point Plugin URI to plugins.ettic.nl/magicauth
Updates the Plugin URI header so the "Visit plugin site" link on the
WordPress Plugins screen resolves to the canonical product page instead
of the GitHub source repo.
---
magicauth.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/magicauth.php b/magicauth.php
index 21db5cb..401a33f 100644
--- a/magicauth.php
+++ b/magicauth.php
@@ -1,7 +1,7 @@
Date: Wed, 13 May 2026 11:56:15 +0200
Subject: [PATCH 7/7] fix: address CodeRabbit feedback on login styling PR
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Item 4 (Minor): initialize follower swatch on first render so the
preview matches the source picker before any user input.
https://github.com/EtticDevelopment/magicauth/pull/4#discussion_r3226398090
- Item 5 (Nitpick): add prominent bidirectional SYNC-LOCK comments
cross-referencing the duplicated dark-surface hex in both
assets/css/magicauth.css and includes/Admin/Settings.php. Chose
option (c) — option (a) would require injecting only one of nine
dark-mode tokens via PHP, creating asymmetry vs the eight others
that legitimately live in CSS.
- Item 6 (Nitpick): same SYNC-LOCK comment on the PHP constant side
names both CSS line numbers and the cascade direction.
- Item 7 (Nitpick): extract magic number into
private const BACKGROUND_SIZE_SOFT_LIMIT_BYTES = 1024 * 1024 and
replace both call sites.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
assets/css/magicauth.css | 18 ++++++++++++++----
assets/js/magicauth-admin.js | 5 +++++
includes/Admin/Settings.php | 18 +++++++++++++-----
3 files changed, 32 insertions(+), 9 deletions(-)
diff --git a/assets/css/magicauth.css b/assets/css/magicauth.css
index dd22b26..a93abd0 100644
--- a/assets/css/magicauth.css
+++ b/assets/css/magicauth.css
@@ -451,10 +451,16 @@
/* Dark mode — explicit. Body-class specificity (0,1,1) beats :root (0,0,1),
so the shell-vars block can still set per-instance brand_color / link_color /
font_stack without being overridden. Curated values, not exposed to admin
- pickers. The surface hex must match Admin\Settings::DARK_SURFACE_HEX. */
+ pickers.
+ !!! SYNC-LOCK !!! --magicauth-color-surface (#181c22) MUST equal the PHP
+ constant MagicAuth\Admin\Settings::DARK_SURFACE_HEX
+ (includes/Admin/Settings.php:~42). The PHP constant feeds the brand-color
+ contrast check; the CSS variable paints the card. If either changes without
+ the other, dark-mode contrast warnings will lie. Value also duplicated at
+ line ~482 below (auto-mode @media block) — update all three together. */
body.magicauth-mode-dark {
--magicauth-color-page: #0e1116;
- --magicauth-color-surface: #181c22;
+ --magicauth-color-surface: #181c22; /* SYNC with Settings::DARK_SURFACE_HEX */
--magicauth-color-code-bg: #202832;
--magicauth-color-text: #eef0f3;
--magicauth-color-text-muted: #9aa3ad;
@@ -465,11 +471,15 @@ body.magicauth-mode-dark {
}
/* Dark mode — auto. Only kicks in when admin chose color_mode = auto AND
- the visitor's browser reports a dark preference. */
+ the visitor's browser reports a dark preference.
+ !!! SYNC-LOCK !!! --magicauth-color-surface (#181c22) MUST equal the PHP
+ constant MagicAuth\Admin\Settings::DARK_SURFACE_HEX
+ (includes/Admin/Settings.php:~42). See full note above the explicit-dark
+ block (~line 451). Three locations to update in lockstep. */
@media (prefers-color-scheme: dark) {
body.magicauth-mode-auto {
--magicauth-color-page: #0e1116;
- --magicauth-color-surface: #181c22;
+ --magicauth-color-surface: #181c22; /* SYNC with Settings::DARK_SURFACE_HEX */
--magicauth-color-code-bg: #202832;
--magicauth-color-text: #eef0f3;
--magicauth-color-text-muted: #9aa3ad;
diff --git a/assets/js/magicauth-admin.js b/assets/js/magicauth-admin.js
index 67b59ce..497b670 100644
--- a/assets/js/magicauth-admin.js
+++ b/assets/js/magicauth-admin.js
@@ -148,6 +148,11 @@
mirrorFromSource();
}
} );
+
+ // Initial render: if follower is blank, mirror immediately.
+ if ( isConnected() ) {
+ mirrorFromSource();
+ }
} );
}
diff --git a/includes/Admin/Settings.php b/includes/Admin/Settings.php
index b33921d..b81960a 100644
--- a/includes/Admin/Settings.php
+++ b/includes/Admin/Settings.php
@@ -32,9 +32,13 @@ final class Settings {
private const COLOR_MODE_KEYS = [ 'light', 'dark', 'auto' ];
- // Curated dark-mode surface — must match the value emitted by magicauth.css
- // for body.magicauth-mode-dark / .magicauth-mode-auto. Used by the
- // brand-color contrast check below.
+ // Curated dark-mode surface — used by the brand-color contrast check below.
+ // !!! SYNC-LOCK !!! This value MUST equal the --magicauth-color-surface
+ // declarations in assets/css/magicauth.css at:
+ // - line ~463 (body.magicauth-mode-dark block)
+ // - line ~482 (@media (prefers-color-scheme: dark) > body.magicauth-mode-auto)
+ // Three locations, one truth. If you change one, change all three or the
+ // contrast warning shown to admins will disagree with the rendered card.
private const DARK_SURFACE_HEX = '#181c22';
// Keyed by extension regex (wp_check_filetype_and_ext format); membership checks hit values.
@@ -53,6 +57,10 @@ final class Settings {
'webp' => 'image/webp',
];
+ // Background image soft-limit. Files larger than this trigger a non-blocking
+ // "consider compressing" admin notice but still save successfully.
+ private const BACKGROUND_SIZE_SOFT_LIMIT_BYTES = 1024 * 1024; // 1 MB.
+
public static function setup(): void {
add_action( 'admin_init', [ self::class, 'register' ] );
add_action( 'admin_menu', [ self::class, 'menu' ] );
@@ -654,14 +662,14 @@ private static function sanitize_background( int $attachment_id ): int {
}
$size = (int) @filesize( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
- if ( $size > 1024 * 1024 ) {
+ if ( $size > self::BACKGROUND_SIZE_SOFT_LIMIT_BYTES ) {
add_settings_error(
self::OPTION_NAME,
'magicauth_bg_large',
sprintf(
/* translators: %s: file size in MB */
__( 'Background image saved. File is %s MB — consider compressing for faster page load.', 'magicauth' ),
- number_format( $size / ( 1024 * 1024 ), 1 )
+ number_format( $size / self::BACKGROUND_SIZE_SOFT_LIMIT_BYTES, 1 )
),
'warning'
);