diff --git a/src/InventoryAdmin.php b/src/InventoryAdmin.php index 70e6e0f..780b16d 100644 --- a/src/InventoryAdmin.php +++ b/src/InventoryAdmin.php @@ -16,6 +16,11 @@ * value is escaped before it reaches the page. The SKU/location inputs offer a * `` of what already exists (a typo-guard, not a hard gate — receiving a * genuinely new SKU is still allowed by typing it). + * + * 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])) { @@ -293,8 +319,8 @@ private function forms(string $csrf): string { $t = ''; - return '
' - . '
' + return '
' + . '' . '

Receive stock

' . $t . $this->field('SKU', 'sku', 'e.g. house-blend', 'inv-skus') . $this->field('Location', 'location', 'main', 'inv-locs') @@ -302,7 +328,7 @@ private function forms(string $csrf): string . $this->field('Unit', 'uom', 'each') . '' - . '
' + . '' . '

Adjust stock

' . $t . $this->field('SKU', 'sku', 'e.g. house-blend', 'inv-skus') . $this->field('Location', 'location', 'main', 'inv-locs') @@ -310,15 +336,15 @@ private function forms(string $csrf): string . $this->field('Reason', 'reason', 'waste') . '
' - . '
' + . '' . '

Count stock

' . $t - . '

Set on-hand to a counted figure; the correction is recorded as a movement.

' + . '

Set on-hand to a counted figure; the correction is recorded as a movement.

' . $this->field('SKU', 'sku', 'e.g. house-blend', 'inv-skus') . $this->field('Location', 'location', 'main', 'inv-locs') . $this->field('Counted', 'qty', 'e.g. 40') . '
' - . '
' + . '' . '

Transfer stock

' . $t . $this->field('SKU', 'sku', 'e.g. house-blend', 'inv-skus') . $this->field('From', 'from', 'main', 'inv-locs') @@ -331,8 +357,8 @@ private function forms(string $csrf): string /** A SKU substring filter for the stock table (GET, no JS). */ private function filterForm(string $q): string { - return '' - . '
' + return '' + . '
' . '
' . '' . ($q === '' ? '' : ' Clear') diff --git a/src/InventoryPlugin.php b/src/InventoryPlugin.php index 06d7cc1..e998afa 100644 --- a/src/InventoryPlugin.php +++ b/src/InventoryPlugin.php @@ -61,7 +61,7 @@ public function register(PluginContext $context): void 'inventory', 'Inventory', '📦', - static fn (Request $r, string $nonce = '', string $csrf = ''): string => (new InventoryAdmin($storage))->render($csrf, $r->query('ok') ?? $r->query('err'), $r->query('q'), $r->query('sku')), + static fn (Request $r, string $nonce = '', string $csrf = ''): string => (new InventoryAdmin($storage))->render($csrf, $r->query('ok') ?? $r->query('err'), $r->query('q'), $r->query('sku'), $nonce), self::ID . ':write', ); $context->adminPages()->action('inventory', 'receive', static function (Request $r) use ($ledger): Response { diff --git a/tests/InventoryAdminTest.php b/tests/InventoryAdminTest.php index d45f907..0473ab6 100644 --- a/tests/InventoryAdminTest.php +++ b/tests/InventoryAdminTest.php @@ -135,4 +135,17 @@ public function test_an_unknown_sku_shows_a_helpful_note(): void $html = $this->admin->render('tok', null, null, 'no-such-sku'); self::assertStringContainsString('Nothing recorded for this SKU', $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. + // Both the overview and the drill-down must style via one nonce'd block. + $overview = $this->admin->render('tok', null, null, null, 'NONCE123'); + self::assertStringContainsString('