javascript-solid-server 0.0.160 → 0.0.161
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/handlers/resource.js +39 -5
- package/src/mashlib/index.js +85 -6
- package/test/data-island.test.js +209 -0
package/package.json
CHANGED
package/src/handlers/resource.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from '../rdf/conneg.js';
|
|
15
15
|
import { emitChange } from '../notifications/events.js';
|
|
16
16
|
import { checkIfMatch, checkIfNoneMatchForGet, checkIfNoneMatchForWrite } from '../utils/conditional.js';
|
|
17
|
-
import { generateDatabrowserHtml, generateModuleDatabrowserHtml, shouldServeMashlib } from '../mashlib/index.js';
|
|
17
|
+
import { generateDatabrowserHtml, generateModuleDatabrowserHtml, shouldServeMashlib, DATA_ISLAND_MAX_BYTES } from '../mashlib/index.js';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Live reload script - injected into HTML when --live-reload is enabled
|
|
@@ -238,9 +238,21 @@ export async function handleGet(request, reply) {
|
|
|
238
238
|
|
|
239
239
|
// Check if we should serve Mashlib data browser for containers
|
|
240
240
|
if (shouldServeMashlib(request, request.mashlibEnabled, 'application/ld+json')) {
|
|
241
|
+
// Phase 1 of #7: also embed the container's JSON-LD listing as a
|
|
242
|
+
// data island so consumers that look for `<script
|
|
243
|
+
// type="application/ld+json">` (search-engine rich-results,
|
|
244
|
+
// archival crawlers, future mashlib zero-fetch path) get the data
|
|
245
|
+
// without a second request. Use compact (no-whitespace) form for
|
|
246
|
+
// the embed so we don't burn bytes against DATA_ISLAND_MAX_BYTES
|
|
247
|
+
// on indentation that nothing will ever read.
|
|
248
|
+
const embedJsonLd = JSON.stringify(jsonLd);
|
|
241
249
|
const html = request.mashlibModule
|
|
242
|
-
? generateModuleDatabrowserHtml(request.mashlibModule)
|
|
243
|
-
: generateDatabrowserHtml(
|
|
250
|
+
? generateModuleDatabrowserHtml(request.mashlibModule, resourceUrl, { embedJsonLd })
|
|
251
|
+
: generateDatabrowserHtml(
|
|
252
|
+
resourceUrl,
|
|
253
|
+
request.mashlibCdn ? request.mashlibVersion : null,
|
|
254
|
+
{ embedJsonLd }
|
|
255
|
+
);
|
|
244
256
|
const headers = getAllHeaders({
|
|
245
257
|
isContainer: true,
|
|
246
258
|
etag: stats.etag,
|
|
@@ -318,9 +330,31 @@ export async function handleGet(request, reply) {
|
|
|
318
330
|
// Check if we should serve Mashlib data browser
|
|
319
331
|
// Only for RDF resources when Accept: text/html is requested
|
|
320
332
|
if (shouldServeMashlib(request, request.mashlibEnabled, storedContentType)) {
|
|
333
|
+
// Phase 1 of #7: embed the resource's JSON-LD bytes as a data
|
|
334
|
+
// island when it's already JSON-LD (the JSS-native format). Other
|
|
335
|
+
// formats are out of Phase-1 scope; the wrapper still loads
|
|
336
|
+
// correctly and mashlib XHR-fetches as before.
|
|
337
|
+
//
|
|
338
|
+
// Cap-aware short-circuit: skip the read entirely when the file is
|
|
339
|
+
// already over the embed cap. The island would be dropped anyway,
|
|
340
|
+
// and large JSON-LD resources would otherwise load into memory on
|
|
341
|
+
// every HTML navigation.
|
|
342
|
+
let embedJsonLd;
|
|
343
|
+
if (storedContentType === 'application/ld+json' &&
|
|
344
|
+
stats.size <= DATA_ISLAND_MAX_BYTES) {
|
|
345
|
+
// dataIsland() in mashlib/index.js coerces Buffer → string itself,
|
|
346
|
+
// so we hand it the Buffer directly instead of allocating a UTF-8
|
|
347
|
+
// string copy on every navigation.
|
|
348
|
+
const buf = await storage.read(storagePath);
|
|
349
|
+
if (buf) embedJsonLd = buf;
|
|
350
|
+
}
|
|
321
351
|
const html = request.mashlibModule
|
|
322
|
-
? generateModuleDatabrowserHtml(request.mashlibModule)
|
|
323
|
-
: generateDatabrowserHtml(
|
|
352
|
+
? generateModuleDatabrowserHtml(request.mashlibModule, resourceUrl, { embedJsonLd })
|
|
353
|
+
: generateDatabrowserHtml(
|
|
354
|
+
resourceUrl,
|
|
355
|
+
request.mashlibCdn ? request.mashlibVersion : null,
|
|
356
|
+
{ embedJsonLd }
|
|
357
|
+
);
|
|
324
358
|
const headers = getAllHeaders({
|
|
325
359
|
isContainer: false,
|
|
326
360
|
etag: stats.etag,
|
package/src/mashlib/index.js
CHANGED
|
@@ -4,23 +4,97 @@
|
|
|
4
4
|
* Generates HTML wrapper that loads SolidOS Mashlib from CDN.
|
|
5
5
|
* When a browser requests an RDF resource with Accept: text/html,
|
|
6
6
|
* we return this wrapper which then fetches and renders the data.
|
|
7
|
+
*
|
|
8
|
+
* Phase 1 of #7: when the originating resource is reasonably small
|
|
9
|
+
* RDF, the JSON-LD bytes are embedded in the wrapper as a `<script
|
|
10
|
+
* type="application/ld+json" id="dataisland" data-uri="…">` block.
|
|
11
|
+
* Browsers ignore non-JS script bodies, so this is harmless to all
|
|
12
|
+
* existing clients (mashlib still XHR-fetches today). It immediately
|
|
13
|
+
* benefits anything that knows to look for `application/ld+json`
|
|
14
|
+
* islands — search engine rich-results, archival crawlers, scrapers,
|
|
15
|
+
* static-site exporters — and gives Phase 2 a zero-network fast path.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Cap on how much JSON-LD we'll inline. A 256 KB resource fits any
|
|
20
|
+
* realistic profile, type index, or container listing. Above that we
|
|
21
|
+
* drop the island and let the existing XHR path handle it so we don't
|
|
22
|
+
* make every navigation re-download a multi-megabyte resource.
|
|
23
|
+
*/
|
|
24
|
+
export const DATA_ISLAND_MAX_BYTES = 256 * 1024;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Escape a JSON-LD body for safe inclusion inside `<script
|
|
28
|
+
* type="application/ld+json">…</script>`.
|
|
29
|
+
*
|
|
30
|
+
* Browsers don't execute the script (wrong MIME), but the HTML parser
|
|
31
|
+
* still scans the body for an end-of-script tag. The relevant rule:
|
|
32
|
+
* any `</` followed by `script` (case-insensitive) terminates the
|
|
33
|
+
* element regardless of what follows — `</script>`, `</script >`,
|
|
34
|
+
* `</script\n>`, `</SCRIPT>` and friends all close it. Escaping just
|
|
35
|
+
* the literal `</script>` token is too narrow.
|
|
36
|
+
*
|
|
37
|
+
* The robust fix is to replace every literal `<` byte in the body with
|
|
38
|
+
* the JSON string-escape for U+003C — the six characters
|
|
39
|
+
* backslash-u-0-0-3-c (the same form the implementation emits below).
|
|
40
|
+
* JSON-LD is JSON, and a JSON parser decodes that escape back to a
|
|
41
|
+
* literal `<` natively, so document semantics are preserved. After
|
|
42
|
+
* this transform the body literally cannot contain a `<` byte — so no
|
|
43
|
+
* end-tag (or comment, CDATA, etc.) can possibly start.
|
|
44
|
+
*/
|
|
45
|
+
function escapeForScriptBlock(jsonLdString) {
|
|
46
|
+
return String(jsonLdString).replace(/</g, '\\u003c');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build the data-island `<script>` block for the given JSON-LD payload.
|
|
51
|
+
* Returns an empty string if the payload is missing or over the size
|
|
52
|
+
* cap so callers can unconditionally interpolate `dataIsland(...)`.
|
|
53
|
+
*
|
|
54
|
+
* The cap applies to the *escaped* body — i.e. the bytes that will
|
|
55
|
+
* actually appear in the HTTP response. `escapeForScriptBlock` can
|
|
56
|
+
* expand input up to 6x (each literal `<` becomes the 6-byte JSON
|
|
57
|
+
* escape sequence backslash-u-0-0-3-c), so checking the raw input
|
|
58
|
+
* size alone could let an HTML response balloon past the cap.
|
|
59
|
+
*
|
|
60
|
+
* Two-stage check:
|
|
61
|
+
* 1. Cheap raw-byte pre-check — escape can only grow the body,
|
|
62
|
+
* so a raw payload already over the cap is guaranteed to be
|
|
63
|
+
* over after escaping; drop without doing the work.
|
|
64
|
+
* 2. Post-escape check — catches the rare case where input was
|
|
65
|
+
* under the cap but expanded above it (`<`-heavy bodies).
|
|
7
66
|
*/
|
|
67
|
+
function dataIsland(resourceUrl, jsonLdString) {
|
|
68
|
+
if (!jsonLdString) return '';
|
|
69
|
+
const raw = String(jsonLdString);
|
|
70
|
+
if (Buffer.byteLength(raw, 'utf8') > DATA_ISLAND_MAX_BYTES) return '';
|
|
71
|
+
const safeBody = escapeForScriptBlock(raw);
|
|
72
|
+
if (Buffer.byteLength(safeBody, 'utf8') > DATA_ISLAND_MAX_BYTES) return '';
|
|
73
|
+
const safeUri = escapeHtml(String(resourceUrl));
|
|
74
|
+
return `<script type="application/ld+json" id="dataisland" data-uri="${safeUri}">${safeBody}</script>`;
|
|
75
|
+
}
|
|
8
76
|
|
|
9
77
|
/**
|
|
10
78
|
* Generate Mashlib databrowser HTML
|
|
11
79
|
*
|
|
12
|
-
* @param {string} resourceUrl - The URL of the resource being viewed
|
|
80
|
+
* @param {string} resourceUrl - The URL of the resource being viewed
|
|
13
81
|
* @param {string} cdnVersion - If provided, load mashlib from unpkg CDN (e.g., "2.0.0")
|
|
82
|
+
* @param {object} [opts]
|
|
83
|
+
* @param {string|Buffer} [opts.embedJsonLd] - JSON-LD body to inline
|
|
84
|
+
* as a `<script type="application/ld+json">` data island. Accepts a
|
|
85
|
+
* UTF-8 string or a Buffer (coerced via `String()`). Honors a 256 KB
|
|
86
|
+
* size cap; oversize payloads are silently dropped. Phase 1 of #7.
|
|
14
87
|
* @returns {string} HTML content
|
|
15
88
|
*/
|
|
16
|
-
export function generateDatabrowserHtml(resourceUrl, cdnVersion = null) {
|
|
89
|
+
export function generateDatabrowserHtml(resourceUrl, cdnVersion = null, opts = {}) {
|
|
90
|
+
const island = dataIsland(resourceUrl, opts.embedJsonLd);
|
|
17
91
|
if (cdnVersion) {
|
|
18
92
|
// CDN mode - use script.onload to ensure mashlib is fully loaded before init
|
|
19
93
|
// This avoids race conditions with defer + DOMContentLoaded
|
|
20
94
|
const cdnBase = `https://unpkg.com/mashlib@${cdnVersion}/dist`;
|
|
21
95
|
return `<!doctype html><html><head><meta charset="utf-8"/><title>SolidOS Web App</title>
|
|
22
96
|
<link href="${cdnBase}/mash.css" rel="stylesheet"></head>
|
|
23
|
-
<body id="PageBody"
|
|
97
|
+
<body id="PageBody">${island}<header id="PageHeader"></header>
|
|
24
98
|
<div class="TabulatorOutline" id="DummyUUID" role="main"><table id="outline"></table><div id="GlobalDashboard"></div></div>
|
|
25
99
|
<footer id="PageFooter"></footer>
|
|
26
100
|
<script>
|
|
@@ -37,22 +111,27 @@ export function generateDatabrowserHtml(resourceUrl, cdnVersion = null) {
|
|
|
37
111
|
// Local mode - use defer (reliable when served locally)
|
|
38
112
|
return `<!doctype html><html><head><meta charset="utf-8"/><title>SolidOS Web App</title><script>document.addEventListener('DOMContentLoaded', function() {
|
|
39
113
|
panes.runDataBrowser()
|
|
40
|
-
})</script><script defer="defer" src="/mashlib.min.js"></script><link href="/mash.css" rel="stylesheet"></head><body id="PageBody"
|
|
114
|
+
})</script><script defer="defer" src="/mashlib.min.js"></script><link href="/mash.css" rel="stylesheet"></head><body id="PageBody">${island}<header id="PageHeader"></header><div class="TabulatorOutline" id="DummyUUID" role="main"><table id="outline"></table><div id="GlobalDashboard"></div></div><footer id="PageFooter"></footer></body></html>`;
|
|
41
115
|
}
|
|
42
116
|
|
|
43
117
|
/**
|
|
44
118
|
* Generate ES module-based databrowser HTML
|
|
45
119
|
*
|
|
46
120
|
* @param {string} moduleUrl - URL to the ES module entry point
|
|
121
|
+
* @param {string} resourceUrl - The URL of the resource being viewed
|
|
122
|
+
* @param {object} [opts]
|
|
123
|
+
* @param {string|Buffer} [opts.embedJsonLd] - JSON-LD body for the
|
|
124
|
+
* data island, same contract as `generateDatabrowserHtml`. Phase 1 of #7.
|
|
47
125
|
* @returns {string} HTML content
|
|
48
126
|
*/
|
|
49
|
-
export function generateModuleDatabrowserHtml(moduleUrl) {
|
|
127
|
+
export function generateModuleDatabrowserHtml(moduleUrl, resourceUrl = '', opts = {}) {
|
|
50
128
|
const cssUrl = moduleUrl.replace(/\.js$/, '.css');
|
|
129
|
+
const island = dataIsland(resourceUrl, opts.embedJsonLd);
|
|
51
130
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8"/>
|
|
52
131
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
53
132
|
<title>Solid Data Browser</title>
|
|
54
133
|
<link rel="stylesheet" href="${cssUrl}"></head>
|
|
55
|
-
<body
|
|
134
|
+
<body>${island}<div id="mashlib"></div>
|
|
56
135
|
<script type="module" src="${moduleUrl}"></script>
|
|
57
136
|
</body></html>`;
|
|
58
137
|
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-1 tests for the JSON-LD data island (#7).
|
|
3
|
+
*
|
|
4
|
+
* The mashlib HTML wrapper now carries the resource's JSON-LD bytes
|
|
5
|
+
* inside a `<script type="application/ld+json" id="dataisland"
|
|
6
|
+
* data-uri="...">` block. Phase 1 doesn't change mashlib's runtime behaviour — the
|
|
7
|
+
* island is purely additive — so these tests pin:
|
|
8
|
+
* - emission shape (script tag, id, MIME, data-uri)
|
|
9
|
+
* - escape: any `</script>` substring inside the body must not
|
|
10
|
+
* prematurely close the script tag
|
|
11
|
+
* - size cap: oversized payloads silently drop the island so we
|
|
12
|
+
* don't make every navigation re-download a multi-megabyte file
|
|
13
|
+
* - presence in the live HTTP response (resource and container)
|
|
14
|
+
* - safe URI attribute against quote / angle-bracket injection
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, before, after } from 'node:test';
|
|
18
|
+
import assert from 'node:assert';
|
|
19
|
+
import {
|
|
20
|
+
startTestServer,
|
|
21
|
+
stopTestServer,
|
|
22
|
+
request,
|
|
23
|
+
createTestPod,
|
|
24
|
+
assertStatus,
|
|
25
|
+
assertHeaderContains
|
|
26
|
+
} from './helpers.js';
|
|
27
|
+
import {
|
|
28
|
+
generateDatabrowserHtml,
|
|
29
|
+
generateModuleDatabrowserHtml,
|
|
30
|
+
DATA_ISLAND_MAX_BYTES
|
|
31
|
+
} from '../src/mashlib/index.js';
|
|
32
|
+
|
|
33
|
+
describe('mashlib data island — emission (unit, #7)', () => {
|
|
34
|
+
it('emits <script type="application/ld+json" id="dataisland" data-uri="..."> when payload supplied', () => {
|
|
35
|
+
const html = generateDatabrowserHtml(
|
|
36
|
+
'https://test.solid.social/profile/card.jsonld',
|
|
37
|
+
'2.0.0',
|
|
38
|
+
{ embedJsonLd: '{"@id":"#me","foaf:name":"Alice"}' }
|
|
39
|
+
);
|
|
40
|
+
assert.match(html, /<script type="application\/ld\+json" id="dataisland" data-uri="https:\/\/test\.solid\.social\/profile\/card\.jsonld">/);
|
|
41
|
+
assert.match(html, /"@id":"#me"/);
|
|
42
|
+
assert.match(html, /<\/script>/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('omits the data island when no payload is supplied (back-compat)', () => {
|
|
46
|
+
const html = generateDatabrowserHtml('https://x.test/foo', '2.0.0');
|
|
47
|
+
assert.doesNotMatch(html, /id="dataisland"/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('omits the data island for oversized payloads (size cap)', () => {
|
|
51
|
+
// Construct a JSON-LD body just over the cap.
|
|
52
|
+
const filler = 'x'.repeat(DATA_ISLAND_MAX_BYTES + 1024);
|
|
53
|
+
const oversized = `{"content":"${filler}"}`;
|
|
54
|
+
const html = generateDatabrowserHtml(
|
|
55
|
+
'https://x.test/big',
|
|
56
|
+
'2.0.0',
|
|
57
|
+
{ embedJsonLd: oversized }
|
|
58
|
+
);
|
|
59
|
+
assert.doesNotMatch(html, /id="dataisland"/,
|
|
60
|
+
'island must drop silently above DATA_ISLAND_MAX_BYTES');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('cap applies post-escape (defends against `<`-heavy expansion)', () => {
|
|
64
|
+
// A pathological body that's well under the cap as raw bytes but
|
|
65
|
+
// explodes 6x after escaping — every literal `<` byte becomes the
|
|
66
|
+
// 6-byte JSON escape sequence backslash-u-0-0-3-c. Without the
|
|
67
|
+
// post-escape check, this would emit a multi-megabyte island.
|
|
68
|
+
const halfCap = Math.floor(DATA_ISLAND_MAX_BYTES / 2);
|
|
69
|
+
const payload = '<'.repeat(halfCap); // 128 KB raw, ~768 KB escaped
|
|
70
|
+
const html = generateDatabrowserHtml(
|
|
71
|
+
'https://x.test/expand',
|
|
72
|
+
'2.0.0',
|
|
73
|
+
{ embedJsonLd: payload }
|
|
74
|
+
);
|
|
75
|
+
assert.doesNotMatch(html, /id="dataisland"/,
|
|
76
|
+
'island must drop when the escaped body exceeds the cap');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// The escape strategy is "encode every `<` byte as the six-character
|
|
80
|
+
// JSON escape `\\u003c`". Test the wide variety of strings that an
|
|
81
|
+
// HTML parser would otherwise treat as a closing tag — `</script>`,
|
|
82
|
+
// `</script >`, `</script\n>`, `</SCRIPT>`, `</scRIPT>` — plus
|
|
83
|
+
// `<!--`, all of which require a literal `<` to start the dangerous
|
|
84
|
+
// sequence. After escaping, no literal `<` exists in the body and
|
|
85
|
+
// every transformed location appears as `\\u003c`.
|
|
86
|
+
const escapeTrojans = [
|
|
87
|
+
['exact </script>', '{"x":"a</script>b"}'],
|
|
88
|
+
['with space </script >', '{"x":"a</script >b"}'],
|
|
89
|
+
['with newline </script\\n>', '{"x":"a</script\n>b"}'],
|
|
90
|
+
['uppercase </SCRIPT>', '{"x":"a</SCRIPT>b"}'],
|
|
91
|
+
['mixed </ScRiPt>', '{"x":"a</ScRiPt>b"}'],
|
|
92
|
+
['<!-- comment', '{"x":"<!-- hidden -->"}']
|
|
93
|
+
];
|
|
94
|
+
for (const [label, body] of escapeTrojans) {
|
|
95
|
+
it(`escape blocks ${label} from prematurely terminating the tag`, () => {
|
|
96
|
+
const html = generateDatabrowserHtml(
|
|
97
|
+
'https://x.test/r',
|
|
98
|
+
'2.0.0',
|
|
99
|
+
{ embedJsonLd: body }
|
|
100
|
+
);
|
|
101
|
+
const start = html.indexOf('id="dataisland"');
|
|
102
|
+
assert.ok(start > 0, 'data island should be present');
|
|
103
|
+
// Slice from the island's `>` (end of opening tag) forward.
|
|
104
|
+
const open = html.indexOf('>', start) + 1;
|
|
105
|
+
const close = html.indexOf('</script>', open);
|
|
106
|
+
const inner = html.slice(open, close);
|
|
107
|
+
// After our escape, the body must contain NO literal `<`.
|
|
108
|
+
assert.doesNotMatch(inner, /</,
|
|
109
|
+
`script body must not contain a literal "<" — got: ${JSON.stringify(inner)}`);
|
|
110
|
+
// The escaped form (the six characters backslash-u-0-0-3-c)
|
|
111
|
+
// should be present.
|
|
112
|
+
assert.match(inner, /\\u003c/,
|
|
113
|
+
'escaped form `\\u003c` must appear');
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
it('accepts a Buffer payload (handler may pass storage.read() result directly)', () => {
|
|
118
|
+
// The src/handlers/resource.js path used to convert with .toString()
|
|
119
|
+
// before passing in; tightening that contract risks regressions, so
|
|
120
|
+
// the helper accepts a Buffer transparently.
|
|
121
|
+
const buf = Buffer.from('{"@id":"#me","foaf:name":"BufferAlice"}', 'utf8');
|
|
122
|
+
const html = generateDatabrowserHtml(
|
|
123
|
+
'https://x.test/r',
|
|
124
|
+
'2.0.0',
|
|
125
|
+
{ embedJsonLd: buf }
|
|
126
|
+
);
|
|
127
|
+
assert.match(html, /id="dataisland"/);
|
|
128
|
+
assert.match(html, /"foaf:name":"BufferAlice"/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('the module-mode wrapper also emits the data island', () => {
|
|
132
|
+
const html = generateModuleDatabrowserHtml(
|
|
133
|
+
'https://example.test/mashlib.js',
|
|
134
|
+
'https://test.solid.social/profile/card.jsonld',
|
|
135
|
+
{ embedJsonLd: '{"@id":"#me"}' }
|
|
136
|
+
);
|
|
137
|
+
assert.match(html, /<script type="application\/ld\+json" id="dataisland" data-uri="https:\/\/test\.solid\.social\/profile\/card\.jsonld">/);
|
|
138
|
+
assert.match(html, /"@id":"#me"/);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('escapes the data-uri attribute against quote / angle-bracket injection', () => {
|
|
142
|
+
const html = generateDatabrowserHtml(
|
|
143
|
+
'https://x.test/r"><img src=x onerror=alert(1)>',
|
|
144
|
+
'2.0.0',
|
|
145
|
+
{ embedJsonLd: '{}' }
|
|
146
|
+
);
|
|
147
|
+
// The dangerous characters must be HTML-entity encoded inside the
|
|
148
|
+
// attribute, so the attribute can't be broken open.
|
|
149
|
+
assert.match(html, /data-uri="https:\/\/x\.test\/r"><img/);
|
|
150
|
+
assert.doesNotMatch(html, /data-uri="https:\/\/x\.test\/r"><img/);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Integration coverage — the data island must actually appear in the
|
|
155
|
+
// HTTP response when a browser asks for the wrapper.
|
|
156
|
+
describe('mashlib data island — integration (#7)', () => {
|
|
157
|
+
before(async () => {
|
|
158
|
+
await startTestServer({ mashlibCdn: true });
|
|
159
|
+
await createTestPod('islandtest');
|
|
160
|
+
// Put a small JSON-LD resource we can fetch back as HTML.
|
|
161
|
+
await request('/islandtest/public/note.jsonld', {
|
|
162
|
+
method: 'PUT',
|
|
163
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
'@context': { foaf: 'http://xmlns.com/foaf/0.1/' },
|
|
166
|
+
'@id': '#note',
|
|
167
|
+
'foaf:name': 'island test'
|
|
168
|
+
}),
|
|
169
|
+
auth: 'islandtest'
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
after(async () => { await stopTestServer(); });
|
|
174
|
+
|
|
175
|
+
it('a browser GET to a JSON-LD resource carries the data island', async () => {
|
|
176
|
+
const res = await request('/islandtest/public/note.jsonld', {
|
|
177
|
+
headers: { Accept: 'text/html,application/xhtml+xml,*/*;q=0.8' }
|
|
178
|
+
});
|
|
179
|
+
assertStatus(res, 200);
|
|
180
|
+
assertHeaderContains(res, 'Content-Type', 'text/html');
|
|
181
|
+
const body = await res.text();
|
|
182
|
+
assert.match(body, /id="dataisland"/);
|
|
183
|
+
assert.match(body, /<script type="application\/ld\+json"/);
|
|
184
|
+
assert.match(body, /"foaf:name":"island test"/);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('a browser GET to a container carries the listing as a data island', async () => {
|
|
188
|
+
const res = await request('/islandtest/public/', {
|
|
189
|
+
headers: { Accept: 'text/html,application/xhtml+xml,*/*;q=0.8' }
|
|
190
|
+
});
|
|
191
|
+
assertStatus(res, 200);
|
|
192
|
+
assertHeaderContains(res, 'Content-Type', 'text/html');
|
|
193
|
+
const body = await res.text();
|
|
194
|
+
assert.match(body, /id="dataisland"/);
|
|
195
|
+
assert.match(body, /<script type="application\/ld\+json"/);
|
|
196
|
+
// Contains an ldp:contains pointing at the resource we just PUT.
|
|
197
|
+
assert.match(body, /note\.jsonld/);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('non-HTML Accept (mashlib XHR) does NOT trigger the wrapper', async () => {
|
|
201
|
+
const res = await request('/islandtest/public/note.jsonld', {
|
|
202
|
+
headers: { Accept: 'application/ld+json' }
|
|
203
|
+
});
|
|
204
|
+
assertHeaderContains(res, 'Content-Type', 'application/ld+json');
|
|
205
|
+
const body = await res.text();
|
|
206
|
+
assert.doesNotMatch(body, /id="dataisland"/);
|
|
207
|
+
assert.doesNotMatch(body, /<!doctype html>/i);
|
|
208
|
+
});
|
|
209
|
+
});
|