fix(labels): single PDF per click (#1628)

window.open(url, '_blank', 'noopener,noreferrer') returns null even on
  success per the WindowFeatures spec — `noopener` deliberately suppresses
  the return reference. The label-print modal treated null as "popup
  blocked → fall back to <a download> click", so the fallback fired on
  every click. Result: window.open opened the blob tab (downloading a
  random-named PDF on systems without an inline viewer) AND the fallback
  downloaded bambuddy-labels.pdf — two identical PDFs per click.

  Drop noopener,noreferrer. The blob is same-origin, the destination is a
  passive PDF preview tab with no script context, and noreferrer is no-op
  for blob URLs. window.open now returns a real window reference on
  success and the if (!win) fallback only fires on genuine popup-block.
This commit is contained in:
maziggy
2026-06-04 11:04:57 +02:00
parent 51abc4b7a8
commit 24ce250176
4 changed files with 15 additions and 3 deletions
+3
View File
@@ -4,6 +4,9 @@ All notable changes to Bambuddy will be documented in this file.
## [0.2.5b1] - Unreleased
### Fixed
- **Label printing produced two identical PDFs per click (#1628)** — `LabelTemplatePickerModal.tsx::openBlobInNewTab` called `window.open(url, '_blank', 'noopener,noreferrer')` and treated a `null` return as "popup blocked → fall back to `<a download>` click." Per the WindowFeatures spec, `noopener` deliberately forces `window.open` to return `null` even on success, so the `if (!win)` fallback fired on EVERY click. Path 1 (window.open) opened the blob tab — on Linux Chromium without an inline PDF viewer the OS saved a random-named copy (the `zo70GhSL.pdf` / `f7w0OcDi.pdf` files in the reporter's screenshot). Path 2 (fallback) downloaded a second copy named `bambuddy-labels.pdf`. Two identical PDFs per click. Fix: drop `noopener,noreferrer`. The blob is same-origin (created via `URL.createObjectURL` from our own fetch response), the destination is a passive PDF preview tab with no script context to abuse `window.opener`, and `noreferrer` is a no-op for blob URLs. After removal, `window.open` returns a real window reference on success → `if (!win)` only fires on genuine popup-block, single PDF per click. Existing 17 vitest cases in `LabelTemplatePickerModal.test.tsx` still pass; the change is comment + one parameter.
### Changed
- **AMS drying now enabled for H2C starting at firmware 01.02.00.00** — H2C was previously in `_DRYING_UNSUPPORTED_MODELS` alongside the A1 family. Moved to `_DRYING_MIN_FIRMWARE` with the same `01.02.00.00` floor as H2S / P2S. Both SSDP model codes the H2C advertises (`O1C`, `O1C2` — single- and dual-nozzle variants) get the same firmware gate so the `supports_drying()` check fires correctly regardless of which form is in the printer record. Test coverage extended in `TestSupportsDrying`: H2C / O1C / O1C2 cases added to the with-firmware pass set, the old-firmware fail set, and removed from the unsupported-models loop.
@@ -73,7 +73,16 @@ const TEMPLATE_OPTIONS: TemplateOption[] = [
function openBlobInNewTab(blob: Blob): void {
const url = window.URL.createObjectURL(blob);
const win = window.open(url, '_blank', 'noopener,noreferrer');
// Do NOT pass `noopener,noreferrer`: per the WindowFeatures spec, `noopener`
// forces window.open to return `null` even on success, which made the
// `if (!win)` popup-block fallback below fire on EVERY click — so the blob
// tab opened (downloading a random-named PDF on systems without an inline
// viewer) AND the `<a download>` fallback fired (downloading a second copy
// named bambuddy-labels.pdf). Two identical PDFs per click — issue #1628.
// The blob is same-origin, the destination is a passive PDF tab with no
// script context, and `noreferrer` is a no-op for blob URLs, so dropping
// these flags has no security impact.
const win = window.open(url, '_blank');
if (!win) {
const a = document.createElement('a');
a.href = url;
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -26,7 +26,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-B_QEe6y0.js"></script>
<script type="module" crossorigin src="/assets/index-CYiRVzBv.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C3FyyVE7.css">
</head>
<body>