| 1 | // Geen uitvoerbaar script in inhoud die htmx wisselt (shaer-0i6).
|
|---|
| 2 | //
|
|---|
| 3 | // De CSP-nonce rouleert per verzoek. Een pagina die je via een link BINNEN de
|
|---|
| 4 | // site opent komt als fragment binnen, met de nonce van dat verzoek -- en die
|
|---|
| 5 | // kent het al geladen document niet, dus wordt zo'n script geweigerd. Elk
|
|---|
| 6 | // paginascript hoort daarom uit de shell te komen, die op body[data-js] een
|
|---|
| 7 | // module importeert; de gegevens gaan via partials/page-data.ejs.
|
|---|
| 8 | //
|
|---|
| 9 | // De valkuil die dit bewaakt: de fout is ONZICHTBAAR na een herlading. Wie een
|
|---|
| 10 | // script terugzet in een view merkt daar bij het testen niets van, tenzij hij
|
|---|
| 11 | // er via de navigatie naartoe gaat. Deze toets merkt het wel.
|
|---|
| 12 | import { test } from 'node:test';
|
|---|
| 13 | import assert from 'node:assert/strict';
|
|---|
| 14 | import fs from 'node:fs';
|
|---|
| 15 | import path from 'node:path';
|
|---|
| 16 | import { fileURLToPath } from 'node:url';
|
|---|
| 17 |
|
|---|
| 18 | const VIEWS = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'views');
|
|---|
| 19 |
|
|---|
| 20 | // Losse DOCUMENTEN, geen fragmenten: deze gaan via res.render en niet via
|
|---|
| 21 | // renderPage, dus htmx wisselt ze nooit in en hun scripts komen altijd met de
|
|---|
| 22 | // nonce van hun eigen laadbeurt binnen. Wie hier iets aan toevoegt moet dus
|
|---|
| 23 | // eerst nagaan langs welke weg de pagina gerenderd wordt.
|
|---|
| 24 | const LOSSE_DOCUMENTEN = new Set(['pages/embed-player.ejs', 'pages/guardian.ejs']);
|
|---|
| 25 |
|
|---|
| 26 | function views() {
|
|---|
| 27 | const uit = [];
|
|---|
| 28 | for (const map of ['pages', 'partials']) {
|
|---|
| 29 | for (const naam of fs.readdirSync(path.join(VIEWS, map))) {
|
|---|
| 30 | if (naam.endsWith('.ejs')) uit.push(`${map}/${naam}`);
|
|---|
| 31 | }
|
|---|
| 32 | }
|
|---|
| 33 | return uit.sort();
|
|---|
| 34 | }
|
|---|
| 35 |
|
|---|
| 36 | test('geen uitvoerbaar script in een view die als fragment binnenkomt', () => {
|
|---|
| 37 | const overtreders = [];
|
|---|
| 38 | for (const rel of views()) {
|
|---|
| 39 | if (LOSSE_DOCUMENTEN.has(rel)) continue;
|
|---|
| 40 | // EJS-commentaar eerst weg: dat komt nooit in de uitvoer, en de views
|
|---|
| 41 | // leggen juist in zo'n commentaar uit welke scripttag daar wegging. Zonder
|
|---|
| 42 | // dit sloeg deze toets aan op zijn eigen uitleg.
|
|---|
| 43 | const bron = fs.readFileSync(path.join(VIEWS, rel), 'utf8').replace(/<%#[\s\S]*?%>/g, '');
|
|---|
| 44 | for (const m of bron.matchAll(/<script\b[^>]*>/gi)) {
|
|---|
| 45 | // Een datablok (application/json) wordt door de browser nooit
|
|---|
| 46 | // uitgevoerd, dus daar gaat script-src niet over. Dat is de vorm
|
|---|
| 47 | // waarin een pagina zijn gegevens wel mag meegeven.
|
|---|
| 48 | if (/type\s*=\s*["']application\/json["']/i.test(m[0])) continue;
|
|---|
| 49 | overtreders.push(`${rel}: ${m[0].slice(0, 80)}`);
|
|---|
| 50 | }
|
|---|
| 51 | }
|
|---|
| 52 | assert.deepEqual(overtreders, [],
|
|---|
| 53 | 'deze scripts overleven een htmx-navigatie niet; laat de shell de module laden (zie shaer-0i6)');
|
|---|
| 54 | });
|
|---|
| 55 |
|
|---|
| 56 | test('de losse documenten bestaan nog, anders bewaakt de uitzondering niets', () => {
|
|---|
| 57 | // Een uitzonderingslijst die naar verdwenen bestanden wijst maakt de toets
|
|---|
| 58 | // stilletjes zwakker: hernoemt iemand zo'n pagina, dan valt hij ineens onder
|
|---|
| 59 | // de regel zonder dat iemand dat besloot.
|
|---|
| 60 | for (const rel of LOSSE_DOCUMENTEN) {
|
|---|
| 61 | assert.ok(fs.existsSync(path.join(VIEWS, rel)), `${rel} staat op de uitzonderingslijst maar bestaat niet meer`);
|
|---|
| 62 | }
|
|---|
| 63 | });
|
|---|