@longsightgroup/qti3-core 0.2.1 → 0.4.0
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/README.md +85 -8
- package/dist/delivery-security.d.ts +26 -0
- package/dist/delivery-security.d.ts.map +1 -0
- package/dist/delivery-security.js +213 -0
- package/dist/delivery-security.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +21 -3
- package/dist/parser.js.map +1 -1
- package/dist/server-scoring.d.ts +27 -0
- package/dist/server-scoring.d.ts.map +1 -0
- package/dist/server-scoring.js +162 -0
- package/dist/server-scoring.js.map +1 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +192 -105
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +136 -3
- package/dist/validation.js.map +1 -1
- package/dist/value-format.d.ts +11 -0
- package/dist/value-format.d.ts.map +1 -0
- package/dist/value-format.js +107 -0
- package/dist/value-format.js.map +1 -0
- package/dist/xml.d.ts +12 -0
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +200 -50
- package/dist/xml.js.map +1 -1
- package/package.json +2 -2
- package/src/delivery-security.ts +283 -0
- package/src/index.ts +25 -0
- package/src/parser.ts +23 -3
- package/src/server-scoring.ts +244 -0
- package/src/session.ts +336 -291
- package/src/types.ts +3 -0
- package/src/validation.ts +151 -2
- package/src/value-format.ts +103 -0
- package/src/xml.ts +224 -52
package/dist/xml.js
CHANGED
|
@@ -3,51 +3,88 @@ export function parseXmlTree(xml) {
|
|
|
3
3
|
const parser = new StaxXmlParserSync(xml, {
|
|
4
4
|
autoDecodeEntities: true,
|
|
5
5
|
});
|
|
6
|
+
const tagTokens = scanXmlTagTokens(xml);
|
|
6
7
|
const stack = [];
|
|
7
8
|
const errors = [];
|
|
8
9
|
let root;
|
|
9
|
-
let
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (event.type === XmlEventType.START_ELEMENT) {
|
|
16
|
-
const parent = stack.at(-1);
|
|
17
|
-
const offset = findStartElementOffset(xml, event.name, searchOffset);
|
|
18
|
-
searchOffset = offset >= 0 ? offset + 1 : searchOffset;
|
|
19
|
-
const node = {
|
|
20
|
-
name: event.name,
|
|
21
|
-
localName: event.localName ?? event.name,
|
|
22
|
-
attributes: event.attributes,
|
|
23
|
-
children: [],
|
|
24
|
-
content: [],
|
|
25
|
-
text: "",
|
|
26
|
-
source: sourceLocation(xml, offset, nodePath(parent, event.localName ?? event.name)),
|
|
27
|
-
};
|
|
28
|
-
if (parent) {
|
|
29
|
-
node.parent = parent;
|
|
30
|
-
parent.children.push(node);
|
|
31
|
-
parent.content.push(node);
|
|
10
|
+
let tagTokenIndex = 0;
|
|
11
|
+
try {
|
|
12
|
+
for (const event of parser) {
|
|
13
|
+
if (event.type === XmlEventType.ERROR) {
|
|
14
|
+
errors.push(event.error);
|
|
15
|
+
continue;
|
|
32
16
|
}
|
|
33
|
-
|
|
34
|
-
|
|
17
|
+
if (event.type === XmlEventType.START_ELEMENT) {
|
|
18
|
+
const parent = stack.at(-1);
|
|
19
|
+
const path = nodePath(parent, event.localName ?? event.name);
|
|
20
|
+
const sourceRange = { startOffset: -1, startTagEndOffset: -1 };
|
|
21
|
+
const token = tagTokens[tagTokenIndex];
|
|
22
|
+
if (token?.kind === "start" && token.name === event.name) {
|
|
23
|
+
tagTokenIndex += 1;
|
|
24
|
+
sourceRange.startOffset = token.startOffset;
|
|
25
|
+
sourceRange.startTagEndOffset = token.startTagEndOffset;
|
|
26
|
+
if (token.endOffset !== undefined) {
|
|
27
|
+
sourceRange.endOffset = token.endOffset;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
errors.push(new Error(`XML source range alignment failed for <${event.name}>.`));
|
|
32
|
+
}
|
|
33
|
+
const node = {
|
|
34
|
+
name: event.name,
|
|
35
|
+
localName: event.localName ?? event.name,
|
|
36
|
+
prefix: event.prefix,
|
|
37
|
+
uri: event.uri,
|
|
38
|
+
attributes: event.attributes,
|
|
39
|
+
children: [],
|
|
40
|
+
content: [],
|
|
41
|
+
text: "",
|
|
42
|
+
source: sourceLocation(xml, sourceRange.startOffset, path),
|
|
43
|
+
sourceRange,
|
|
44
|
+
};
|
|
45
|
+
if (parent) {
|
|
46
|
+
node.parent = parent;
|
|
47
|
+
parent.children.push(node);
|
|
48
|
+
parent.content.push(node);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
root = node;
|
|
52
|
+
}
|
|
53
|
+
stack.push(node);
|
|
54
|
+
continue;
|
|
35
55
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
if (event.type === XmlEventType.END_ELEMENT) {
|
|
57
|
+
const node = stack.pop();
|
|
58
|
+
if (node) {
|
|
59
|
+
if (node.sourceRange.endOffset === undefined) {
|
|
60
|
+
const token = tagTokens[tagTokenIndex];
|
|
61
|
+
if (token?.kind === "end" && token.name === event.name) {
|
|
62
|
+
tagTokenIndex += 1;
|
|
63
|
+
node.sourceRange.endOffset = token.endOffset;
|
|
64
|
+
node.endSource = sourceLocation(xml, token.startOffset, node.source.path);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
errors.push(new Error(`XML source range alignment failed for </${event.name}>.`));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (event.type === XmlEventType.CHARACTERS || event.type === XmlEventType.CDATA) {
|
|
74
|
+
const node = stack.at(-1);
|
|
75
|
+
if (node) {
|
|
76
|
+
node.text += event.value;
|
|
77
|
+
node.content.push(event.value);
|
|
78
|
+
}
|
|
48
79
|
}
|
|
49
80
|
}
|
|
50
81
|
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
84
|
+
}
|
|
85
|
+
for (const node of [...stack].reverse()) {
|
|
86
|
+
errors.push(new Error(`Unexpected end of document. Missing closing tag for <${node.name}>.`));
|
|
87
|
+
}
|
|
51
88
|
return { root, errors };
|
|
52
89
|
}
|
|
53
90
|
export function childElements(node, localName) {
|
|
@@ -66,26 +103,139 @@ export function textContent(node) {
|
|
|
66
103
|
const parts = node.content.map((entry) => typeof entry === "string" ? entry : textContent(entry));
|
|
67
104
|
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
68
105
|
}
|
|
69
|
-
function
|
|
70
|
-
|
|
106
|
+
function scanXmlTagTokens(xml) {
|
|
107
|
+
const tokens = [];
|
|
108
|
+
let offset = 0;
|
|
71
109
|
while (offset < xml.length) {
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
offset = start + 1;
|
|
110
|
+
const startOffset = xml.indexOf("<", offset);
|
|
111
|
+
if (startOffset === -1 || startOffset + 1 >= xml.length)
|
|
112
|
+
return tokens;
|
|
113
|
+
if (xml.startsWith("<!--", startOffset)) {
|
|
114
|
+
offset = skipPastSequence(xml, "-->", startOffset + 4);
|
|
78
115
|
continue;
|
|
79
116
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return start;
|
|
117
|
+
if (xml.startsWith("<![CDATA[", startOffset)) {
|
|
118
|
+
offset = skipPastSequence(xml, "]]>", startOffset + 9);
|
|
119
|
+
continue;
|
|
84
120
|
}
|
|
85
|
-
|
|
121
|
+
const next = xml.charAt(startOffset + 1);
|
|
122
|
+
if (next === "?") {
|
|
123
|
+
offset = skipPastSequence(xml, "?>", startOffset + 2);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (next === "!") {
|
|
127
|
+
const declarationEndOffset = findMarkupDeclarationEndOffset(xml, startOffset + 2);
|
|
128
|
+
offset = declarationEndOffset >= 0 ? declarationEndOffset + 1 : xml.length;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (next === "/") {
|
|
132
|
+
const tagEndOffset = findTagEndOffset(xml, startOffset + 2);
|
|
133
|
+
if (tagEndOffset < 0)
|
|
134
|
+
return tokens;
|
|
135
|
+
const name = readTagName(xml, startOffset + 2, tagEndOffset);
|
|
136
|
+
if (name) {
|
|
137
|
+
tokens.push({
|
|
138
|
+
kind: "end",
|
|
139
|
+
name,
|
|
140
|
+
startOffset,
|
|
141
|
+
startTagEndOffset: tagEndOffset,
|
|
142
|
+
endOffset: tagEndOffset + 1,
|
|
143
|
+
selfClosing: false,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
offset = tagEndOffset + 1;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const tagEndOffset = findTagEndOffset(xml, startOffset + 1);
|
|
150
|
+
if (tagEndOffset < 0)
|
|
151
|
+
return tokens;
|
|
152
|
+
const name = readTagName(xml, startOffset + 1, tagEndOffset);
|
|
153
|
+
if (name) {
|
|
154
|
+
const selfClosing = isSelfClosingStartTag(xml, tagEndOffset);
|
|
155
|
+
tokens.push({
|
|
156
|
+
kind: "start",
|
|
157
|
+
name,
|
|
158
|
+
startOffset,
|
|
159
|
+
startTagEndOffset: tagEndOffset,
|
|
160
|
+
endOffset: selfClosing ? tagEndOffset + 1 : undefined,
|
|
161
|
+
selfClosing,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
offset = tagEndOffset + 1;
|
|
165
|
+
}
|
|
166
|
+
return tokens;
|
|
167
|
+
}
|
|
168
|
+
function skipPastSequence(xml, sequence, from) {
|
|
169
|
+
const endOffset = xml.indexOf(sequence, from);
|
|
170
|
+
return endOffset >= 0 ? endOffset + sequence.length : xml.length;
|
|
171
|
+
}
|
|
172
|
+
function findMarkupDeclarationEndOffset(xml, from) {
|
|
173
|
+
let quote = null;
|
|
174
|
+
let internalSubsetDepth = 0;
|
|
175
|
+
for (let index = from; index < xml.length; index += 1) {
|
|
176
|
+
const char = xml.charAt(index);
|
|
177
|
+
if (quote) {
|
|
178
|
+
if (char === quote)
|
|
179
|
+
quote = null;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (char === '"' || char === "'") {
|
|
183
|
+
quote = char;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (char === "[") {
|
|
187
|
+
internalSubsetDepth += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (char === "]" && internalSubsetDepth > 0) {
|
|
191
|
+
internalSubsetDepth -= 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (char === ">" && internalSubsetDepth === 0)
|
|
195
|
+
return index;
|
|
196
|
+
}
|
|
197
|
+
return -1;
|
|
198
|
+
}
|
|
199
|
+
function readTagName(xml, from, to) {
|
|
200
|
+
let start = from;
|
|
201
|
+
while (start < to && /\s/.test(xml.charAt(start)))
|
|
202
|
+
start += 1;
|
|
203
|
+
let end = start;
|
|
204
|
+
while (end < to) {
|
|
205
|
+
const char = xml.charAt(end);
|
|
206
|
+
if (/\s/.test(char) || char === "/" || char === ">")
|
|
207
|
+
break;
|
|
208
|
+
end += 1;
|
|
209
|
+
}
|
|
210
|
+
return xml.slice(start, end);
|
|
211
|
+
}
|
|
212
|
+
function findTagEndOffset(xml, from) {
|
|
213
|
+
let quote = null;
|
|
214
|
+
for (let index = from; index < xml.length; index += 1) {
|
|
215
|
+
const char = xml.charAt(index);
|
|
216
|
+
if (quote) {
|
|
217
|
+
if (char === quote)
|
|
218
|
+
quote = null;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (char === '"' || char === "'") {
|
|
222
|
+
quote = char;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (char === ">")
|
|
226
|
+
return index;
|
|
86
227
|
}
|
|
87
228
|
return -1;
|
|
88
229
|
}
|
|
230
|
+
function isSelfClosingStartTag(xml, tagEndOffset) {
|
|
231
|
+
for (let index = tagEndOffset - 1; index >= 0; index -= 1) {
|
|
232
|
+
const char = xml.charAt(index);
|
|
233
|
+
if (/\s/.test(char))
|
|
234
|
+
continue;
|
|
235
|
+
return char === "/";
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
89
239
|
function sourceLocation(xml, offset, path) {
|
|
90
240
|
if (offset < 0)
|
|
91
241
|
return { line: 1, column: 1, offset: 0, path };
|
package/dist/xml.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xml.js","sourceRoot":"","sources":["../src/xml.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"xml.js","sourceRoot":"","sources":["../src/xml.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longsightgroup/qti3-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Framework-neutral TypeScript core for parsing, validating, scoring, and serializing QTI 3 assessment items.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"assessment",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"type": "module",
|
|
37
37
|
"exports": {
|
|
38
38
|
".": {
|
|
39
|
-
"types": "./
|
|
39
|
+
"types": "./src/index.ts",
|
|
40
40
|
"import": "./dist/index.js"
|
|
41
41
|
}
|
|
42
42
|
},
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import type { QtiDiagnostic, QtiSourceLocation } from "./types.js";
|
|
2
|
+
import { descendants, parseXmlTree, type XmlNode } from "./xml.js";
|
|
3
|
+
|
|
4
|
+
const FORBIDDEN_DELIVERY_ELEMENT_NAMES = new Set([
|
|
5
|
+
"correct-response",
|
|
6
|
+
"mapping",
|
|
7
|
+
"area-mapping",
|
|
8
|
+
"match-table",
|
|
9
|
+
"interpolation-table",
|
|
10
|
+
"response-processing",
|
|
11
|
+
"feedback-inline",
|
|
12
|
+
"feedback-block",
|
|
13
|
+
"modal-feedback",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const UNSUPPORTED_SECURE_DELIVERY_ELEMENT_NAMES = new Set([
|
|
17
|
+
"template-processing",
|
|
18
|
+
"set-correct-response",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const DEFAULT_VALUE_DECLARATION_ELEMENT_NAMES = new Set([
|
|
22
|
+
"response-declaration",
|
|
23
|
+
"outcome-declaration",
|
|
24
|
+
"template-declaration",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export type QtiDeliverySecurityFindingKind =
|
|
28
|
+
| "forbidden-delivery-element"
|
|
29
|
+
| "unsupported-secure-delivery-element"
|
|
30
|
+
| "unsupported-adaptive-response-processing";
|
|
31
|
+
|
|
32
|
+
export interface QtiDeliverySecurityFinding {
|
|
33
|
+
kind: QtiDeliverySecurityFindingKind;
|
|
34
|
+
qtiName: string;
|
|
35
|
+
localName: string;
|
|
36
|
+
message: string;
|
|
37
|
+
source?: QtiSourceLocation | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface QtiDeliverySecurityAnalysis {
|
|
41
|
+
diagnostics: QtiDiagnostic[];
|
|
42
|
+
findings: QtiDeliverySecurityFinding[];
|
|
43
|
+
/** True when this exact XML contains no known answer/scoring/feedback delivery leaks. */
|
|
44
|
+
deliverySafe: boolean;
|
|
45
|
+
/** True when secure-delivery redaction can be attempted for this XML. */
|
|
46
|
+
secureDeliverySupported: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface QtiDeliverySafeXmlResult {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
diagnostics: QtiDiagnostic[];
|
|
52
|
+
analysis: QtiDeliverySecurityAnalysis;
|
|
53
|
+
xml?: string | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function analyzeQtiDeliverySecurity(xml: string): QtiDeliverySecurityAnalysis {
|
|
57
|
+
const parsed = parseDeliveryXml(xml);
|
|
58
|
+
return analyzeParsedDeliveryXml(parsed);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function analyzeParsedDeliveryXml(parsed: {
|
|
62
|
+
root: XmlNode | undefined;
|
|
63
|
+
diagnostics: QtiDiagnostic[];
|
|
64
|
+
}): QtiDeliverySecurityAnalysis {
|
|
65
|
+
const diagnostics = [...parsed.diagnostics];
|
|
66
|
+
const findings: QtiDeliverySecurityFinding[] = [];
|
|
67
|
+
|
|
68
|
+
if (parsed.root) {
|
|
69
|
+
const nodes = [parsed.root, ...descendants(parsed.root, () => true)];
|
|
70
|
+
for (const node of nodes) {
|
|
71
|
+
const normalizedName = normalizedQtiElementName(node.localName);
|
|
72
|
+
if (isForbiddenDeliveryElement(node, normalizedName)) {
|
|
73
|
+
findings.push({
|
|
74
|
+
kind: "forbidden-delivery-element",
|
|
75
|
+
qtiName: node.name,
|
|
76
|
+
localName: node.localName,
|
|
77
|
+
message: `${node.name} exposes answer keys, scoring, mapping, feedback, or solution information during delivery.`,
|
|
78
|
+
source: node.source,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (UNSUPPORTED_SECURE_DELIVERY_ELEMENT_NAMES.has(normalizedName)) {
|
|
82
|
+
findings.push({
|
|
83
|
+
kind: "unsupported-secure-delivery-element",
|
|
84
|
+
qtiName: node.name,
|
|
85
|
+
localName: node.localName,
|
|
86
|
+
message: `${node.name} is not supported by secure delivery redaction v1.`,
|
|
87
|
+
source: node.source,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
parseXmlBoolean(parsed.root.attributes.adaptive) === true &&
|
|
94
|
+
nodes.some((node) => normalizedQtiElementName(node.localName) === "response-processing")
|
|
95
|
+
) {
|
|
96
|
+
findings.push({
|
|
97
|
+
kind: "unsupported-adaptive-response-processing",
|
|
98
|
+
qtiName: parsed.root.name,
|
|
99
|
+
localName: parsed.root.localName,
|
|
100
|
+
message:
|
|
101
|
+
"Adaptive response processing requires server-side item materialization before secure delivery.",
|
|
102
|
+
source: parsed.root.source,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
diagnostics.push(...findings.map(findingToDiagnostic));
|
|
108
|
+
const parseOk = parsed.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
diagnostics,
|
|
112
|
+
findings,
|
|
113
|
+
deliverySafe:
|
|
114
|
+
parseOk && !findings.some((finding) => finding.kind === "forbidden-delivery-element"),
|
|
115
|
+
secureDeliverySupported:
|
|
116
|
+
parseOk &&
|
|
117
|
+
!findings.some(
|
|
118
|
+
(finding) =>
|
|
119
|
+
finding.kind === "unsupported-secure-delivery-element" ||
|
|
120
|
+
finding.kind === "unsupported-adaptive-response-processing",
|
|
121
|
+
),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function buildQtiDeliverySafeXml(xml: string): QtiDeliverySafeXmlResult {
|
|
126
|
+
const parsed = parseDeliveryXml(xml);
|
|
127
|
+
const analysis = analyzeParsedDeliveryXml(parsed);
|
|
128
|
+
if (!analysis.secureDeliverySupported) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
diagnostics: analysis.diagnostics,
|
|
132
|
+
analysis,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!parsed.root) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
diagnostics: parsed.diagnostics,
|
|
140
|
+
analysis,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const redactionRanges = readRedactionRanges([
|
|
145
|
+
parsed.root,
|
|
146
|
+
...descendants(parsed.root, () => true),
|
|
147
|
+
]);
|
|
148
|
+
const redactedXml = removeSourceRanges(xml, redactionRanges);
|
|
149
|
+
const redactedAnalysis = analyzeQtiDeliverySecurity(redactedXml);
|
|
150
|
+
if (!redactedAnalysis.deliverySafe) {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
diagnostics: [...analysis.diagnostics, ...redactedAnalysis.diagnostics],
|
|
154
|
+
analysis: redactedAnalysis,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
xml: redactedXml,
|
|
161
|
+
diagnostics: redactedAnalysis.diagnostics,
|
|
162
|
+
analysis: redactedAnalysis,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseDeliveryXml(xml: string): {
|
|
167
|
+
root: XmlNode | undefined;
|
|
168
|
+
diagnostics: QtiDiagnostic[];
|
|
169
|
+
} {
|
|
170
|
+
let parsed: ReturnType<typeof parseXmlTree>;
|
|
171
|
+
try {
|
|
172
|
+
parsed = parseXmlTree(xml);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return {
|
|
175
|
+
root: undefined,
|
|
176
|
+
diagnostics: [
|
|
177
|
+
{
|
|
178
|
+
code: "xml.parse",
|
|
179
|
+
severity: "error",
|
|
180
|
+
message: error instanceof Error ? error.message : String(error),
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const diagnostics: QtiDiagnostic[] = parsed.errors.map((error) => ({
|
|
187
|
+
code: "xml.parse",
|
|
188
|
+
severity: "error",
|
|
189
|
+
message: error.message,
|
|
190
|
+
}));
|
|
191
|
+
if (!parsed.root) {
|
|
192
|
+
diagnostics.push({
|
|
193
|
+
code: "xml.empty",
|
|
194
|
+
severity: "error",
|
|
195
|
+
message: "No XML root element was found.",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return { root: parsed.root, diagnostics };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizedQtiElementName(localName: string): string {
|
|
202
|
+
const lower = localName.toLowerCase();
|
|
203
|
+
return lower.startsWith("qti-") ? lower.slice("qti-".length) : lower;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseXmlBoolean(value: string | undefined): boolean | undefined {
|
|
207
|
+
if (value === undefined) return undefined;
|
|
208
|
+
const normalized = value.trim().toLowerCase();
|
|
209
|
+
if (normalized === "true" || normalized === "1") return true;
|
|
210
|
+
if (normalized === "false" || normalized === "0") return false;
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isForbiddenDeliveryElement(
|
|
215
|
+
node: XmlNode,
|
|
216
|
+
normalizedName = normalizedQtiElementName(node.localName),
|
|
217
|
+
): boolean {
|
|
218
|
+
if (FORBIDDEN_DELIVERY_ELEMENT_NAMES.has(normalizedName)) return true;
|
|
219
|
+
return (
|
|
220
|
+
normalizedName === "default-value" &&
|
|
221
|
+
DEFAULT_VALUE_DECLARATION_ELEMENT_NAMES.has(
|
|
222
|
+
normalizedQtiElementName(node.parent?.localName ?? ""),
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function findingToDiagnostic(finding: QtiDeliverySecurityFinding): QtiDiagnostic {
|
|
228
|
+
const code = diagnosticCodeForFinding(finding.kind);
|
|
229
|
+
return {
|
|
230
|
+
code,
|
|
231
|
+
severity: "error",
|
|
232
|
+
message: finding.message,
|
|
233
|
+
path: finding.source?.path,
|
|
234
|
+
source: finding.source,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function diagnosticCodeForFinding(kind: QtiDeliverySecurityFindingKind): string {
|
|
239
|
+
if (kind === "forbidden-delivery-element") return "delivery.forbiddenElement";
|
|
240
|
+
if (kind === "unsupported-adaptive-response-processing") {
|
|
241
|
+
return "delivery.unsupportedAdaptiveResponseProcessing";
|
|
242
|
+
}
|
|
243
|
+
return "delivery.unsupportedSecureDelivery";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
interface RedactionRange {
|
|
247
|
+
startOffset: number;
|
|
248
|
+
endOffset: number;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function readRedactionRanges(nodes: XmlNode[]): RedactionRange[] {
|
|
252
|
+
const ranges = nodes.flatMap((node) => {
|
|
253
|
+
if (!isForbiddenDeliveryElement(node)) return [];
|
|
254
|
+
const endOffset = node.sourceRange.endOffset;
|
|
255
|
+
if (node.sourceRange.startOffset < 0 || endOffset === undefined) return [];
|
|
256
|
+
return [{ startOffset: node.sourceRange.startOffset, endOffset }];
|
|
257
|
+
});
|
|
258
|
+
return mergeSourceRanges(ranges);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function mergeSourceRanges(ranges: RedactionRange[]): RedactionRange[] {
|
|
262
|
+
const sorted = [...ranges].sort((left, right) => left.startOffset - right.startOffset);
|
|
263
|
+
const merged: RedactionRange[] = [];
|
|
264
|
+
for (const range of sorted) {
|
|
265
|
+
const last = merged.at(-1);
|
|
266
|
+
if (last && range.startOffset <= last.endOffset) {
|
|
267
|
+
last.endOffset = Math.max(last.endOffset, range.endOffset);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
merged.push({ ...range });
|
|
271
|
+
}
|
|
272
|
+
return merged;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function removeSourceRanges(xml: string, ranges: RedactionRange[]): string {
|
|
276
|
+
let output = "";
|
|
277
|
+
let cursor = 0;
|
|
278
|
+
for (const range of ranges) {
|
|
279
|
+
output += xml.slice(cursor, range.startOffset);
|
|
280
|
+
cursor = range.endOffset;
|
|
281
|
+
}
|
|
282
|
+
return output + xml.slice(cursor);
|
|
283
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,21 @@ export {
|
|
|
5
5
|
type QtiResolvedCatalogReference,
|
|
6
6
|
type QtiResolvedCatalogSupport,
|
|
7
7
|
} from "./catalog.js";
|
|
8
|
+
export {
|
|
9
|
+
analyzeQtiDeliverySecurity,
|
|
10
|
+
buildQtiDeliverySafeXml,
|
|
11
|
+
type QtiDeliverySafeXmlResult,
|
|
12
|
+
type QtiDeliverySecurityAnalysis,
|
|
13
|
+
type QtiDeliverySecurityFinding,
|
|
14
|
+
type QtiDeliverySecurityFindingKind,
|
|
15
|
+
} from "./delivery-security.js";
|
|
8
16
|
export { parseQtiXml } from "./parser.js";
|
|
17
|
+
export {
|
|
18
|
+
scoreQtiItemServerSide,
|
|
19
|
+
type QtiServerScoringInput,
|
|
20
|
+
type QtiServerScoringResponseInput,
|
|
21
|
+
type QtiServerScoringResult,
|
|
22
|
+
} from "./server-scoring.js";
|
|
9
23
|
export {
|
|
10
24
|
createTextToSpeechTraversal,
|
|
11
25
|
parseQtiDataSsml,
|
|
@@ -83,3 +97,14 @@ export type {
|
|
|
83
97
|
QtiValidationResult,
|
|
84
98
|
QtiValue,
|
|
85
99
|
} from "./types.js";
|
|
100
|
+
export {
|
|
101
|
+
isQtiPortableCustomStateValue,
|
|
102
|
+
isQtiValue,
|
|
103
|
+
qtiScalarToString,
|
|
104
|
+
qtiValueToIdentifierList,
|
|
105
|
+
qtiValueToString,
|
|
106
|
+
qtiValueToStringList,
|
|
107
|
+
readQtiPortableCustomStateValue,
|
|
108
|
+
readQtiJsonValue,
|
|
109
|
+
unknownToDisplayString,
|
|
110
|
+
} from "./value-format.js";
|