@scratch/scratch-svg-renderer 13.6.0 → 13.6.2
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scratch/scratch-svg-renderer",
|
|
3
|
-
"version": "13.6.
|
|
3
|
+
"version": "13.6.2",
|
|
4
4
|
"description": "SVG renderer for Scratch",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://github.com/scratchfoundation/scratch-svg-renderer#readme",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"base64-js": "1.5.1",
|
|
54
54
|
"base64-loader": "1.0.0",
|
|
55
|
-
"css-tree": "
|
|
55
|
+
"css-tree": "3.2.1",
|
|
56
56
|
"fastestsmallesttextencoderdecoder": "1.0.22",
|
|
57
57
|
"isomorphic-dompurify": "2.36.0",
|
|
58
58
|
"transformation-matrix": "1.15.3",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"babel-loader": "9.2.1",
|
|
65
65
|
"copy-webpack-plugin": "6.4.1",
|
|
66
66
|
"eslint": "9.39.4",
|
|
67
|
-
"eslint-config-scratch": "14.1.
|
|
67
|
+
"eslint-config-scratch": "14.1.4",
|
|
68
68
|
"globals": "16.5.0",
|
|
69
69
|
"jsdom": "13.2.0",
|
|
70
70
|
"mkdirp": "2.1.6",
|
package/src/sanitize-svg.js
CHANGED
|
@@ -4,49 +4,100 @@
|
|
|
4
4
|
*/
|
|
5
5
|
const fixupSvgString = require('./fixup-svg-string');
|
|
6
6
|
const {generate, parse, walk} = require('css-tree');
|
|
7
|
+
const {ident} = require('css-tree/utils');
|
|
7
8
|
const DOMPurify = require('isomorphic-dompurify');
|
|
8
9
|
|
|
9
10
|
const sanitizeSvg = {};
|
|
10
11
|
|
|
11
|
-
const isInternalRef = ref => ref.startsWith('#') || ref.startsWith('data:');
|
|
12
|
+
const isInternalRef = ref => ref.startsWith('#') || ref.toLowerCase().startsWith('data:');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if raw CSS text contains an external url() reference via regex.
|
|
16
|
+
* Used for Raw nodes (e.g. custom property values) that css-tree doesn't fully parse.
|
|
17
|
+
* @param {string} text - raw CSS text to check
|
|
18
|
+
* @returns {boolean} true if an external url() reference was found
|
|
19
|
+
*/
|
|
20
|
+
const rawTextHasExternalUrls = text => {
|
|
21
|
+
const normalized = text.toLowerCase().replace(/\s/g, '');
|
|
22
|
+
const urlPattern = /url\((.+?)\)/g;
|
|
23
|
+
let match;
|
|
24
|
+
while ((match = urlPattern.exec(normalized)) !== null) {
|
|
25
|
+
const ref = match[1].replace(/['"]/g, '');
|
|
26
|
+
if (!isInternalRef(ref)) return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Walk a css-tree AST and return true if any Url node references an external resource.
|
|
33
|
+
* Also checks Raw nodes, which css-tree produces for custom property values and other
|
|
34
|
+
* unparsed content that could still contain url() references.
|
|
35
|
+
* @param {import('css-tree').CssNode} ast - The CSS tree or subtree to walk
|
|
36
|
+
* @returns {boolean} True if an external url() reference was found
|
|
37
|
+
*/
|
|
38
|
+
const astHasExternalUrls = ast => {
|
|
39
|
+
let found = false;
|
|
40
|
+
walk(ast, node => {
|
|
41
|
+
if (node.type === 'Url') {
|
|
42
|
+
const urlValue = node.value.trim().replace(/['"]/g, '');
|
|
43
|
+
if (!isInternalRef(urlValue)) {
|
|
44
|
+
found = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (node.type === 'Raw' && rawTextHasExternalUrls(node.value)) {
|
|
48
|
+
found = true;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return found;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Canonicalize a CSS string and check it for external url() references.
|
|
56
|
+
* Canonicalization: decode CSS escapes, then parse through css-tree so that all syntax
|
|
57
|
+
* variations (quoting, whitespace, comments, escapes) are normalized into AST nodes.
|
|
58
|
+
* @param {string} cssText - raw CSS text
|
|
59
|
+
* @param {string} parseContext - css-tree parse context: 'value' for a single CSS value
|
|
60
|
+
* (presentation attributes like fill, stroke), or 'declarationList' for style attributes.
|
|
61
|
+
* @returns {boolean} true if an external url() reference was found
|
|
62
|
+
*/
|
|
63
|
+
const cssHasExternalUrls = (cssText, parseContext) => {
|
|
64
|
+
const decoded = ident.decode(cssText);
|
|
65
|
+
try {
|
|
66
|
+
return astHasExternalUrls(parse(decoded, {context: parseContext}));
|
|
67
|
+
} catch {
|
|
68
|
+
// If css-tree can't parse it, conservatively check the decoded text.
|
|
69
|
+
// This handles edge cases where creative syntax breaks the parser but
|
|
70
|
+
// a browser might still interpret a url() call.
|
|
71
|
+
return rawTextHasExternalUrls(decoded);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Attributes that directly reference a URI (not via CSS url())
|
|
76
|
+
const URI_ATTRIBUTES = new Set(['href', 'xlink:href']);
|
|
12
77
|
|
|
13
78
|
DOMPurify.addHook(
|
|
14
79
|
'beforeSanitizeAttributes',
|
|
15
80
|
currentNode => {
|
|
81
|
+
if (!currentNode || !currentNode.attributes) return currentNode;
|
|
16
82
|
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
83
|
+
for (let i = currentNode.attributes.length - 1; i >= 0; i--) {
|
|
84
|
+
const attr = currentNode.attributes[i];
|
|
85
|
+
if (!attr.value) continue;
|
|
86
|
+
|
|
87
|
+
if (URI_ATTRIBUTES.has(attr.name)) {
|
|
88
|
+
// Direct URI: strip whitespace and check
|
|
89
|
+
if (!isInternalRef(attr.value.replace(/\s/g, ''))) {
|
|
90
|
+
currentNode.removeAttribute(attr.name);
|
|
25
91
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
92
|
+
} else {
|
|
93
|
+
// CSS value that might contain url()
|
|
94
|
+
const context = attr.name === 'style' ? 'declarationList' : 'value';
|
|
95
|
+
if (cssHasExternalUrls(attr.value, context)) {
|
|
96
|
+
currentNode.removeAttribute(attr.name);
|
|
29
97
|
}
|
|
30
98
|
}
|
|
31
99
|
}
|
|
32
100
|
|
|
33
|
-
// Remove url(...) usages with external references
|
|
34
|
-
if (currentNode && currentNode.attributes) {
|
|
35
|
-
for (let i = currentNode.attributes.length - 1; i >= 0; i--) {
|
|
36
|
-
const attr = currentNode.attributes[i];
|
|
37
|
-
const rawValue = attr.value || '';
|
|
38
|
-
const value = rawValue.toLowerCase().replace(/\s/g, '');
|
|
39
|
-
|
|
40
|
-
const urlMatch = value.match(/url\((.+?)\)/);
|
|
41
|
-
if (urlMatch) {
|
|
42
|
-
const ref = urlMatch[1].replace(/['"]/g, '');
|
|
43
|
-
if (!isInternalRef(ref)) {
|
|
44
|
-
currentNode.removeAttribute(attr.name);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
101
|
return currentNode;
|
|
51
102
|
}
|
|
52
103
|
);
|
|
@@ -55,38 +106,34 @@ DOMPurify.addHook(
|
|
|
55
106
|
'uponSanitizeElement',
|
|
56
107
|
(node, data) => {
|
|
57
108
|
if (data.tagName === 'style') {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// Elements using url(...) for external resources
|
|
69
|
-
if (astNode.type === 'Declaration' && astNode.value) {
|
|
70
|
-
let shouldRemove = false;
|
|
71
|
-
walk(astNode.value, valueNode => {
|
|
72
|
-
if (valueNode.type === 'Url') {
|
|
73
|
-
const urlValue = (valueNode.value.value || '').trim().replace(/['"]/g, '');
|
|
74
|
-
|
|
75
|
-
if (!isInternalRef(urlValue)) {
|
|
76
|
-
shouldRemove = true;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
if (shouldRemove) {
|
|
109
|
+
try {
|
|
110
|
+
// Canonicalize: decode CSS escapes then parse, so css-tree sees
|
|
111
|
+
// normalized tokens (e.g. \75\72\6c becomes url).
|
|
112
|
+
const decodedCss = ident.decode(node.textContent);
|
|
113
|
+
const ast = parse(decodedCss);
|
|
114
|
+
let isModified = decodedCss !== node.textContent;
|
|
115
|
+
|
|
116
|
+
walk(ast, (astNode, item, list) => {
|
|
117
|
+
// @import rules
|
|
118
|
+
if (astNode.type === 'Atrule' && astNode.name.toLowerCase() === 'import') {
|
|
82
119
|
list.remove(item);
|
|
83
120
|
isModified = true;
|
|
84
121
|
}
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
122
|
|
|
88
|
-
|
|
89
|
-
|
|
123
|
+
// Declarations using url(...) for external resources
|
|
124
|
+
if (astNode.type === 'Declaration' && astNode.value && astHasExternalUrls(astNode.value)) {
|
|
125
|
+
list.remove(item);
|
|
126
|
+
isModified = true;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (isModified) {
|
|
131
|
+
node.textContent = generate(ast);
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// If CSS parsing fails, remove the style content entirely
|
|
135
|
+
// rather than risk passing through unsanitized CSS.
|
|
136
|
+
node.textContent = '';
|
|
90
137
|
}
|
|
91
138
|
}
|
|
92
139
|
}
|
|
@@ -98,7 +145,7 @@ let _TextDecoder;
|
|
|
98
145
|
let _TextEncoder;
|
|
99
146
|
if (typeof TextDecoder === 'undefined' || typeof TextEncoder === 'undefined') {
|
|
100
147
|
// Wait to require the text encoding polyfill until we know it's needed.
|
|
101
|
-
|
|
148
|
+
|
|
102
149
|
const encoding = require('fastestsmallesttextencoderdecoder');
|
|
103
150
|
_TextDecoder = encoding.TextDecoder;
|
|
104
151
|
_TextEncoder = encoding.TextEncoder;
|