url-sanitizer 2.3.0 → 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/package.json CHANGED
@@ -25,29 +25,28 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "dompurify": "^3.4.3",
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.8.0",
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": "^62.9.0",
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.5",
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-run-all -s bundle:*",
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-run-all -s copy:*",
77
- "copy:src": "copyfiles-ng src/**/* bundle --up=1 --verbose --exclude=src/mjs/dompurify.js --exclude=src/mjs/file-reader.js --exclude=src/mjs/lib-util.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 --exclude=src/mjs/lib-util.js",
79
- "copy:repl": "copyfiles-ng repl/dompurify.js repl/file-reader.js repl/lib-util.js bundle/mjs --up=1 --verbose",
80
- "copy:repl-wo-dompurify": "copyfiles-ng repl/dompurify-global.js repl/file-reader.js repl/lib-util.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",
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
- "version": "2.3.0"
86
+ "engines": {
87
+ "node": ">=22"
88
+ },
89
+ "version": "3.0.0"
88
90
  }
@@ -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
- if (type === 'error' && !this.#error) {
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 && isString(format) && isString(encoding))) {
151
- this.abort();
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) {
@@ -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
@@ -84,9 +113,15 @@ class URLSanitizer extends URISchemes {
84
113
  }
85
114
  this.#nest++;
86
115
  this.#recurse.add(dataUrl);
87
- const parsedDataUrl = this.sanitize(dataUrl, {
88
- allow: ['data']
89
- });
116
+ let parsedDataUrl;
117
+ try {
118
+ parsedDataUrl = this.sanitize(dataUrl, {
119
+ allow: ['data']
120
+ });
121
+ } finally {
122
+ this.#nest--;
123
+ this.#recurse.delete(dataUrl);
124
+ }
90
125
  const { index } = item;
91
126
  const [preDataUrl, postDataUrl] = [
92
127
  replacedData.substring(0, index),
@@ -149,7 +184,6 @@ class URLSanitizer extends URISchemes {
149
184
  */
150
185
  sanitize(url, opt) {
151
186
  if (this.#nest > HEX) {
152
- this.#nest = 0;
153
187
  throw new Error('Data URLs nested too deeply.');
154
188
  }
155
189
  const {
@@ -159,69 +193,36 @@ class URLSanitizer extends URISchemes {
159
193
  ['blob', false],
160
194
  ['data', false],
161
195
  ['file', false],
162
- ['javascrpt', false],
196
+ ['javascript', false],
163
197
  ['vbscript', false]
164
198
  ]);
165
199
  const tempScheme = new Set();
200
+ const ctx = { schemeMap, tempScheme, debug };
166
201
  let restrictScheme = false;
167
202
  if (Array.isArray(only) && only.length) {
168
203
  const schemes = super.get();
169
204
  for (const item of schemes) {
170
205
  schemeMap.set(item, false);
171
206
  }
172
- const items = Object.values(only);
173
- for (let item of items) {
207
+ for (let item of only) {
174
208
  if (isString(item)) {
175
209
  item = item.trim();
176
- if (!REG_SCRIPT_BLOB.test(item)) {
177
- if (super.has(item)) {
178
- schemeMap.set(item, true);
179
- } else {
180
- try {
181
- super.add(item);
182
- } catch (e) {
183
- const msg = `Failed to add scheme '${item}' in 'only' list.`;
184
- logDebug(debug, msg, e);
185
- }
186
- if (super.has(item)) {
187
- schemeMap.set(item, true);
188
- tempScheme.add(item);
189
- }
190
- }
191
- if (!restrictScheme && schemeMap.has(item)) {
192
- restrictScheme = schemeMap.get(item);
193
- }
210
+ const registered = this.#registerScheme(item, 'only', ctx);
211
+ if (registered && !restrictScheme && schemeMap.has(item)) {
212
+ restrictScheme = schemeMap.get(item);
194
213
  }
195
214
  }
196
215
  }
197
216
  } else {
198
217
  if (Array.isArray(allow) && allow.length) {
199
- const items = Object.values(allow);
200
- for (let item of items) {
218
+ for (const item of allow) {
201
219
  if (isString(item)) {
202
- item = item.trim();
203
- if (!REG_SCRIPT_BLOB.test(item)) {
204
- if (super.has(item)) {
205
- schemeMap.set(item, true);
206
- } else {
207
- try {
208
- super.add(item);
209
- } catch (e) {
210
- const msg = `Failed to add scheme '${item}' in 'allow' list.`;
211
- logDebug(debug, msg, e);
212
- }
213
- if (super.has(item)) {
214
- schemeMap.set(item, true);
215
- tempScheme.add(item);
216
- }
217
- }
218
- }
220
+ this.#registerScheme(item.trim(), 'allow', ctx);
219
221
  }
220
222
  }
221
223
  }
222
224
  if (Array.isArray(deny) && deny.length) {
223
- const items = Object.values(deny);
224
- for (let item of items) {
225
+ for (let item of deny) {
225
226
  if (isString(item)) {
226
227
  item = item.trim();
227
228
  if (item) {
@@ -271,7 +272,6 @@ class URLSanitizer extends URISchemes {
271
272
  }
272
273
  if (bool) {
273
274
  const isDataUrl = isRelative ? false : schemeParts.includes('data');
274
- let finalize;
275
275
  let urlToSanitize = isRelative ? url : href;
276
276
  if (isDataUrl) {
277
277
  const [mediaType, ...dataParts] = pathname.split(',');
@@ -297,15 +297,7 @@ class URLSanitizer extends URISchemes {
297
297
  if (parsedData !== data || containsDataUrl) {
298
298
  if (containsDataUrl) {
299
299
  parsedData = this.replace(parsedData);
300
- } else if (this.#recurse.has(url)) {
301
- this.#recurse.delete(url);
302
- } else {
303
- finalize = true;
304
300
  }
305
- } else if (this.#recurse.has(url)) {
306
- this.#recurse.delete(url);
307
- } else {
308
- finalize = true;
309
301
  }
310
302
  if (!mediaType || REG_MIME_DOM.test(mediaType)) {
311
303
  parsedData = this.purify(parsedData);
@@ -318,8 +310,6 @@ class URLSanitizer extends URISchemes {
318
310
  } else {
319
311
  urlToSanitize = '';
320
312
  }
321
- } else {
322
- finalize = true;
323
313
  }
324
314
  if (!isDataUrl && REG_TAG_QUOT.test(urlToSanitize)) {
325
315
  const item = REG_TAG_QUOT.exec(urlToSanitize);
@@ -330,11 +320,6 @@ class URLSanitizer extends URISchemes {
330
320
  if (urlToSanitize) {
331
321
  sanitizedUrl =
332
322
  urlToSanitize.replace(/%26/g, escapeURLEncodedHTMLChars);
333
- if (finalize) {
334
- this.#nest = 0;
335
- }
336
- } else {
337
- this.#nest = 0;
338
323
  }
339
324
  }
340
325
  if (tempScheme.size) {
@@ -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.clear();
238
- for (const scheme of uriSchemes) {
239
- this.#schemes.add(scheme);
240
- }
232
+ this.#schemes = new Set(uriSchemes);
241
233
  }
242
234
  }
@@ -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');
@@ -1,3 +0,0 @@
1
- export function parseJson(url: string): string | any[] | object;
2
- export const textChars: Array<number>;
3
- export const uriSchemes: Array<string>;