url-sanitizer 2.3.1 → 3.0.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 +64 -34
- package/dist/url-sanitizer-wo-dompurify.min.js +1 -1
- package/dist/url-sanitizer-wo-dompurify.min.js.map +4 -4
- package/dist/url-sanitizer.min.js +4 -4
- package/dist/url-sanitizer.min.js.map +4 -4
- package/package.json +14 -12
- package/src/mjs/file-reader.js +22 -12
- package/src/mjs/sanitizer.js +37 -41
- package/src/mjs/uri-util.js +5 -13
- package/src/mjs/lib-util.js +0 -37
- package/types/mjs/lib-util.d.ts +0 -3
package/package.json
CHANGED
|
@@ -25,29 +25,28 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"dompurify": "^3.4.
|
|
28
|
+
"dompurify": "^3.4.5",
|
|
29
29
|
"jsdom": "^29.1.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@braintree/sanitize-url": "^7.1.2",
|
|
33
33
|
"@types/jsdom": "^28.0.3",
|
|
34
|
-
"@types/node": "^25.
|
|
34
|
+
"@types/node": "^25.9.1",
|
|
35
35
|
"c8": "^11.0.0",
|
|
36
36
|
"chai": "^6.2.2",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"copyfiles-ng": "^2.4.7",
|
|
39
39
|
"esbuild": "^0.28.0",
|
|
40
40
|
"eslint": "^9.39.4",
|
|
41
|
-
"eslint-plugin-jsdoc": "^
|
|
41
|
+
"eslint-plugin-jsdoc": "^63.0.0",
|
|
42
42
|
"eslint-plugin-regexp": "^3.1.0",
|
|
43
43
|
"eslint-plugin-unicorn": "^64.0.0",
|
|
44
44
|
"fast-check": "^4.8.0",
|
|
45
45
|
"globals": "^17.6.0",
|
|
46
46
|
"http-server": "^14.1.1",
|
|
47
47
|
"mitata": "^1.0.34",
|
|
48
|
-
"mocha": "^11.7.
|
|
48
|
+
"mocha": "^11.7.6",
|
|
49
49
|
"neostandard": "^0.13.0",
|
|
50
|
-
"npm-run-all2": "^8.0.4",
|
|
51
50
|
"papaparse": "^5.5.3",
|
|
52
51
|
"sinon": "^22.0.0",
|
|
53
52
|
"strict-url-sanitise": "^0.0.1",
|
|
@@ -69,20 +68,23 @@
|
|
|
69
68
|
"scripts": {
|
|
70
69
|
"bench": "node benchmark/benchmark.js",
|
|
71
70
|
"build": "npm run include && npm run tsc && npm run lint && npm test && npm run copy && npm run bundle",
|
|
72
|
-
"bundle": "npm
|
|
71
|
+
"bundle": "npm run bundle:min && npm run bundle:min-wo-dompurify",
|
|
73
72
|
"bundle:min": "esbuild --format=esm --platform=browser --outfile=dist/url-sanitizer.min.js --bundle --minify --sourcemap bundle/index.js",
|
|
74
73
|
"bundle:min-wo-dompurify": "esbuild --format=esm --platform=browser --outfile=dist/url-sanitizer-wo-dompurify.min.js --bundle --minify --sourcemap bundle_wo_dompurify/index.js",
|
|
75
74
|
"char": "node scripts/index char -i",
|
|
76
|
-
"copy": "npm
|
|
77
|
-
"copy:src": "copyfiles-ng src/**/* bundle --up=1 --verbose --exclude=src/mjs/dompurify.js --exclude=src/mjs/file-reader.js
|
|
78
|
-
"copy:src-wo-dompurify": "copyfiles-ng src/**/* bundle_wo_dompurify --up=1 --verbose --exclude=src/mjs/dompurify.js --exclude=src/mjs/file-reader.js
|
|
79
|
-
"copy:repl": "copyfiles-ng repl/dompurify.js repl/file-reader.js
|
|
80
|
-
"copy:repl-wo-dompurify": "copyfiles-ng repl/dompurify-global.js repl/file-reader.js
|
|
75
|
+
"copy": "npm run copy:src && npm run copy:src-wo-dompurify && npm run copy:repl && npm run copy:repl-wo-dompurify",
|
|
76
|
+
"copy:src": "copyfiles-ng src/**/* bundle --up=1 --verbose --exclude=src/mjs/dompurify.js --exclude=src/mjs/file-reader.js",
|
|
77
|
+
"copy:src-wo-dompurify": "copyfiles-ng src/**/* bundle_wo_dompurify --up=1 --verbose --exclude=src/mjs/dompurify.js --exclude=src/mjs/file-reader.js",
|
|
78
|
+
"copy:repl": "copyfiles-ng repl/dompurify.js repl/file-reader.js bundle/mjs --up=1 --verbose",
|
|
79
|
+
"copy:repl-wo-dompurify": "copyfiles-ng repl/dompurify-global.js repl/file-reader.js bundle_wo_dompurify/mjs --up=1 --verbose && node scripts/index rename --oldpath=bundle_wo_dompurify/mjs/dompurify-global.js --newpath=bundle_wo_dompurify/mjs/dompurify.js -i",
|
|
81
80
|
"include": "node scripts/index include --dir=iana -i",
|
|
82
81
|
"lint": "eslint --fix .",
|
|
83
82
|
"test": "c8 --reporter=text mocha --exit test/*.test.js",
|
|
84
83
|
"test:bdd": "http-server ./ -c-1 -o test/bdd/url-sanitizer.test.html",
|
|
85
84
|
"tsc": "node scripts/index clean --dir=types -i && npx tsc"
|
|
86
85
|
},
|
|
87
|
-
"
|
|
86
|
+
"engines": {
|
|
87
|
+
"node": ">=22"
|
|
88
|
+
},
|
|
89
|
+
"version": "3.0.0"
|
|
88
90
|
}
|
package/src/mjs/file-reader.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/* shared */
|
|
6
|
+
import textChars from '../lib/file/text-chars.json' with { type: 'json' };
|
|
6
7
|
import { getType, isString } from './common.js';
|
|
7
|
-
import { textChars } from './lib-util.js';
|
|
8
8
|
|
|
9
9
|
/* constants */
|
|
10
10
|
import {
|
|
@@ -103,19 +103,17 @@ export class FileReader extends EventTarget {
|
|
|
103
103
|
* @returns {boolean} - result
|
|
104
104
|
*/
|
|
105
105
|
_dispatchProgressEvent(type) {
|
|
106
|
-
if (isString(type)) {
|
|
107
|
-
type = type.trim();
|
|
108
|
-
if (!/abort|error|load(?:end|start)?|progress/.test(type)) {
|
|
109
|
-
this.#error = new DOMException('Invalid state.', 'InvalidStateError');
|
|
110
|
-
this._dispatchProgressEvent('error');
|
|
111
|
-
throw this.#error;
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
106
|
+
if (!isString(type)) {
|
|
114
107
|
this.#error = new TypeError(`Expected String but got ${getType(type)}.`);
|
|
115
108
|
this._dispatchProgressEvent('error');
|
|
116
109
|
throw this.#error;
|
|
117
110
|
}
|
|
118
|
-
|
|
111
|
+
type = type.trim();
|
|
112
|
+
if (!/abort|error|load(?:end|start)?|progress/.test(type)) {
|
|
113
|
+
this.#error = new DOMException('Invalid state.', 'InvalidStateError');
|
|
114
|
+
this._dispatchProgressEvent('error');
|
|
115
|
+
throw this.#error;
|
|
116
|
+
} else if (type === 'error' && !this.#error) {
|
|
119
117
|
this.#error = new Error('Unknown error.');
|
|
120
118
|
}
|
|
121
119
|
const evt = new ProgressEvent(type, {
|
|
@@ -132,9 +130,15 @@ export class FileReader extends EventTarget {
|
|
|
132
130
|
* @returns {void}
|
|
133
131
|
*/
|
|
134
132
|
abort() {
|
|
133
|
+
if (this.#state === this.EMPTY || this.#state === this.DONE) {
|
|
134
|
+
this.#result = null;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
135
137
|
this.#state = this.DONE;
|
|
136
138
|
this.#result = null;
|
|
137
139
|
this.#terminate = true;
|
|
140
|
+
this.#error =
|
|
141
|
+
new DOMException('The read operation was aborted.', 'AbortError');
|
|
138
142
|
this._dispatchProgressEvent('abort');
|
|
139
143
|
this._dispatchProgressEvent('loadend');
|
|
140
144
|
}
|
|
@@ -147,8 +151,14 @@ export class FileReader extends EventTarget {
|
|
|
147
151
|
* @returns {Promise.<void>} - void
|
|
148
152
|
*/
|
|
149
153
|
async _read(blob, format, encoding = '') {
|
|
150
|
-
if (!(blob instanceof Blob
|
|
151
|
-
|
|
154
|
+
if (!(blob instanceof Blob)) {
|
|
155
|
+
throw new TypeError(`Expected Blob but got ${getType(blob)}.`);
|
|
156
|
+
}
|
|
157
|
+
if (!isString(format)) {
|
|
158
|
+
throw new TypeError(`Expected String but got ${getType(format)}.`);
|
|
159
|
+
}
|
|
160
|
+
if (!isString(encoding)) {
|
|
161
|
+
throw new TypeError(`Expected String but got ${getType(encoding)}.`);
|
|
152
162
|
}
|
|
153
163
|
if (!this.#terminate) {
|
|
154
164
|
if (this.#state === this.LOADING) {
|
package/src/mjs/sanitizer.js
CHANGED
|
@@ -64,6 +64,35 @@ class URLSanitizer extends URISchemes {
|
|
|
64
64
|
this.#nest = 0;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* helper to register schemes for allow/only options
|
|
69
|
+
* @private
|
|
70
|
+
* @param {string} item - scheme to register
|
|
71
|
+
* @param {string} listName - name of the option list
|
|
72
|
+
* @param {object} ctx - local context for state management
|
|
73
|
+
* @returns {boolean} - true if scheme is acceptable
|
|
74
|
+
*/
|
|
75
|
+
#registerScheme(item, listName, ctx) {
|
|
76
|
+
if (!REG_SCRIPT_BLOB.test(item)) {
|
|
77
|
+
if (super.has(item)) {
|
|
78
|
+
ctx.schemeMap.set(item, true);
|
|
79
|
+
} else {
|
|
80
|
+
try {
|
|
81
|
+
super.add(item);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
const msg = `Failed to add scheme '${item}' in '${listName}' list.`;
|
|
84
|
+
logDebug(ctx.debug, msg, e);
|
|
85
|
+
}
|
|
86
|
+
if (super.has(item)) {
|
|
87
|
+
ctx.schemeMap.set(item, true);
|
|
88
|
+
ctx.tempScheme.add(item);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
67
96
|
/**
|
|
68
97
|
* replace matched data URLs
|
|
69
98
|
* @param {string} data - data URL
|
|
@@ -168,65 +197,32 @@ class URLSanitizer extends URISchemes {
|
|
|
168
197
|
['vbscript', false]
|
|
169
198
|
]);
|
|
170
199
|
const tempScheme = new Set();
|
|
200
|
+
const ctx = { schemeMap, tempScheme, debug };
|
|
171
201
|
let restrictScheme = false;
|
|
172
202
|
if (Array.isArray(only) && only.length) {
|
|
173
203
|
const schemes = super.get();
|
|
174
204
|
for (const item of schemes) {
|
|
175
205
|
schemeMap.set(item, false);
|
|
176
206
|
}
|
|
177
|
-
|
|
178
|
-
for (let item of items) {
|
|
207
|
+
for (let item of only) {
|
|
179
208
|
if (isString(item)) {
|
|
180
209
|
item = item.trim();
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
} else {
|
|
185
|
-
try {
|
|
186
|
-
super.add(item);
|
|
187
|
-
} catch (e) {
|
|
188
|
-
const msg = `Failed to add scheme '${item}' in 'only' list.`;
|
|
189
|
-
logDebug(debug, msg, e);
|
|
190
|
-
}
|
|
191
|
-
if (super.has(item)) {
|
|
192
|
-
schemeMap.set(item, true);
|
|
193
|
-
tempScheme.add(item);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (!restrictScheme && schemeMap.has(item)) {
|
|
197
|
-
restrictScheme = schemeMap.get(item);
|
|
198
|
-
}
|
|
210
|
+
const registered = this.#registerScheme(item, 'only', ctx);
|
|
211
|
+
if (registered && !restrictScheme && schemeMap.has(item)) {
|
|
212
|
+
restrictScheme = schemeMap.get(item);
|
|
199
213
|
}
|
|
200
214
|
}
|
|
201
215
|
}
|
|
202
216
|
} else {
|
|
203
217
|
if (Array.isArray(allow) && allow.length) {
|
|
204
|
-
const
|
|
205
|
-
for (let item of items) {
|
|
218
|
+
for (const item of allow) {
|
|
206
219
|
if (isString(item)) {
|
|
207
|
-
item
|
|
208
|
-
if (!REG_SCRIPT_BLOB.test(item)) {
|
|
209
|
-
if (super.has(item)) {
|
|
210
|
-
schemeMap.set(item, true);
|
|
211
|
-
} else {
|
|
212
|
-
try {
|
|
213
|
-
super.add(item);
|
|
214
|
-
} catch (e) {
|
|
215
|
-
const msg = `Failed to add scheme '${item}' in 'allow' list.`;
|
|
216
|
-
logDebug(debug, msg, e);
|
|
217
|
-
}
|
|
218
|
-
if (super.has(item)) {
|
|
219
|
-
schemeMap.set(item, true);
|
|
220
|
-
tempScheme.add(item);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
220
|
+
this.#registerScheme(item.trim(), 'allow', ctx);
|
|
224
221
|
}
|
|
225
222
|
}
|
|
226
223
|
}
|
|
227
224
|
if (Array.isArray(deny) && deny.length) {
|
|
228
|
-
|
|
229
|
-
for (let item of items) {
|
|
225
|
+
for (let item of deny) {
|
|
230
226
|
if (isString(item)) {
|
|
231
227
|
item = item.trim();
|
|
232
228
|
if (item) {
|
package/src/mjs/uri-util.js
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/* shared */
|
|
6
|
+
import textChars from '../lib/file/text-chars.json' with { type: 'json' };
|
|
7
|
+
import uriSchemes from '../lib/iana/uri-schemes.json' with { type: 'json' };
|
|
6
8
|
import { getType, isString } from './common.js';
|
|
7
9
|
import { FileReader } from './file-reader.js';
|
|
8
|
-
import { textChars, uriSchemes } from './lib-util.js';
|
|
9
10
|
|
|
11
|
+
/* constants */
|
|
10
12
|
import {
|
|
11
13
|
HEX, MAX_BLOB_SIZE, REG_B64, REG_NUM_REF, REG_SCHEME, REG_SCHEME_EXT,
|
|
12
14
|
REG_SCRIPT, REG_URL_ENC
|
|
@@ -154,14 +156,7 @@ export const createDataURLFromBlob = (blob, maxBlobSize = MAX_BLOB_SIZE) =>
|
|
|
154
156
|
*/
|
|
155
157
|
export class URISchemes {
|
|
156
158
|
/* private fields */
|
|
157
|
-
#schemes;
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* construct
|
|
161
|
-
*/
|
|
162
|
-
constructor() {
|
|
163
|
-
this.#schemes = new Set(uriSchemes);
|
|
164
|
-
}
|
|
159
|
+
#schemes = new Set(uriSchemes);
|
|
165
160
|
|
|
166
161
|
/**
|
|
167
162
|
* get schemes
|
|
@@ -234,9 +229,6 @@ export class URISchemes {
|
|
|
234
229
|
* @returns {void}
|
|
235
230
|
*/
|
|
236
231
|
reset() {
|
|
237
|
-
this.#schemes
|
|
238
|
-
for (const scheme of uriSchemes) {
|
|
239
|
-
this.#schemes.add(scheme);
|
|
240
|
-
}
|
|
232
|
+
this.#schemes = new Set(uriSchemes);
|
|
241
233
|
}
|
|
242
234
|
}
|
package/src/mjs/lib-util.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib-util.js
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
|
|
8
|
-
/* constants */
|
|
9
|
-
const CHAR = 'utf8';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* parse json
|
|
13
|
-
* @param {string} url - file url
|
|
14
|
-
* @returns {string|Array|object} - parsed json
|
|
15
|
-
*/
|
|
16
|
-
export const parseJson = url => {
|
|
17
|
-
const { href } = new URL(url, import.meta.url);
|
|
18
|
-
const filePath = fileURLToPath(href);
|
|
19
|
-
const file = fs.readFileSync(filePath, {
|
|
20
|
-
encoding: CHAR,
|
|
21
|
-
flag: 'r'
|
|
22
|
-
});
|
|
23
|
-
const content = JSON.parse(file);
|
|
24
|
-
return content;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* text chars
|
|
29
|
-
* @type {Array.<number>}
|
|
30
|
-
*/
|
|
31
|
-
export const textChars = parseJson('../lib/file/text-chars.json');
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* uri schemes
|
|
35
|
-
* @type {Array.<string>}
|
|
36
|
-
*/
|
|
37
|
-
export const uriSchemes = parseJson('../lib/iana/uri-schemes.json');
|
package/types/mjs/lib-util.d.ts
DELETED