diff --git a/src/CommerceAdmin.php b/src/CommerceAdmin.php index 9d6c4f9..32d7034 100644 --- a/src/CommerceAdmin.php +++ b/src/CommerceAdmin.php @@ -15,6 +15,11 @@ * escaped before it reaches the page. Status "pills" use the admin theme's * semantic tokens (which redefine per theme) rather than hard-coded colours, so * they stay legible in dark and every selectable theme. + * + * The admin CSP is nonce-only for `style-src` (no `'unsafe-inline'`), so inline + * `style=` attributes are dropped by the browser. All styling therefore lives in + * ONE nonce-carrying `'; + } + private function notice(?string $notice): string { if ($notice === null || !isset(self::NOTICES[$notice])) { @@ -199,13 +231,11 @@ private function statusLabel(string $status): string }; } - /** A coloured status pill using theme tokens (dark-safe). */ + /** A coloured status pill (dark-safe via the nonce'd tone classes in {@see styles}). */ private function pill(string $status): string { - [$bg, $fg] = self::STATUS_TONE[$status] ?? ['--nb-surface-2', '--nb-muted']; - - return '' . $this->e($status) . ''; + $mod = isset(self::STATUS_TONE[$status]) ? ' cx-pill--' . $status : ''; + return '' . $this->e($status) . ''; } /** The lifecycle buttons valid for this order's status; each is a CSRF-protected POST. */ @@ -220,13 +250,13 @@ private function actions(string $reference, string $status, string $csrf): strin return ''; } - $out = '
'; + $out = '
'; foreach ($verbs as $action => $label) { $primary = $action === 'cancel' ? '' : ' nb-btn-primary'; - $out .= '
' + $out .= '' . '' . '' - . '' + . '' . '
'; } return $out . '
'; @@ -242,8 +272,7 @@ private function statusFilter(?string $active): string return '' . $this->e($label) . ''; }; - $out = '
' - . $chip('All', null); + $out = '
' . $chip('All', null); foreach (array_keys(self::STATUS_TONE) as $status) { $out .= $chip(ucfirst($status), $status); } @@ -279,14 +308,14 @@ private function placeForm(string $csrf): string { $f = function (string $label, string $name, string $ph, bool $suggest = false): string { $list = $suggest ? ' list="ord-skus"' : ''; - return '
' + return '
' . '
'; }; - return '
' + return '' . '

Place an order

' . '' - . '
' + . '
' . $f('SKU', 'sku', 'house-blend', true) . $f('Location', 'location', 'main') . $f('Qty', 'qty', '2') diff --git a/src/CommercePlugin.php b/src/CommercePlugin.php index 9fb97bb..bf15923 100644 --- a/src/CommercePlugin.php +++ b/src/CommercePlugin.php @@ -51,7 +51,7 @@ public function register(PluginContext $context): void 'commerce', 'Commerce', '🧾', - static fn (Request $r, string $nonce = '', string $csrf = ''): string => (new CommerceAdmin($storage))->render($csrf, $r->query('ok') ?? $r->query('err'), $r->query('status'), $r->query('order')), + static fn (Request $r, string $nonce = '', string $csrf = ''): string => (new CommerceAdmin($storage))->render($csrf, $r->query('ok') ?? $r->query('err'), $r->query('status'), $r->query('order'), $nonce), self::ID . ':write', ); $context->adminPages()->action('commerce', 'place', static function (Request $r) use ($orders): Response { diff --git a/tests/CommerceAdminTest.php b/tests/CommerceAdminTest.php index bc81684..ac5e1e9 100644 --- a/tests/CommerceAdminTest.php +++ b/tests/CommerceAdminTest.php @@ -159,4 +159,17 @@ public function test_the_order_detail_escapes_a_hostile_reference(): void self::assertStringNotContainsString('', $html); self::assertStringContainsString('No order with that reference', $html); } + + public function test_styling_is_a_nonced_style_block_not_inline_attributes(): void + { + // The admin CSP is nonce-only for style-src, so inline style= is dropped. + // Styling must live in one nonce-carrying