Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4326ea68d9 | ||
|
|
4248bc791a | ||
|
|
ac6d57562b | ||
|
|
d0a2feff20 |
No files matched your search
@@ -28,13 +28,16 @@ jobs:
|
|||||||
# shared runner; the vendor stage downloads about 60 MB from npm.
|
# shared runner; the vendor stage downloads about 60 MB from npm.
|
||||||
- name: build the image
|
- name: build the image
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
run: docker build --build-arg VERSION="ci-${GITHUB_SHA::8}" -t bau-pm-pdf-tools:ci .
|
# --pull, as in the release workflow (bau-pm BAU-191): both bases are
|
||||||
|
# floating tags, and a gate that runs against a months-old cached base
|
||||||
|
# is not testing what the release builds.
|
||||||
|
run: docker build --pull --build-arg VERSION="ci-${GITHUB_SHA::8}" -t bau-pm-pdf-tools:ci .
|
||||||
|
|
||||||
- name: build the gate
|
- name: build the gate
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
run: docker build -f test/Dockerfile -t bau-pm-pdf-tools-gate:ci .
|
run: docker build -f test/Dockerfile -t bau-pm-pdf-tools-gate:ci .
|
||||||
|
|
||||||
- name: gate - nothing leaves the origin
|
- name: gate - nothing leaves the origin, no signature is kept
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
run: |
|
run: |
|
||||||
net="bau-pm-pdf-tools-ci-${GITHUB_RUN_ID}"
|
net="bau-pm-pdf-tools-ci-${GITHUB_RUN_ID}"
|
||||||
@@ -47,3 +50,5 @@ jobs:
|
|||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
docker run --rm --network "$net" bau-pm-pdf-tools-gate:ci node test/zero-hosts.mjs http://pdf-tools:8080
|
docker run --rm --network "$net" bau-pm-pdf-tools-gate:ci node test/zero-hosts.mjs http://pdf-tools:8080
|
||||||
|
# bau-pm §2.77: the signing tool keeps no signature on the device.
|
||||||
|
docker run --rm --network "$net" bau-pm-pdf-tools-gate:ci node test/signatures.mjs http://pdf-tools:8080
|
||||||
@@ -42,11 +42,16 @@ jobs:
|
|||||||
|
|
||||||
- name: build the image
|
- name: build the image
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
|
# --pull (bau-pm BAU-191, review finding 41). Both base images are
|
||||||
|
# floating tags - node:20-alpine and nginx-unprivileged:alpine-slim -
|
||||||
|
# so the nginx and OS packages that ship are whatever the tag pointed
|
||||||
|
# at when the runner last pulled it. v0.1.0 went out with nginx 1.31.6
|
||||||
|
# while the tag had moved to 1.31.8. This is the build people run.
|
||||||
run: |
|
run: |
|
||||||
docker build --build-arg VERSION="v$VERSION" \
|
docker build --pull --build-arg VERSION="v$VERSION" \
|
||||||
-t "$REGISTRY/$IMAGE:$VERSION" -t "$REGISTRY/$IMAGE:latest" .
|
-t "$REGISTRY/$IMAGE:$VERSION" -t "$REGISTRY/$IMAGE:latest" .
|
||||||
|
|
||||||
- name: gate - nothing leaves the origin
|
- name: gate - nothing leaves the origin, no signature is kept
|
||||||
timeout-minutes: 50
|
timeout-minutes: 50
|
||||||
run: |
|
run: |
|
||||||
docker build -f test/Dockerfile -t bau-pm-pdf-tools-gate:release .
|
docker build -f test/Dockerfile -t bau-pm-pdf-tools-gate:release .
|
||||||
@@ -60,6 +65,8 @@ jobs:
|
|||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
docker run --rm --network "$net" bau-pm-pdf-tools-gate:release node test/zero-hosts.mjs http://pdf-tools:8080
|
docker run --rm --network "$net" bau-pm-pdf-tools-gate:release node test/zero-hosts.mjs http://pdf-tools:8080
|
||||||
|
# bau-pm §2.77: the signing tool keeps no signature on the device.
|
||||||
|
docker run --rm --network "$net" bau-pm-pdf-tools-gate:release node test/signatures.mjs http://pdf-tools:8080
|
||||||
|
|
||||||
- name: push
|
- name: push
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -4,6 +4,34 @@ All notable changes to this project are documented in this file. The format is b
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.1.2] - 2026-11-01
|
||||||
|
|
||||||
|
The signing tool keeps no signature on the device. PDF.js's signature dialog saved every signature in the browser, ticked by default, for the PDF tools address rather than for a bau-pm user, so the next person on a shared tablet or site office PC was offered the previous one's. Signing now means typing or drawing the signature each time, and the image tab is gone (issue #1, bau-pm BAU-260).
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- The signing tool keeps no signature on the device (issue #1, bau-pm BAU-260). PDF.js's signature dialog saved every signature in the browser's `localStorage`, with *Unterschrift speichern* ticked by default, for this address rather than for a bau-pm user, so the next person on a shared tablet or site office PC was offered the previous one's signature, typed, drawn or traced from an uploaded image, and logging out of bau-pm did not clear it. Every page now deletes that store and refuses its writes, and the dialog's save box is held unticked and hidden. Signatures a device saved before are deleted on its first visit after the update.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- The *Bild* tab of the signing dialog, and its file picker: a scanned signature no longer enters the tool. Signing means typing or drawing it.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- A second gate, `test/signatures.mjs`, in CI and in the release workflow: nothing saved before is offered, nothing signed is stored, a fresh page is offered nothing, and the dialog shows neither the save box nor the image tab. Fed v0.1.0 it fails with 9 findings.
|
||||||
|
|
||||||
|
## [0.1.1] - 2026-10-24
|
||||||
|
|
||||||
|
The same BentoPDF on current packages. Nothing in this repository's own code changed; what changed is that a build no longer ships whatever base image the runner happened to have cached, so this image carries nginx 1.31.8 where v0.1.0 carried 1.31.6. Out of bau-pm's v0.22.3 security review.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- The CI and release builds pull their base images fresh (`docker build --pull`). Both bases are floating tags, so what shipped was whatever the runner had cached: v0.1.0 went out with nginx 1.31.6 while the tag had already moved to 1.31.8, past five October CVEs. None of the five is reachable in a static file server configured like this one, which is why this is the fix rather than a pin. From bau-pm's v0.22.3 security review, finding 41 (bau-pm BAU-191). **A release is what applies it** - the running image does not change until one is cut.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The image is built on the current `node:20-alpine` and `nginx-unprivileged:alpine-slim`, which is where the operating-system packages under BentoPDF come from. BentoPDF itself is unchanged: still v2.8.8 at the same pinned commit, the same vendored modules at the same integrity hashes, the same two tools disabled.
|
||||||
|
|
||||||
## [0.1.0] - 2026-10-10
|
## [0.1.0] - 2026-10-10
|
||||||
|
|
||||||
The first release: BentoPDF v2.8.8 for bau-pm, about 120 PDF tools that run in the browser and talk to no third party, with the hand-off that brings a PDF from bau-pm into a tool and the result back.
|
The first release: BentoPDF v2.8.8 for bau-pm, about 120 PDF tools that run in the browser and talk to no third party, with the hand-off that brings a PDF from bau-pm into a tool and the result back.
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ This repository is public on purpose. BentoPDF is AGPL-3.0, and this is a modifi
|
|||||||
- **Every module BentoPDF would otherwise load from a CDN**, served from `/wasm/`: PyMuPDF and Ghostscript from BentoPDF's own repository at that commit, CoherentPDF, Tesseract with `deu`, `fra`, `ita` and `eng`, the OCR text-layer font and the PDF editor's Latin, Arabic and Hebrew fallback fonts from npm. `scripts/vendor.sh` packs each one and refuses the build when the registry's integrity differs from `vendor.lock`. The editor's CJK fonts (30 to 42 MB each) are left out; a document that needs them asks this server and gets a 404, never a third party.
|
- **Every module BentoPDF would otherwise load from a CDN**, served from `/wasm/`: PyMuPDF and Ghostscript from BentoPDF's own repository at that commit, CoherentPDF, Tesseract with `deu`, `fra`, `ita` and `eng`, the OCR text-layer font and the PDF editor's Latin, Arabic and Hebrew fallback fonts from npm. `scripts/vendor.sh` packs each one and refuses the build when the registry's integrity differs from `vendor.lock`. The editor's CJK fonts (30 to 42 MB each) are left out; a document that needs them asks this server and gets a 404, never a third party.
|
||||||
- **A Content-Security-Policy without a single host**: BentoPDF's generated header file with every `http(s):` source taken out, `img-src` included (`scripts/finish.sh`, which fails the build if any host survives).
|
- **A Content-Security-Policy without a single host**: BentoPDF's generated header file with every `http(s):` source taken out, `img-src` included (`scripts/finish.sh`, which fails the build if any host survives).
|
||||||
- **Two tools disabled** in `config/config.json`, because they cannot work without a third party: `digital-sign-pdf` (X.509 signing fetches certificate chains through a CORS proxy) and `timestamp-pdf` (an RFC 3161 timestamp needs an external timestamp authority).
|
- **Two tools disabled** in `config/config.json`, because they cannot work without a third party: `digital-sign-pdf` (X.509 signing fetches certificate chains through a CORS proxy) and `timestamp-pdf` (an RFC 3161 timestamp needs an external timestamp authority).
|
||||||
- **The glue**, `glue/handoff.js`, added to every HTML page by `scripts/finish.sh`: the source link, the hand-in of a file from bau-pm, the hand-back of a result, and the signing tool's flatten switch locked on. See the file's header for how each works.
|
- **The glue**, `glue/handoff.js`, added to every HTML page by `scripts/finish.sh`: the source link, the hand-in of a file from bau-pm, the hand-back of a result, the signing tool's flatten switch locked on, and no signature kept on the device. See the file's header for how each works.
|
||||||
|
- **No saved signatures, no image signatures.** PDF.js's signature dialog would save up to five signatures in the browser's `localStorage`, with *Unterschrift speichern* ticked by default, for this origin rather than for a bau-pm user: on a shared tablet the next person found the last one's signature one click away, a logout in bau-pm included (issue #1). The glue deletes the store on every page, refuses its writes, holds the box unticked and hides it, and hides the dialog's *Bild* tab, so a scanned signature never enters the tool. Signing means typing or drawing it, each time.
|
||||||
|
|
||||||
Nothing of BentoPDF's code is edited: its documented build arguments, its runtime `config.json`, its generated header file with fewer hosts, and our files beside its own.
|
Nothing of BentoPDF's code is edited: its documented build arguments, its runtime `config.json`, its generated header file with fewer hosts, and our files beside its own.
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ bau-pm runs the image as its `pdf-tools` service on an origin of its own (`PDF_T
|
|||||||
|
|
||||||
It has teeth: fed the stock upstream image it fails with 115 findings (PyMuPDF, Pyodide and CoherentPDF from jsdelivr), and fed a misspelled disabled id it fails with "no page".
|
It has teeth: fed the stock upstream image it fails with 115 findings (PyMuPDF, Pyodide and CoherentPDF from jsdelivr), and fed a misspelled disabled id it fails with "no page".
|
||||||
|
|
||||||
|
`test/signatures.mjs` is the second gate, run in the same step: on a browser that already holds a saved signature (a real entry, `test/fixtures/saved-signature.json`) the signing tool offers none and the store ends up empty; in its dialog the save box and the image tab are hidden; a typed signature, once placed, is not stored; and a fresh page, the next person, is offered nothing. Fed v0.1.0 it fails with 9 findings.
|
||||||
|
|
||||||
Locally (bridged containers need no internet for it):
|
Locally (bridged containers need no internet for it):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -32,6 +35,7 @@ docker build -f test/Dockerfile -t bau-pm-pdf-tools-gate:ci .
|
|||||||
docker network create pdf-gate
|
docker network create pdf-gate
|
||||||
docker run -d --name pdf-gate-app --network pdf-gate --network-alias pdf-tools bau-pm-pdf-tools:ci
|
docker run -d --name pdf-gate-app --network pdf-gate --network-alias pdf-tools bau-pm-pdf-tools:ci
|
||||||
docker run --rm --network pdf-gate bau-pm-pdf-tools-gate:ci node test/zero-hosts.mjs http://pdf-tools:8080
|
docker run --rm --network pdf-gate bau-pm-pdf-tools-gate:ci node test/zero-hosts.mjs http://pdf-tools:8080
|
||||||
|
docker run --rm --network pdf-gate bau-pm-pdf-tools-gate:ci node test/signatures.mjs http://pdf-tools:8080
|
||||||
docker rm -f -v pdf-gate-app && docker network rm pdf-gate
|
docker rm -f -v pdf-gate-app && docker network rm pdf-gate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+51
-1
@@ -2,7 +2,7 @@
|
|||||||
*
|
*
|
||||||
* Loaded by every page of this image, the PDF.js viewers' pages inside the
|
* Loaded by every page of this image, the PDF.js viewers' pages inside the
|
||||||
* tools included, as a classic script in <head>, so it runs before BentoPDF's
|
* tools included, as a classic script in <head>, so it runs before BentoPDF's
|
||||||
* own modules. It does four things:
|
* own modules. It does five things:
|
||||||
*
|
*
|
||||||
* 1. A link to this image's source on every page, because the AGPL asks the
|
* 1. A link to this image's source on every page, because the AGPL asks the
|
||||||
* source of a modified version served over a network to be offered
|
* source of a modified version served over a network to be offered
|
||||||
@@ -20,6 +20,14 @@
|
|||||||
* blob is read inside click(), because the next line revokes its URL.
|
* blob is read inside click(), because the next line revokes its URL.
|
||||||
* 4. For the signing tool, BentoPDF's "flatten" switched on and locked, so a
|
* 4. For the signing tool, BentoPDF's "flatten" switched on and locked, so a
|
||||||
* signature becomes part of the page and cannot be moved later.
|
* signature becomes part of the page and cannot be moved later.
|
||||||
|
* 5. No signature kept on the device. PDF.js's signature editor saves up to
|
||||||
|
* five signatures in localStorage, ticked by default, for this origin and
|
||||||
|
* not for a bau-pm user, so the next person on a shared tablet would find
|
||||||
|
* the last one's signature one click away, a Redmine logout included
|
||||||
|
* (bau-pm PLAN.md §2.77, Gitea #1). Every window deletes what is stored,
|
||||||
|
* refuses the store's writes, and in the viewer the "save" box is held off
|
||||||
|
* and hidden. The dialog's image tab is hidden too, so a scanned
|
||||||
|
* signature never enters the tool: signing means drawing or typing it.
|
||||||
*
|
*
|
||||||
* Nothing here talks to anything but this origin. The words of the hand-off
|
* Nothing here talks to anything but this origin. The words of the hand-off
|
||||||
* come from bau-pm in the user's language; only the source link has its own.
|
* come from bau-pm in the user's language; only the source link has its own.
|
||||||
@@ -31,6 +39,7 @@
|
|||||||
var COOKIE = '__Host-bau_pm_handoff';
|
var COOKIE = '__Host-bau_pm_handoff';
|
||||||
var API = '/bau-pm/handoff';
|
var API = '/bau-pm/handoff';
|
||||||
var SOURCE_URL = '__BAU_PM_SOURCE_URL__';
|
var SOURCE_URL = '__BAU_PM_SOURCE_URL__';
|
||||||
|
var SIGNATURES = 'pdfjs.signature';
|
||||||
var isTop = window.top === window;
|
var isTop = window.top === window;
|
||||||
|
|
||||||
// --- shared state --------------------------------------------------------
|
// --- shared state --------------------------------------------------------
|
||||||
@@ -72,6 +81,9 @@
|
|||||||
return nativeClick.apply(this, arguments);
|
return nativeClick.apply(this, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
forgetSignatures();
|
||||||
|
ready(signatureDialog);
|
||||||
|
|
||||||
if (!isTop) {
|
if (!isTop) {
|
||||||
signFrame();
|
signFrame();
|
||||||
return;
|
return;
|
||||||
@@ -294,6 +306,44 @@
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 5. no signature kept on the device ---------------------------------
|
||||||
|
|
||||||
|
// Runs before PDF.js's modules in every window, so the viewer reads an empty
|
||||||
|
// store, and a device that saved signatures before this glue loses them on
|
||||||
|
// its first visit. The write is refused rather than undone afterwards: a
|
||||||
|
// signature that is never stored cannot be read by the next page either.
|
||||||
|
function forgetSignatures() {
|
||||||
|
try {
|
||||||
|
window.localStorage.removeItem(SIGNATURES);
|
||||||
|
var nativeSetItem = Storage.prototype.setItem;
|
||||||
|
Storage.prototype.setItem = function (key) {
|
||||||
|
if (String(key) === SIGNATURES) { return undefined; }
|
||||||
|
return nativeSetItem.apply(this, arguments);
|
||||||
|
};
|
||||||
|
} catch (e) { /* storage blocked: then nothing can be kept either */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The dialog sets the box to "save" each time it opens; a property of the
|
||||||
|
// element's own shadows the prototype's, so it reads unticked whatever is
|
||||||
|
// assigned to it. Only PDF.js's viewer has these elements.
|
||||||
|
function signatureDialog() {
|
||||||
|
var save = document.getElementById('addSignatureSaveCheckbox');
|
||||||
|
if (!save) { return; }
|
||||||
|
Object.defineProperty(save, 'checked', {
|
||||||
|
configurable: true,
|
||||||
|
get: function () { return false; },
|
||||||
|
set: function () {}
|
||||||
|
});
|
||||||
|
hide(document.getElementById('addSignatureSaveContainer'));
|
||||||
|
hide(document.getElementById('addSignatureImageButton'));
|
||||||
|
var picker = document.getElementById('addSignatureFilePicker');
|
||||||
|
if (picker) { picker.disabled = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide(element) {
|
||||||
|
if (element) { element.style.setProperty('display', 'none', 'important'); }
|
||||||
|
}
|
||||||
|
|
||||||
// --- 3. the hand-back's sheet --------------------------------------------
|
// --- 3. the hand-back's sheet --------------------------------------------
|
||||||
|
|
||||||
function button(label, kind) {
|
function button(label, kind) {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# The gate's own image (test/zero-hosts.mjs): Chromium and playwright-core,
|
# The gates' own image (test/zero-hosts.mjs, test/signatures.mjs): Chromium and playwright-core,
|
||||||
# with the test files and the glue baked in, because the CI runner passes the
|
# with the test files and the glue baked in, because the CI runner passes the
|
||||||
# host's Docker socket through and a mounted workspace would be empty.
|
# host's Docker socket through and a mounted workspace would be empty.
|
||||||
FROM public.ecr.aws/docker/library/node:20-alpine
|
FROM public.ecr.aws/docker/library/node:20-alpine
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"description":"Vorher gespeichert","signatureData":"pZBLq1xFFIVXnXqeZz9v7o3GoE4EHSpB1KBCBEEQBXGi/8CxSgb6BxzpVCciOBbEiQNnkkSIkBglCl5R4315+3b3eVbVOafKdLpN5rqh4NuLWrX2rq857lQYA58S3K0zAFbtcwBeYsAnAJ4BcFGt+VEAb1DgMwBDAL9J4HMATwC4EgJfAHgFwCUBXNnohQCubt55UAHXNrpkwPUNv8jv8Tvhmh8G8BEFfgDwJIDv2JqfAnBe3ON35ZrP3eZGrXmV9RMHfgSwA+CqXDMDoAWwu+FXQ+CP2znj2zNcZsBfAM4C+IWveaU/Lda82vdteU+v1JoFgAsM2N94P6DAweYvh3LNlnbCTXbfFzxumYl/PnUQ2qznHKGddNv+DKUKSgd74Q3MTWNcgRO5iNNr9MTbrm/RBla5ye57ggsv2sH103txG3rGEJudfuzvoyz2oiFH0be21H1+x519HxRe636ZffOWW/pDf5PstloTyx0nr7PHuVII9QsfTvaJ803vDBrWCSjJo15or5tulWpPHztDOwnB2XYwe5lIQVUv6tFX0k3Be9Z0ZukLwpek7K3117NlaMI2JTFlit2vhITSKqczV2kdWEnCRCV0imEX27DmBYqusbZu97VqVLdjKIGiLPLCBAdtZ9GINq7PHb0GxrgEc9wk11KaYmIn82SfLnDidKtLm2M/8i7oYHRX8KNpMW5O+bEcpGFGEz+2YSubTlscRSXqTpekEvnWXmQjhDSSkSHGmpkpMBPLyHliXNt4yw/ehKRUQrbZ5cFJbKYtId2I8IiIltVB7cu2Xf4nz5IdJ77uO42O6p35BS9X+0mbXUryYT3qCOsjqCBQ4L3IyczaHHO2iIqs2uqGZBoNUzH1AzOax8d8huO+LquFm4t5ttw2Wz4iSqaDMAmmfmIzLTRpXWfccVrSBcpW18/eykpqSWXrpf0bBatFkRZbduzHPBnEGZ36UZuZsPJG9/MkZ3MU1mo46s7vZUVgUZkmb/+Pcy6WST5pdvyUx0kUUkWm/pSJrKq8XTmXbIGmr8rn/0xLqtHYZjXpoToeFlt2m6Qs5CLmGYn7UU6omepskZrgBEXfkI75ULAweADbOi3j2UMfz4s9b2TxWPMIJGccym59GWhveqcRwN05qc/UrcHvqfNBF+ZJGXfKx6I6a5My1aJitbPWL2gl86hKdepW9w/TXwfOkz6eK53au4pLZv/2s/jmyDumZR3VSUs8IQ6hD2WQ9kObNVEt6qDp27Zvgjw5TCvlpFcikD7rBjauQ01zVL3voxtZnXX/AA=="}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// The signing tool keeps no signature on the device (bau-pm PLAN.md §2.77,
|
||||||
|
// Gitea #1): PDF.js's signature editor saves signatures in localStorage for
|
||||||
|
// this origin, not for a bau-pm user, so on a shared tablet the next person
|
||||||
|
// would find the last one's signature one click away.
|
||||||
|
//
|
||||||
|
// node test/signatures.mjs <base-url> [chromium-path]
|
||||||
|
//
|
||||||
|
// 1. A device that already holds a saved signature (written the way an older
|
||||||
|
// image left it, from a page the glue does not run on) is offered none,
|
||||||
|
// and the store is empty afterwards.
|
||||||
|
// 2. In the signing dialog the "save" box and the image tab are hidden, and a
|
||||||
|
// signature typed and added with the dialog as it opens is not stored.
|
||||||
|
// 3. A fresh page on the same browser, the next person, is offered none.
|
||||||
|
//
|
||||||
|
// Exits 0 when nothing was kept, 1 otherwise, 2 when it could not run. Fed an
|
||||||
|
// image without the glue's fifth duty (v0.1.1 or older) it must fail.
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const base = (process.argv[2] || '').replace(/\/$/, '');
|
||||||
|
const executablePath = process.argv[3] || process.env.CHROMIUM || '/usr/bin/chromium';
|
||||||
|
if (!base) {
|
||||||
|
console.error('usage: signatures.mjs <base-url> [chromium]');
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { chromium } = await import(process.env.PLAYWRIGHT_CORE || 'playwright-core');
|
||||||
|
const KEY = 'pdfjs.signature';
|
||||||
|
const failures = [];
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
executablePath,
|
||||||
|
args: ['--no-sandbox', `--unsafely-treat-insecure-origin-as-secure=${new URL(base).origin}`],
|
||||||
|
});
|
||||||
|
// Wide enough for the viewer's toolbar to show the signature button.
|
||||||
|
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||||
|
|
||||||
|
async function viewer(page) {
|
||||||
|
await page.goto(`${base}/de/sign-pdf`, { waitUntil: 'networkidle' });
|
||||||
|
await page.setInputFiles('#file-input', join(here, 'fixtures/probe.pdf'));
|
||||||
|
const frame = await (await page.waitForSelector('iframe', { timeout: 60000 })).contentFrame();
|
||||||
|
await frame.waitForSelector('#editorSignatureButton:not([disabled])', { timeout: 60000 });
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stored = (frame) => frame.evaluate((key) => window.localStorage.getItem(key), KEY);
|
||||||
|
|
||||||
|
// The saved signatures the toolbar's menu offers, read from the menu itself.
|
||||||
|
async function offered(frame) {
|
||||||
|
await frame.click('#editorSignatureButton');
|
||||||
|
await frame.waitForSelector('#editorSignatureAddSignature', { state: 'visible', timeout: 10000 });
|
||||||
|
const titles = await frame.$$eval('#addSignatureDoorHanger .toolbarAddSignatureButtonContainer', (rows) =>
|
||||||
|
rows.map((row) => row.querySelector('button')?.getAttribute('title') || row.textContent.trim()));
|
||||||
|
await frame.click('#editorSignatureButton');
|
||||||
|
return titles;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shown = (frame, selector) => frame.$eval(selector, (e) => e.getClientRects().length > 0 && getComputedStyle(e).display !== 'none');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// --- 1. a device that saved one before --------------------------------------
|
||||||
|
// config.json is served without the glue, so this write is the browser's own.
|
||||||
|
// The entry is a real one, typed in v0.1.0's dialog and read back from its
|
||||||
|
// store: a made-up entry might simply fail to render in the menu, and the
|
||||||
|
// check would pass on an image that still offers saved signatures.
|
||||||
|
const seed = await context.newPage();
|
||||||
|
await seed.goto(`${base}/config.json`);
|
||||||
|
await seed.evaluate(([key, entry]) => window.localStorage.setItem(key, JSON.stringify({
|
||||||
|
'00000000-0000-4000-8000-000000000000': JSON.parse(entry),
|
||||||
|
})), [KEY, readFileSync(join(here, 'fixtures/saved-signature.json'), 'utf8')]);
|
||||||
|
await seed.close();
|
||||||
|
|
||||||
|
const a = await context.newPage();
|
||||||
|
let frame = await viewer(a);
|
||||||
|
const before = await offered(frame);
|
||||||
|
if (before.length) failures.push(`a signature saved before is still offered: ${before.join(', ')}`);
|
||||||
|
if (await stored(frame)) failures.push('a signature saved before is still stored');
|
||||||
|
|
||||||
|
// --- 2. signing --------------------------------------------------------------
|
||||||
|
// The toolbar button switches the signature mode on; the dialog's controls
|
||||||
|
// are clicked through the DOM, because the dialog is taller than the frame.
|
||||||
|
await frame.click('#editorSignatureButton');
|
||||||
|
await frame.$eval('#editorSignatureAddSignature', (e) => e.click());
|
||||||
|
await frame.waitForSelector('#addSignatureDialog[open]', { timeout: 10000 });
|
||||||
|
if (await shown(frame, '#addSignatureSaveContainer')) failures.push('the dialog offers "save signature"');
|
||||||
|
if (await shown(frame, '#addSignatureImageButton')) failures.push('the dialog offers the image tab');
|
||||||
|
if (!(await frame.$eval('#addSignatureFilePicker', (e) => e.disabled))) failures.push('the image file picker is enabled');
|
||||||
|
if (await frame.$eval('#addSignatureSaveCheckbox', (e) => e.checked)) failures.push('the "save" box reads ticked');
|
||||||
|
await frame.$eval('#addSignatureTypeInput', (e) => {
|
||||||
|
e.value = 'Max Muster';
|
||||||
|
e.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
});
|
||||||
|
await frame.waitForSelector('#addSignatureAddButton:not([disabled])', { state: 'attached', timeout: 10000 });
|
||||||
|
await frame.$eval('#addSignatureAddButton', (e) => e.click());
|
||||||
|
await frame.waitForSelector('#addSignatureDialog[open]', { state: 'detached', timeout: 10000 }).catch(() => {});
|
||||||
|
const placed = await frame.$$eval('.signatureEditor', (e) => e.length);
|
||||||
|
if (placed === 0) failures.push('the typed signature was not placed (the check below would prove nothing)');
|
||||||
|
if (await stored(frame)) failures.push('the signature just added was stored');
|
||||||
|
await a.close();
|
||||||
|
|
||||||
|
// --- 3. the next person ------------------------------------------------------
|
||||||
|
const b = await context.newPage();
|
||||||
|
frame = await viewer(b);
|
||||||
|
const after = await offered(frame);
|
||||||
|
if (after.length) failures.push(`the next page is offered: ${after.join(', ')}`);
|
||||||
|
if (await stored(frame)) failures.push('the next page finds a stored signature');
|
||||||
|
await b.close();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`signatures: could not run: ${e.message}`);
|
||||||
|
await browser.close();
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
if (failures.length) {
|
||||||
|
console.error(`signatures: ${failures.length} finding(s)`);
|
||||||
|
for (const f of failures) console.error(` - ${f}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('signatures: nothing kept, nothing offered, no image tab');
|
||||||
Reference in new issue
Block a user