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/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.1"
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
@@ -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
- const items = Object.values(only);
178
- for (let item of items) {
207
+ for (let item of only) {
179
208
  if (isString(item)) {
180
209
  item = item.trim();
181
- if (!REG_SCRIPT_BLOB.test(item)) {
182
- if (super.has(item)) {
183
- schemeMap.set(item, true);
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 items = Object.values(allow);
205
- for (let item of items) {
218
+ for (const item of allow) {
206
219
  if (isString(item)) {
207
- item = item.trim();
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
- const items = Object.values(deny);
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) {
@@ -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>;