url-sanitizer 2.1.0 → 2.3.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
@@ -29,6 +29,7 @@
29
29
  "jsdom": "^29.1.1"
30
30
  },
31
31
  "devDependencies": {
32
+ "@braintree/sanitize-url": "^7.1.2",
32
33
  "@types/jsdom": "^28.0.3",
33
34
  "@types/node": "^25.8.0",
34
35
  "c8": "^11.0.0",
@@ -40,13 +41,16 @@
40
41
  "eslint-plugin-jsdoc": "^62.9.0",
41
42
  "eslint-plugin-regexp": "^3.1.0",
42
43
  "eslint-plugin-unicorn": "^64.0.0",
44
+ "fast-check": "^4.8.0",
43
45
  "globals": "^17.6.0",
44
46
  "http-server": "^14.1.1",
47
+ "mitata": "^1.0.34",
45
48
  "mocha": "^11.7.5",
46
49
  "neostandard": "^0.13.0",
47
50
  "npm-run-all2": "^8.0.4",
48
51
  "papaparse": "^5.5.3",
49
52
  "sinon": "^22.0.0",
53
+ "strict-url-sanitise": "^0.0.1",
50
54
  "typescript": "^6.0.3",
51
55
  "undici": "^8.0.2"
52
56
  },
@@ -63,6 +67,7 @@
63
67
  "serialize-javascript": "^7.0.4"
64
68
  },
65
69
  "scripts": {
70
+ "bench": "node benchmark/benchmark.js",
66
71
  "build": "npm run include && npm run tsc && npm run lint && npm test && npm run copy && npm run bundle",
67
72
  "bundle": "npm-run-all -s bundle:*",
68
73
  "bundle:min": "esbuild --format=esm --platform=browser --outfile=dist/url-sanitizer.min.js --bundle --minify --sourcemap bundle/index.js",
@@ -79,5 +84,5 @@
79
84
  "test:bdd": "http-server ./ -c-1 -o test/bdd/url-sanitizer.test.html",
80
85
  "tsc": "node scripts/index clean --dir=types -i && npx tsc"
81
86
  },
82
- "version": "2.1.0"
87
+ "version": "2.3.0"
83
88
  }
@@ -1 +1 @@
1
- ["aaa","aaas","about","acap","acct","acd","acr","adiumxtra","adt","afp","afs","agtp","aim","amss","android","appdata","apt","ar","ari","ark","at","attachment","aw","barion","beshare","bitcoin","bitcoincash","bl","blob","bluetooth","bolo","brid","browserext","cabal","caip","calculator","callto","cap","cast","casts","chrome","chrome-extension","cid","coap","coaps","com-eventbrite-attendee","content","content-type","crid","cstr","cttps","cvs","dab","dat","data","dav","dhttp","diaspora","dict","did","dilithium3","dis","dlna-playcontainer","dlna-playsingle","dnp","dns","dntp","doi","donau","dpp","drm","dtmi","dtn","dvb","dvx","dweb","ed2k","eid","elsi","embedded","ens","esim","ethereum","example","facetime","feed","feedready","fido","file","finger","first-run-pen-experience","fish","fm","ftp","fuchsia-pkg","geo","gg","git","gitoid","gizmoproject","go","gopher","graph","gtalk","h323","ham","hcap","hcp","hs20","http","https","hxxp","hxxps","hydrazone","hyper","i0","iax","ibi","ibi-","icap","icon","ilstring","im","imap","info","iotdisco","ipfs","ipn","ipns","ipp","ipps","irc","irc6","ircs","iris","iris.beep","iris.lwz","iris.xpc","iris.xpcs","isostore","itms","jabber","jar","jms","keyparc","lastfm","lbry","ldap","ldaps","leaptofrogans","lid","linkid","lorawan","lpa","lvlt","machineProvisioningProgressReporter","magnet","mailto","maps","market","matrix","message","microsoft.windows.camera","microsoft.windows.camera.multipicker","microsoft.windows.camera.picker","mid","mms","mongodb","moz","moz-extension","mqtt","mqtts","ms-access","ms-appinstaller","ms-browser-extension","ms-calculator","ms-drive-to","ms-enrollment","ms-excel","ms-eyecontrolspeech","ms-gamebarservices","ms-gamingoverlay","ms-getoffice","ms-help","ms-infopath","ms-inputapp","ms-launchremotedesktop","ms-lockscreencomponent-config","ms-media-stream-id","ms-meetnow","ms-mixedrealitycapture","ms-mobileplans","ms-newsandinterests","ms-officeapp","ms-people","ms-personacard","ms-powerpoint","ms-project","ms-publisher","ms-recall","ms-remotedesktop","ms-remotedesktop-launch","ms-restoretabcompanion","ms-screenclip","ms-screensketch","ms-search","ms-search-repair","ms-secondary-screen-controller","ms-secondary-screen-setup","ms-settings","ms-settings-airplanemode","ms-settings-bluetooth","ms-settings-camera","ms-settings-cellular","ms-settings-cloudstorage","ms-settings-connectabledevices","ms-settings-displays-topology","ms-settings-emailandaccounts","ms-settings-language","ms-settings-location","ms-settings-lock","ms-settings-nfctransactions","ms-settings-notifications","ms-settings-power","ms-settings-privacy","ms-settings-proximity","ms-settings-screenrotation","ms-settings-wifi","ms-settings-workplace","ms-spd","ms-stickers","ms-sttoverlay","ms-transit-to","ms-useractivityset","ms-uup","ms-virtualtouchpad","ms-visio","ms-walk-to","ms-whiteboard","ms-whiteboard-cmd","ms-widgetboard","ms-widgets","ms-word","msnim","msrp","msrps","mss","mt","mtqp","mtrust","mumble","mupdate","mvn","mvrp","mvrps","news","nfs","ni","nih","nntp","notes","num","ocf","oid","onenote","onenote-cmd","opaquelocktoken","openid","openpgp4fpr","otpauth","palm","paparazzi","payto","pkcs11","platform","pop","pres","proxy","psyc","pttp","pwid","qb","query","quic-transport","redis","rediss","reload","res","resource","rmi","rsync","rtmfp","rtmp","rtsp","rtsps","rtspu","sarif","secondlife","secret-token","service","session","sftp","sgn","shc","shelter","sieve","simpleledger","simplex","sip","sips","skype","smb","smp","sms","smtp","snmp","soap.beep","soap.beeps","soldat","spacify","spiffe","spotify","ssb","ssh","starknet","steam","stun","stuns","submit","svn","swh","swid","swidpath","tag","taler","teamspeak","teapot","teapots","tel","teliaeid","telnet","tftp","things","thismessage","tip","tn3270","tool","turn","turns","tv","udp","unreal","upn","urn","ut2004","uuid-in-package","v-event","vemmi","ventrilo","ves","view-source","vnc","vscode","vscode-insiders","vsls","w3","wasm","wasm-js","wcr","web3","webcal","wifi","ws","wss","wtai","wyciwyg","xcompute","xcon","xcon-userid","xfire","xftp","xmlrpc.beep","xmlrpc.beeps","xmpp","xrcp","xri","ymsgr","z39.50r","z39.50s"]
1
+ ["aaa","aaas","about","acap","acct","acd","acr","adiumxtra","adt","afp","afs","agtp","aim","amss","android","appdata","apt","ar","ari","ark","at","attachment","aw","barion","beshare","bitcoin","bitcoincash","bl","blob","bluetooth","bolo","brid","browserext","cabal","caip","calculator","callto","cap","cast","casts","chrome","chrome-extension","cid","coap","coaps","com-eventbrite-attendee","content","content-type","crid","cstr","cttps","cvs","dab","dat","data","dav","dhttp","diaspora","dict","did","dilithium3","dis","dlna-playcontainer","dlna-playsingle","dnp","dns","dntp","doi","donau","dpp","drm","dtmi","dtn","dvb","dvx","dweb","ed2k","eid","elsi","embedded","ens","esim","ethereum","example","facetime","feed","feedready","fido","file","finger","first-run-pen-experience","fish","fm","ftp","fuchsia-pkg","geo","gg","git","gitoid","gizmoproject","go","gopher","graph","gtalk","h323","ham","hcap","hcp","hs20","http","https","hxxp","hxxps","hydrazone","hyper","i0","iax","ibi","ibi-","icap","icon","ilstring","im","imap","info","iotdisco","ipfs","ipn","ipns","ipp","ipps","irc","irc6","ircs","iris","iris.beep","iris.lwz","iris.xpc","iris.xpcs","isostore","itms","jabber","jar","jms","keyparc","lastfm","lbry","ldap","ldaps","leaptofrogans","lid","linkid","lorawan","lpa","lvlt","machineProvisioningProgressReporter","magnet","mailto","maps","market","matrix","message","microsoft.windows.camera","microsoft.windows.camera.multipicker","microsoft.windows.camera.picker","mid","mms","mongodb","moz","moz-extension","mqtt","mqtts","ms-access","ms-appinstaller","ms-browser-extension","ms-calculator","ms-drive-to","ms-enrollment","ms-excel","ms-eyecontrolspeech","ms-gamebarservices","ms-gamingoverlay","ms-getoffice","ms-help","ms-infopath","ms-inputapp","ms-launchremotedesktop","ms-lockscreencomponent-config","ms-media-stream-id","ms-meetnow","ms-mixedrealitycapture","ms-mobileplans","ms-newsandinterests","ms-officeapp","ms-people","ms-personacard","ms-powerpoint","ms-project","ms-publisher","ms-recall","ms-remotedesktop","ms-remotedesktop-launch","ms-restoretabcompanion","ms-screenclip","ms-screensketch","ms-search","ms-search-repair","ms-secondary-screen-controller","ms-secondary-screen-setup","ms-settings","ms-settings-airplanemode","ms-settings-bluetooth","ms-settings-camera","ms-settings-cellular","ms-settings-cloudstorage","ms-settings-connectabledevices","ms-settings-displays-topology","ms-settings-emailandaccounts","ms-settings-language","ms-settings-location","ms-settings-lock","ms-settings-nfctransactions","ms-settings-notifications","ms-settings-power","ms-settings-privacy","ms-settings-proximity","ms-settings-screenrotation","ms-settings-wifi","ms-settings-workplace","ms-spd","ms-stickers","ms-sttoverlay","ms-transit-to","ms-useractivityset","ms-uup","ms-virtualtouchpad","ms-visio","ms-walk-to","ms-whiteboard","ms-whiteboard-cmd","ms-widgetboard","ms-widgets","ms-word","msnim","msrp","msrps","mss","mt","mtqp","mtrust","mumble","mupdate","mvn","mvrp","mvrps","news","nfs","ni","nih","nntp","notes","num","ocf","oid","onenote","onenote-cmd","opaquelocktoken","openid","openpgp4fpr","otpauth","palm","paparazzi","payto","pkcs11","platform","pop","pres","proxy","psyc","pttp","pwid","qb","query","quic-transport","redis","rediss","reload","res","resource","rmi","rsync","rtmfp","rtmp","rtsp","rtsps","rtspu","sarif","secondlife","secret-token","service","session","sftp","sgn","shc","shelter","sieve","simpleledger","simplex","sip","sips","skype","smb","smp","sms","smtp","snmp","soap.beep","soap.beeps","soldat","spacify","spiffe","spotify","ssb","ssh","starknet","steam","stun","stuns","submit","svn","swh","swid","swidpath","tag","taler","teamspeak","teapot","teapots","tel","teliaeid","telnet","tftp","things","thismessage","tip","tn3270","tool","turn","turns","tv","udp","unreal","upn","urn","ut2004","uuid-in-package","v-event","vemmi","ventrilo","ves","view-source","vnc","vscode","vscode-insiders","vsls","w3","wasm","wasm-js","wcr","web3","webcal","wifi","ws","wss","wtai","wyciwyg","xcompute","xcon","xcon-userid","xfire","xftp","xmlrpc.beep","xmlrpc.beeps","xmpp","xrcp","xri","ymsgr","z39.50r","z39.50s","ztdnaid"]
@@ -3,7 +3,9 @@
3
3
  */
4
4
 
5
5
  /* numeric */
6
+ export const CHUNK_SIZE = 8 * 1024;
6
7
  export const HEX = 16;
8
+ export const MAX_BLOB_SIZE = 32 * 1024 * 1024;
7
9
  export const TYPE_FROM = 8;
8
10
  export const TYPE_TO = -1;
9
11
 
@@ -7,7 +7,9 @@ import { getType, isString } from './common.js';
7
7
  import { textChars } from './lib-util.js';
8
8
 
9
9
  /* constants */
10
- import { REG_CHARSET, REG_MIME_DOM, REG_MIME_TEXT } from './constant.js';
10
+ import {
11
+ CHUNK_SIZE, REG_CHARSET, REG_MIME_DOM, REG_MIME_TEXT
12
+ } from './constant.js';
11
13
  const DONE = 2;
12
14
  const EMPTY = 0;
13
15
  const LOADING = 1;
@@ -163,7 +165,11 @@ export class FileReader extends EventTarget {
163
165
  const mediaTypes = type ? type.split(';') : [];
164
166
  const buffer = await blob.arrayBuffer();
165
167
  const uint8arr = new Uint8Array(buffer);
166
- const binary = String.fromCharCode(...uint8arr);
168
+ const chunkSize = CHUNK_SIZE;
169
+ let binary = '';
170
+ for (let i = 0; i < uint8arr.length; i += chunkSize) {
171
+ binary += String.fromCharCode(...uint8arr.subarray(i, i + chunkSize));
172
+ }
167
173
  this._dispatchProgressEvent('loadstart');
168
174
  switch (format) {
169
175
  case 'arrayBuffer':
@@ -10,8 +10,8 @@ import {
10
10
  parseURLEncodedNumCharRef, URISchemes
11
11
  } from './uri-util.js';
12
12
  import {
13
- HEX, REG_DATA_URL, REG_DATA_URL_B64, REG_DATA_URL_G, REG_MIME_DOM,
14
- REG_SCRIPT_BLOB, REG_TAG_QUOT
13
+ HEX, MAX_BLOB_SIZE, REG_DATA_URL, REG_DATA_URL_B64, REG_DATA_URL_G,
14
+ REG_MIME_DOM, REG_SCRIPT_BLOB, REG_TAG_QUOT
15
15
  } from './constant.js';
16
16
 
17
17
  /* typedef */
@@ -36,6 +36,18 @@ import {
36
36
  * @property {string} [hash] - fragment identifier of the sanitized URL
37
37
  */
38
38
 
39
+ /**
40
+ * Internal debug logger.
41
+ * @param {boolean} isDebug - Debug flag.
42
+ * @param {string} message - Output message.
43
+ * @param {Error} [error] - Caught error object.
44
+ */
45
+ export const logDebug = (isDebug, message, error) => {
46
+ if (isDebug) {
47
+ console.warn(`[URLSanitizer Debug] ${message}`, error ? error.message : '');
48
+ }
49
+ };
50
+
39
51
  /**
40
52
  * URL sanitizer
41
53
  */
@@ -99,13 +111,26 @@ class URLSanitizer extends URISchemes {
99
111
  if (!isString(dom)) {
100
112
  throw new TypeError(`Expected String but got ${getType(dom)}.`);
101
113
  }
102
- let purifiedDom = domPurify.sanitize(decodeURIComponent(dom));
114
+ let decodedDom = dom;
115
+ try {
116
+ // prevent URIError caused by malformed percent-encoding
117
+ // e.g., a standalone '%'
118
+ decodedDom = decodeURIComponent(dom);
119
+ } catch (e) {
120
+ // fall through
121
+ }
122
+ let purifiedDom = domPurify.sanitize(decodedDom);
103
123
  if (purifiedDom && REG_DATA_URL.test(purifiedDom)) {
104
124
  purifiedDom = this.replace(purifiedDom);
105
125
  }
106
126
  purifiedDom = purifiedDom.replace(/(?:#|%23)$/, '')
107
127
  .replace(/(?<!(?:#|%23).*)(?:\?|%3F)$/, '');
108
- return encodeURI(purifiedDom);
128
+ try {
129
+ // prevent URIError caused by lone surrogates
130
+ return encodeURI(purifiedDom);
131
+ } catch (e) {
132
+ return purifiedDom;
133
+ }
109
134
  }
110
135
 
111
136
  /**
@@ -118,6 +143,8 @@ class URLSanitizer extends URISchemes {
118
143
  * @param {Array.<string>} [opt.allow] - array of allowed schemes
119
144
  * @param {Array.<string>} [opt.deny] - array of denied schemes
120
145
  * @param {Array.<string>} [opt.only] - array of specific schemes to allow
146
+ * @param {boolean} [opt.allowRelative] - allow relative URLs
147
+ * @param {boolean} [opt.debug] - enable debug mode
121
148
  * @returns {?string} - sanitized URL
122
149
  */
123
150
  sanitize(url, opt) {
@@ -125,7 +152,9 @@ class URLSanitizer extends URISchemes {
125
152
  this.#nest = 0;
126
153
  throw new Error('Data URLs nested too deeply.');
127
154
  }
128
- const { allow, deny, only } = opt ?? {};
155
+ const {
156
+ allow, deny, only, allowRelative = false, debug = false
157
+ } = opt ?? {};
129
158
  const schemeMap = new Map([
130
159
  ['blob', false],
131
160
  ['data', false],
@@ -151,7 +180,8 @@ class URLSanitizer extends URISchemes {
151
180
  try {
152
181
  super.add(item);
153
182
  } catch (e) {
154
- // fall through
183
+ const msg = `Failed to add scheme '${item}' in 'only' list.`;
184
+ logDebug(debug, msg, e);
155
185
  }
156
186
  if (super.has(item)) {
157
187
  schemeMap.set(item, true);
@@ -177,7 +207,8 @@ class URLSanitizer extends URISchemes {
177
207
  try {
178
208
  super.add(item);
179
209
  } catch (e) {
180
- // fall through
210
+ const msg = `Failed to add scheme '${item}' in 'allow' list.`;
211
+ logDebug(debug, msg, e);
181
212
  }
182
213
  if (super.has(item)) {
183
214
  schemeMap.set(item, true);
@@ -201,25 +232,47 @@ class URLSanitizer extends URISchemes {
201
232
  }
202
233
  }
203
234
  let sanitizedUrl;
204
- if (super.verify(url)) {
205
- const { hash, href, pathname, protocol, search } = new URL(url);
206
- const scheme = protocol.replace(/:$/, '');
207
- const schemeParts = scheme.split('+');
208
- let bool;
209
- if (restrictScheme) {
210
- bool = schemeParts.every(s => schemeMap.get(s));
235
+ let isVerified = super.verify(url);
236
+ let isRelative = false;
237
+ if (!isVerified && allowRelative) {
238
+ try {
239
+ const { hostname, protocol } = new URL(url, 'http://dummy.local');
240
+ if (protocol === 'http:' && hostname === 'dummy.local') {
241
+ isVerified = true;
242
+ isRelative = true;
243
+ }
244
+ } catch (e) {
245
+ logDebug(debug, 'Failed to parse relative URL.', e);
246
+ }
247
+ }
248
+ if (isVerified) {
249
+ let hash, href, pathname, protocol, search;
250
+ let scheme = '';
251
+ let schemeParts = [];
252
+ let bool = false;
253
+ if (isRelative) {
254
+ bool = true;
211
255
  } else {
212
- for (const [key, value] of schemeMap.entries()) {
213
- bool = value || (scheme !== key && schemeParts.every(s => s !== key));
214
- if (!bool) {
215
- break;
256
+ const urlObj = new URL(url);
257
+ ({ hash, href, pathname, protocol, search } = urlObj);
258
+ scheme = protocol.replace(/:$/, '');
259
+ schemeParts = scheme.split('+');
260
+ if (restrictScheme) {
261
+ bool = schemeParts.every(s => schemeMap.get(s));
262
+ } else {
263
+ for (const [key, value] of schemeMap.entries()) {
264
+ bool =
265
+ value || (scheme !== key && schemeParts.every(s => s !== key));
266
+ if (!bool) {
267
+ break;
268
+ }
216
269
  }
217
270
  }
218
271
  }
219
272
  if (bool) {
220
- const isDataUrl = schemeParts.includes('data');
273
+ const isDataUrl = isRelative ? false : schemeParts.includes('data');
221
274
  let finalize;
222
- let urlToSanitize = href;
275
+ let urlToSanitize = isRelative ? url : href;
223
276
  if (isDataUrl) {
224
277
  const [mediaType, ...dataParts] = pathname.split(',');
225
278
  const data = `${dataParts.join(',')}${search}${hash}`;
@@ -237,7 +290,8 @@ class URLSanitizer extends URISchemes {
237
290
  urlToSanitize = '';
238
291
  }
239
292
  } catch (e) {
240
- // fall through
293
+ const msg = 'Failed to parse inner data URL protocol.';
294
+ logDebug(debug, msg, e);
241
295
  }
242
296
  const containsDataUrl = REG_DATA_URL.test(parsedData);
243
297
  if (parsedData !== data || containsDataUrl) {
@@ -383,8 +437,15 @@ const urlSanitizer = new URLSanitizer();
383
437
  export const sanitizeURL = async (url, opt = {
384
438
  allow: [],
385
439
  deny: [],
386
- only: []
440
+ only: [],
441
+ allowRelative: false,
442
+ debug: false,
443
+ maxBlobSize: MAX_BLOB_SIZE
387
444
  }) => {
445
+ const isDebug = !!opt?.debug;
446
+ const maxBlobSize = Number.isInteger(opt?.maxBlobSize) && opt.maxBlobSize > 0
447
+ ? opt.maxBlobSize
448
+ : MAX_BLOB_SIZE;
388
449
  let res;
389
450
  if (url && isString(url)) {
390
451
  let scheme;
@@ -392,7 +453,7 @@ export const sanitizeURL = async (url, opt = {
392
453
  const { protocol } = new URL(url);
393
454
  scheme = protocol.replace(/:$/, '');
394
455
  } catch (e) {
395
- // fall through;
456
+ logDebug(isDebug, `Invalid URL input format: ${url}`, e);
396
457
  }
397
458
  if (scheme === 'blob') {
398
459
  const { allow, deny, only } = opt;
@@ -401,10 +462,12 @@ export const sanitizeURL = async (url, opt = {
401
462
  (Array.isArray(only) && only.includes('blob'))) {
402
463
  let data;
403
464
  try {
404
- data =
405
- await fetch(url).then(r => r.blob()).then(createDataURLFromBlob);
465
+ data = await fetch(url)
466
+ .then(r => r.blob())
467
+ .then(b => createDataURLFromBlob(b, maxBlobSize));
406
468
  } catch (e) {
407
- // fall through
469
+ const msg = `Failed to fetch and convert blob URL: ${url}`;
470
+ logDebug(isDebug, msg, e);
408
471
  }
409
472
  if (data) {
410
473
  if (Array.isArray(only)) {
@@ -424,7 +487,7 @@ export const sanitizeURL = async (url, opt = {
424
487
  }
425
488
  }
426
489
  URL.revokeObjectURL(url);
427
- } else if (scheme) {
490
+ } else if (scheme || opt.allowRelative) {
428
491
  res = urlSanitizer.sanitize(url, opt);
429
492
  }
430
493
  }
@@ -450,11 +513,12 @@ export const sanitizeURLSync = (url, opt) => {
450
513
  const { protocol } = new URL(url);
451
514
  scheme = protocol.replace(/:$/, '');
452
515
  } catch (e) {
453
- // fall through;
516
+ const msg = `Failed to parse URL: ${url}`;
517
+ logDebug(opt?.debug, msg, e);
454
518
  }
455
519
  if (scheme === 'blob') {
456
520
  URL.revokeObjectURL(url);
457
- } else if (scheme) {
521
+ } else if (scheme || opt?.allowRelative) {
458
522
  res = urlSanitizer.sanitize(url, opt);
459
523
  }
460
524
  }
@@ -8,7 +8,8 @@ import { FileReader } from './file-reader.js';
8
8
  import { textChars, uriSchemes } from './lib-util.js';
9
9
 
10
10
  import {
11
- HEX, REG_B64, REG_NUM_REF, REG_SCHEME, REG_SCHEME_EXT, REG_SCRIPT, REG_URL_ENC
11
+ HEX, MAX_BLOB_SIZE, REG_B64, REG_NUM_REF, REG_SCHEME, REG_SCHEME_EXT,
12
+ REG_SCRIPT, REG_URL_ENC
12
13
  } from './constant.js';
13
14
 
14
15
  /**
@@ -128,15 +129,25 @@ export const parseURLEncodedNumCharRef = (str, nest = 0) => {
128
129
  /**
129
130
  * create data URL from blob
130
131
  * @param {object} blob - blob
132
+ * @param {number} maxBlobSize - max blob size
131
133
  * @returns {Promise.<?string>} - data URL
132
134
  */
133
- export const createDataURLFromBlob = blob => new Promise((resolve, reject) => {
134
- const reader = new FileReader();
135
- reader.addEventListener('error', () => reject(reader.error));
136
- reader.addEventListener('abort', () => resolve(reader.result));
137
- reader.addEventListener('load', () => resolve(reader.result));
138
- reader.readAsDataURL(blob);
139
- });
135
+ export const createDataURLFromBlob = (blob, maxBlobSize = MAX_BLOB_SIZE) =>
136
+ new Promise((resolve, reject) => {
137
+ if (!Number.isInteger(blob?.size)) {
138
+ return resolve(null);
139
+ } else if (Number.isInteger(maxBlobSize) && blob.size > maxBlobSize) {
140
+ return reject(new DOMException(
141
+ `Blob size (${blob.size} bytes) exceeds the maximum allowed size of ${maxBlobSize} bytes.`,
142
+ 'NotReadableError'
143
+ ));
144
+ }
145
+ const reader = new FileReader();
146
+ reader.addEventListener('error', () => reject(reader.error));
147
+ reader.addEventListener('abort', () => resolve(reader.result));
148
+ reader.addEventListener('load', () => resolve(reader.result));
149
+ reader.readAsDataURL(blob);
150
+ });
140
151
 
141
152
  /**
142
153
  * URI schemes
@@ -1,4 +1,6 @@
1
+ export const CHUNK_SIZE: number;
1
2
  export const HEX: 16;
3
+ export const MAX_BLOB_SIZE: number;
2
4
  export const TYPE_FROM: 8;
3
5
  export const TYPE_TO: -1;
4
6
  export const REG_B64: RegExp;
@@ -1,3 +1,4 @@
1
+ export function logDebug(isDebug: boolean, message: string, error?: Error): void;
1
2
  export function sanitizeURL(url: string, opt?: {
2
3
  allow?: string[] | undefined;
3
4
  deny?: string[] | undefined;
@@ -40,6 +41,8 @@ export class URLSanitizer extends URISchemes {
40
41
  allow?: string[] | undefined;
41
42
  deny?: string[] | undefined;
42
43
  only?: string[] | undefined;
44
+ allowRelative?: boolean | undefined;
45
+ debug?: boolean | undefined;
43
46
  }): string | null;
44
47
  parse(url: string, opt?: {
45
48
  allow?: string[] | undefined;
@@ -2,7 +2,7 @@ export function getURLEncodedString(str: string): string;
2
2
  export function escapeURLEncodedHTMLChars(ch: string): string;
3
3
  export function parseBase64(data: string): string;
4
4
  export function parseURLEncodedNumCharRef(str: string, nest?: number): string;
5
- export function createDataURLFromBlob(blob: object): Promise<string | null>;
5
+ export function createDataURLFromBlob(blob: object, maxBlobSize?: number): Promise<string | null>;
6
6
  export class URISchemes {
7
7
  get(): Array<string>;
8
8
  has(scheme: string): boolean;