url-sanitizer 0.4.3 → 0.5.1

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 CHANGED
@@ -8,6 +8,7 @@
8
8
  URL sanitizer for Node.js (>=18), browsers and web sites.
9
9
  **Experimental**
10
10
 
11
+
11
12
  ## Install
12
13
 
13
14
  ```shell
@@ -20,14 +21,16 @@ For browsers and web sites, standalone ESM builds are available in `dist/` direc
20
21
 
21
22
  Or, download them from [Releases](https://github.com/asamuzaK/urlSanitizer/releases).
22
23
 
24
+
23
25
  ## Usage
24
26
 
25
27
  ```javascript
26
28
  import urlSanitizer, {
27
- isURI, isURISync, sanitizeURL, sanitizeURLSync
29
+ isURI, isURISync, parseURL, parseURLSync, sanitizeURL, sanitizeURLSync
28
30
  } from 'url-sanitizer';
29
31
  ```
30
32
 
33
+
31
34
  ## sanitizeURL(url, opt)
32
35
 
33
36
  Sanitize the given URL.
@@ -35,69 +38,198 @@ Sanitize the given URL.
35
38
 
36
39
  ### Parameters
37
40
 
38
- * `url` **[string][1]** URL input
39
- * `opt` **[object][3]** Options
40
- * `opt.allow` **[Array][4]<[string][1]>** Array of allowed schemes, e.g. `['data']`
41
- * `opt.deny` **[Array][4]<[string][1]>** Array of denied schemes, e.g. `['web+foo']`
41
+ * `url` **[string][1]** URL input.
42
+ * `opt` **[object][3]** Options.
43
+ * `opt.allow` **[Array][4]<[string][1]>** Array of allowed schemes, e.g. `['data']`.
44
+ * `opt.deny` **[Array][4]<[string][1]>** Array of denied schemes, e.g. `['web+foo']`.
45
+ * `opt.only` **[Array][4]<[string][1]>** Array of specific schemes to allow, e.g. `['git', 'https']`.
46
+ `only` takes precedence over `allow` and `deny`.
42
47
 
43
- Returns **[Promise][5]<[string][1]?>** Sanitized URL, `null`able
48
+ Returns **[Promise][5]<[string][1]?>** Sanitized URL, `null`able.
44
49
 
45
50
  ```javascript
46
51
  const res1 = await sanitizeURL('http://example.com/?<script>alert(1);</script>')
47
52
  .then(res => decodeURIComponent(res));
48
- // -> 'http://example.com/?&lt;script&gt;alert(1);&lt;/script&gt;'
53
+ // => 'http://example.com/?&lt;script&gt;alert(1);&lt;/script&gt;'
49
54
 
50
55
  const res2 = await sanitizeURL('data:text/html,<script>alert(1);</script>', {
51
56
  allow: ['data']
52
57
  }).then(res => decodeURIComponent(res));
53
- // -> 'data:text/html,&lt;script&gt;alert(1);&lt;/script&gt;'
58
+ // => 'data:text/html,&lt;script&gt;alert(1);&lt;/script&gt;'
54
59
 
55
60
  // Can parse and sanitize base64 encoded data
56
61
  const base64data3 = btoa('<script>alert(1);</script>');
57
62
  const res3 = await sanitizeURL(`data:text/html;base64,${base64data3}`, {
58
63
  allow: ['data']
59
64
  }).then(res => decodeURIComponent(res));
60
- // -> 'data:text/html,&lt;script&gt;alert(1);&lt;/script&gt;'
65
+ // => 'data:text/html,&lt;script&gt;alert(1);&lt;/script&gt;'
61
66
 
62
67
  const res4 = await sanitizeURL('web+foo://example.com', {
63
68
  deny: ['web+foo']
64
69
  });
65
- // -> null
70
+ // => null
71
+
72
+ const res5 = await sanitizeURL('http://example.com', {
73
+ only: ['data', 'git', 'https']
74
+ });
75
+ // => null
76
+
77
+ const res6 = await sanitizeURL('https://example.com/"onmouseover="alert(1)"', {
78
+ only: ['data', 'git', 'https']
79
+ }).then(res => decodeURIComponent(res));
80
+ // => https://example.com/&quot;onmouseover=&quot;alert(1)&quot;
81
+
82
+ const res7 = await sanitizeURL('data:text/html,<script>alert(1);</script>', {
83
+ only: ['data', 'git', 'https']
84
+ }).then(res => decodeURIComponent(res));
85
+ // => 'data:text/html,&lt;script&gt;alert(1);&lt;/script&gt;'
86
+
87
+ // `only` option also allows combinations of the specified schemes
88
+ const res8 = await sanitizeURL('git+https://example.com', {
89
+ only: ['data', 'git', 'https']
90
+ }).then(res => decodeURIComponent(res));;
91
+ // => git+https://example.com
66
92
  ```
67
93
 
94
+
68
95
  ## sanitizeURLSync
69
96
 
70
97
  Synchronous version of the [sanitizeURL()](#sanitizeurlurl-opt).
71
98
 
99
+
100
+ ## parseURL(url)
101
+
102
+ Parse the given URL.
103
+
104
+ ### Parameters
105
+
106
+ * `url` **[string][1]** URL input.
107
+
108
+ Returns **[Promise][5]<[ParsedURL](#parsedurl)>** Result.
109
+
110
+ ### ParsedURL
111
+
112
+ Object with extended properties based on [URL](https://developer.mozilla.org/ja/docs/Web/API/URL) API.
113
+
114
+ Type: [object][3]
115
+
116
+ #### Properties
117
+
118
+ * `input` **[string][1]** URL input.
119
+ * `valid` **[boolean][2]** Is valid URI.
120
+ * `data` **[object][3]** Parsed result of data URL, `null`able.
121
+ * `data.mime` **[string][1]** MIME type.
122
+ * `data.base64` **[boolean][2]** `true` if base64 encoded.
123
+ * `data.data` **[string][1]** Data part of the data URL.
124
+ * `href` **[string][1]** Same as URL API.
125
+ * `origin` **[string][1]** Same as URL API.
126
+ * `protocol` **[string][1]** Same as URL API.
127
+ * `username` **[string][1]** Same as URL API.
128
+ * `password` **[string][1]** Same as URL API.
129
+ * `host` **[string][1]** Same as URL API.
130
+ * `hostname` **[string][1]** Same as URL API.
131
+ * `port` **[string][1]** Same as URL API.
132
+ * `pathname` **[string][1]** Same as URL API.
133
+ * `search` **[string][1]** Same as URL API.
134
+ * `searchParams` **[object][3]** Same as URL API.
135
+ * `hash` **[string][1]** Same as URL API.
136
+
137
+ ```javascript
138
+ const res1 = await parseURL('javascript:alert(1)');
139
+ /* => {
140
+ input: 'javascript:alert(1)',
141
+ valid: false
142
+ } */
143
+
144
+ const res2 = await parseURL('https://example.com/?foo=bar#baz');
145
+ /* => {
146
+ input: 'https://www.example.com/?foo=bar#baz',
147
+ valid: true,
148
+ data: null,
149
+ href: 'https://www.example.com/?foo=bar#baz',
150
+ origin: 'https://www.example.com',
151
+ protocol: 'https:',
152
+ hostname: 'www.example.com',
153
+ pathname: '/',
154
+ search: '?foo=bar',
155
+ hash: '#baz',
156
+ ...
157
+ } */
158
+
159
+ // base64 encoded svg '<svg><g onload="alert(1)"/></svg>'
160
+ const res3 = await parseURL('data:image/svg+xml;base64,PHN2Zz48ZyBvbmxvYWQ9ImFsZXJ0KDEpIi8+PC9zdmc+');
161
+ /* => {
162
+ input: 'data:image/svg+xml;base64,PHN2Zz48ZyBvbmxvYWQ9ImFsZXJ0KDEpIi8+PC9zdmc+',
163
+ valid: true,
164
+ data: {
165
+ mime: 'image/svg+xml',
166
+ base64: false,
167
+ data: '%26lt;svg%26gt;%26lt;g%20onload=%26quot;alert(1)%26quot;/%26gt;%26lt;/svg%26gt;'
168
+ },
169
+ href: 'data:image/svg+xml,%26lt;svg%26gt;%26lt;g%20onload=%26quot;alert(1)%26quot;/%26gt;%26lt;/svg%26gt;',
170
+ protocol: 'data:',
171
+ pathname: 'image/svg+xml,%26lt;svg%26gt;%26lt;g%20onload=%26quot;alert(1)%26quot;/%26gt;%26lt;/svg%26gt;',
172
+ ...
173
+ } */
174
+
175
+ // base64 encoded png
176
+ const res4 = await parseURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==');
177
+ /* => {
178
+ input: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
179
+ valid: true,
180
+ data: {
181
+ mime: 'image/png',
182
+ base64: true,
183
+ data: 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='
184
+ },
185
+ href: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
186
+ protocol: 'data:',
187
+ pathname: 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
188
+ ...
189
+ } */
190
+ ```
191
+
192
+
193
+ ## parseURLSync(url)
194
+
195
+ Synchronous version of the [parseURL()](#parseURL).
196
+
197
+
72
198
  ## isURI(uri)
73
199
 
74
200
  Determines whether the given URI is valid.
75
201
 
76
202
  ### Parameters
77
203
 
78
- * `uri` **[string][1]** URI input
204
+ * `uri` **[string][1]** URI input.
79
205
 
80
- Returns **[Promise][5]<[boolean][2]>** Result
81
- * Always `true` for `web+*` and `ext+*` schemes
206
+ Returns **[Promise][5]<[boolean][2]>** Result.
207
+ * Always `true` for `web+*` and `ext+*` schemes, except `web+javascript`, `web+vbscript`, `ext+javascript`, `ext+vbscript`.
82
208
 
83
209
  ```javascript
84
210
  const res1 = await isURI('https://example.com/foo');
85
- // -> true
211
+ // => true
86
212
 
87
213
  const res2 = await isURI('mailto:foo@example.com');
88
- // -> true
214
+ // => true
89
215
 
90
216
  const res3 = await isURI('foo:bar');
91
- // -> false
217
+ // => false
92
218
 
93
219
  const res4 = await isURI('web+foo:bar');
94
- // -> true
220
+ // => true
221
+
222
+ const res5 = await isURI('web+javascript:alert(1)');
223
+ // => false
95
224
  ```
96
225
 
226
+
97
227
  ## isURISync(uri)
98
228
 
99
229
  Synchronous version of the [isURI()](#isuriuri).
100
230
 
231
+ ---
232
+
101
233
  ## urlSanitizer
102
234
 
103
235
  Instance of the sanitizer.
@@ -108,11 +240,11 @@ Get an array of URI schemes registered at [iana.org](https://www.iana.org/assign
108
240
  * Historical schemes omitted.
109
241
  * `moz-extension` scheme added.
110
242
 
111
- Returns **[Array][4]<[string][1]>** Array of registered URI schemes
243
+ Returns **[Array][4]<[string][1]>** Array of registered URI schemes.
112
244
 
113
245
  ```javascript
114
246
  const schemes = urlSanitizer.get();
115
- // -> ['aaa', 'aaas', 'about', 'acap', 'acct', 'acd', 'acr', ...];
247
+ // => ['aaa', 'aaas', 'about', 'acap', 'acct', ...];
116
248
  ```
117
249
 
118
250
  ### urlSanitizer.has(scheme)
@@ -121,16 +253,16 @@ Check if the given scheme is registered.
121
253
 
122
254
  #### Parameters
123
255
 
124
- * `scheme` **[string][1]** Scheme
256
+ * `scheme` **[string][1]** Scheme.
125
257
 
126
- Returns **[boolean][2]** Result
258
+ Returns **[boolean][2]** Result.
127
259
 
128
260
  ```javascript
129
261
  const res1 = urlSanitizer.has('https');
130
- // -> true
262
+ // => true
131
263
 
132
264
  const res2 = urlSanitizer.has('foo');
133
- // -> false
265
+ // => false
134
266
  ```
135
267
 
136
268
  ### urlSanitizer.add(scheme)
@@ -140,19 +272,19 @@ Add a scheme to the list of URI schemes.
140
272
 
141
273
  #### Parameters
142
274
 
143
- * `scheme` **[string][1]** Scheme
275
+ * `scheme` **[string][1]** Scheme.
144
276
 
145
- Returns **[Array][4]<[string][1]>** Array of registered URI schemes
277
+ Returns **[Array][4]<[string][1]>** Array of registered URI schemes.
146
278
 
147
279
  ```javascript
148
280
  console.log(isURISync('foo'));
149
- // -> false;
281
+ // => false;
150
282
 
151
283
  const res = urlSanitizer.add('foo');
152
- // -> ['aaa', 'aaas', 'about', 'acap', 'acct', 'acd', ... 'foo', ...];
284
+ // => ['aaa', 'aaas', 'about', 'acap', ... 'foo', ...];
153
285
 
154
286
  console.log(isURISync('foo'));
155
- // -> true;
287
+ // => true;
156
288
  ```
157
289
 
158
290
  ### urlSanitizer.remove(scheme)
@@ -161,23 +293,23 @@ Remove a scheme from the list of URI schemes.
161
293
 
162
294
  #### Parameters
163
295
 
164
- * `scheme` **[string][1]** Scheme
296
+ * `scheme` **[string][1]** Scheme.
165
297
 
166
- Returns **[boolean][2]** Result
298
+ Returns **[boolean][2]** Result.
167
299
  * `true` if the scheme is successfully removed, `false` otherwise.
168
300
 
169
301
  ```javascript
170
302
  console.log(isURISync('aaa'));
171
- // -> true;
303
+ // => true;
172
304
 
173
305
  const res1 = urlSanitizer.remove('aaa');
174
- // -> true
306
+ // => true
175
307
 
176
308
  console.log(isURISync('aaa'));
177
- // -> false;
309
+ // => false;
178
310
 
179
311
  const res2 = urlSanitizer.remove('foo');
180
- // -> false
312
+ // => false
181
313
  ```
182
314
 
183
315
  [1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
@@ -591,14 +591,17 @@ var uri_schemes_default = [
591
591
 
592
592
  // src/mjs/uri-util.js
593
593
  var HEX = 16;
594
- var REG_CHARS = /[<>"'\s]/g;
594
+ var REG_BASE64 = /^[\da-z+/\-_=]+$/i;
595
595
  var REG_DATA_URL = /data:[^,]*,[^"]+/g;
596
- var REG_DATA_URL_BASE64 = /data:[^,]*;?base64,[\dA-Za-z+/\-_=]+/;
597
- var REG_DATA_URL_HEADER = /data:[^,]*,/;
596
+ var REG_DATA_URL_BASE64 = /data:[^,]*;?base64,[\da-z+/\-_=]+/i;
597
+ var REG_HTML_SP = /[<>"'\s]/g;
598
+ var REG_HTML_SP_URL_ENC = /%(?:2(?:2|7)|3(?:C|E))/g;
598
599
  var REG_NUM_REF = /&#(x(?:00)?[\dA-F]{2}|0?\d{1,3});?/ig;
599
- var REG_SCHEME = /^[a-z][a-z0-9+\-.]*$/;
600
+ var REG_SCHEME = /^[a-z][\da-z+\-.]*$/;
600
601
  var REG_SCHEME_CUSTOM = /^(?:ext|web)\+[a-z]+$/;
601
602
  var REG_SCRIPT = /(?:java|vb)script/;
603
+ var REG_URL_ENC = /^%[\dA-F]{2}$/i;
604
+ var REG_URL_ENC_AMP = /%26/g;
602
605
  var getUrlEncodedString = (str) => {
603
606
  if (!isString(str)) {
604
607
  throw new TypeError(`Expected String but got ${getType(str)}.`);
@@ -611,7 +614,7 @@ var getUrlEncodedString = (str) => {
611
614
  };
612
615
  var escapeUrlEncodedHtmlChars = (ch) => {
613
616
  if (isString(ch)) {
614
- if (/^%[\dA-F]{2}$/i.test(ch)) {
617
+ if (REG_URL_ENC.test(ch)) {
615
618
  ch = ch.toUpperCase();
616
619
  } else {
617
620
  throw new Error(`Invalid URL encoded character: ${ch}`);
@@ -639,14 +642,14 @@ var escapeUrlEncodedHtmlChars = (ch) => {
639
642
  var parseBase64 = (data) => {
640
643
  if (!isString(data)) {
641
644
  throw new TypeError(`Expected String but got ${getType(data)}.`);
642
- } else if (!/^[\dA-Za-z+/\-_=]+$/.test(data)) {
645
+ } else if (!REG_BASE64.test(data)) {
643
646
  throw new Error(`Invalid base64 data: ${data}`);
644
647
  }
645
648
  const bin = atob(data);
646
649
  const uint8arr = Uint8Array.from([...bin].map((c) => c.charCodeAt(0)));
647
- const textChars = new Set(text_chars_default);
650
+ const textCharCodes = new Set(text_chars_default);
648
651
  let parsedData;
649
- if (uint8arr.every((c) => textChars.has(c))) {
652
+ if (uint8arr.every((c) => textCharCodes.has(c))) {
650
653
  parsedData = bin.replace(/\s/g, getUrlEncodedString);
651
654
  } else {
652
655
  parsedData = data;
@@ -664,7 +667,7 @@ var parseUrlEncodedNumCharRef = (str, nest = 0) => {
664
667
  }
665
668
  let res = decodeURIComponent(str);
666
669
  if (/&#/.test(res)) {
667
- const textChars = new Set(text_chars_default);
670
+ const textCharCodes = new Set(text_chars_default);
668
671
  const items = [...res.matchAll(REG_NUM_REF)].reverse();
669
672
  for (const item of items) {
670
673
  const [numCharRef, value] = item;
@@ -680,9 +683,9 @@ var parseUrlEncodedNumCharRef = (str, nest = 0) => {
680
683
  res.substring(0, index),
681
684
  res.substring(index + numCharRef.length)
682
685
  ];
683
- if (textChars.has(num)) {
686
+ if (textCharCodes.has(num)) {
684
687
  res = `${preNum}${String.fromCharCode(num)}${postNum}`;
685
- if (/#x?$/.test(preNum) || /^#x?[\d]+/.test(postNum)) {
688
+ if (/#x?$/.test(preNum) || /^#(?:x(?:00)?[2-7]|\d)/.test(postNum)) {
686
689
  res = parseUrlEncodedNumCharRef(res, ++nest);
687
690
  }
688
691
  } else if (num < HEX * HEX) {
@@ -789,16 +792,17 @@ var URLSanitizer = class extends URISchemes {
789
792
  * @param {object} opt - options
790
793
  * @param {Array.<string>} opt.allow - array of allowed schemes
791
794
  * @param {Array.<string>} opt.deny - array of denied schemes
795
+ * @param {Array.<string>} opt.only - array of specific schemes to allow
792
796
  * @returns {?string} - sanitized URL
793
797
  */
794
- sanitize(url, opt = { allow: [], deny: [] }) {
798
+ sanitize(url, opt = { allow: [], deny: [], only: [] }) {
795
799
  if (this.#nest > HEX) {
796
800
  this.#nest = 0;
797
801
  throw new Error("Data URLs nested too deeply.");
798
802
  }
799
803
  let sanitizedUrl;
800
804
  if (super.isURI(url)) {
801
- const { allow, deny } = opt ?? {};
805
+ const { allow, deny, only } = opt ?? {};
802
806
  const { hash, href, pathname, protocol, search } = new URL(url);
803
807
  const scheme = protocol.replace(/:$/, "");
804
808
  const schemeParts = scheme.split("+");
@@ -808,8 +812,12 @@ var URLSanitizer = class extends URISchemes {
808
812
  ["javascrpt", false],
809
813
  ["vbscript", false]
810
814
  ]);
811
- if (Array.isArray(allow) && allow.length) {
812
- const items = Object.values(allow);
815
+ if (Array.isArray(only) && only.length) {
816
+ const schemes = super.get();
817
+ for (const item of schemes) {
818
+ schemeMap.set(item, false);
819
+ }
820
+ const items = Object.values(only);
813
821
  for (let item of items) {
814
822
  if (isString(item)) {
815
823
  item = item.trim();
@@ -818,14 +826,26 @@ var URLSanitizer = class extends URISchemes {
818
826
  }
819
827
  }
820
828
  }
821
- }
822
- if (Array.isArray(deny) && deny.length) {
823
- const items = Object.values(deny);
824
- for (let item of items) {
825
- if (isString(item)) {
826
- item = item.trim();
827
- if (item) {
828
- schemeMap.set(item, false);
829
+ } else {
830
+ if (Array.isArray(allow) && allow.length) {
831
+ const items = Object.values(allow);
832
+ for (let item of items) {
833
+ if (isString(item)) {
834
+ item = item.trim();
835
+ if (!REG_SCRIPT.test(item)) {
836
+ schemeMap.set(item, true);
837
+ }
838
+ }
839
+ }
840
+ }
841
+ if (Array.isArray(deny) && deny.length) {
842
+ const items = Object.values(deny);
843
+ for (let item of items) {
844
+ if (isString(item)) {
845
+ item = item.trim();
846
+ if (item) {
847
+ schemeMap.set(item, false);
848
+ }
829
849
  }
830
850
  }
831
851
  }
@@ -838,15 +858,12 @@ var URLSanitizer = class extends URISchemes {
838
858
  }
839
859
  }
840
860
  if (bool) {
841
- const [amp, lt, gt, quot, apos] = ["&", "<", ">", '"', "'"].map(getUrlEncodedString);
842
- const regAmp = new RegExp(amp, "g");
843
- const regEncodedChars = new RegExp(`(${lt}|${gt}|${quot}|${apos})`, "g");
844
861
  let escapeHtml;
845
862
  let urlToSanitize = href;
846
863
  if (schemeParts.includes("data")) {
847
- const [header, ...body] = pathname.split(",");
864
+ const [head, ...body] = pathname.split(",");
848
865
  const data = `${body.join(",")}${search}${hash}`;
849
- const mediaType = header.split(";");
866
+ const mediaType = head.split(";");
850
867
  let parsedData = data;
851
868
  if (mediaType[mediaType.length - 1] === "base64") {
852
869
  mediaType.pop();
@@ -862,7 +879,7 @@ var URLSanitizer = class extends URISchemes {
862
879
  } catch (e) {
863
880
  }
864
881
  }
865
- const containsDataUrl = REG_DATA_URL_HEADER.test(parsedData);
882
+ const containsDataUrl = /data:[^,]*,/.test(parsedData);
866
883
  if (parsedData !== data || containsDataUrl) {
867
884
  if (containsDataUrl) {
868
885
  const matchedDataUrls = parsedData.matchAll(REG_DATA_URL);
@@ -906,9 +923,9 @@ var URLSanitizer = class extends URISchemes {
906
923
  escapeHtml = true;
907
924
  }
908
925
  if (urlToSanitize) {
909
- sanitizedUrl = urlToSanitize.replace(REG_CHARS, getUrlEncodedString).replace(regAmp, escapeUrlEncodedHtmlChars);
926
+ sanitizedUrl = urlToSanitize.replace(REG_HTML_SP, getUrlEncodedString).replace(REG_URL_ENC_AMP, escapeUrlEncodedHtmlChars);
910
927
  if (escapeHtml) {
911
- sanitizedUrl = sanitizedUrl.replace(regEncodedChars, escapeUrlEncodedHtmlChars);
928
+ sanitizedUrl = sanitizedUrl.replace(REG_HTML_SP_URL_ENC, escapeUrlEncodedHtmlChars);
912
929
  this.#nest = 0;
913
930
  }
914
931
  } else {
@@ -919,6 +936,77 @@ var URLSanitizer = class extends URISchemes {
919
936
  }
920
937
  return sanitizedUrl || null;
921
938
  }
939
+ /**
940
+ * object with extended props based on URL API
941
+ *
942
+ * @typedef {object} ParsedURL
943
+ * @property {string} input - URL input
944
+ * @property {boolean} valid - is valid URI
945
+ * @property {object} data - parsed result of data URL, `null`able
946
+ * @property {string} data.mime - MIME type
947
+ * @property {boolean} data.base64 - `true` if base64 encoded
948
+ * @property {string} data.data - data part of the data URL
949
+ * @property {string} href - same as URL API
950
+ * @property {string} origin - same as URL API
951
+ * @property {string} protocol - same as URL API
952
+ * @property {string} username - same as URL API
953
+ * @property {string} password - same as URL API
954
+ * @property {string} host - same as URL API
955
+ * @property {string} hostname - same as URL API
956
+ * @property {string} port - same as URL API
957
+ * @property {string} pathname - same as URL API
958
+ * @property {string} search - same as URL API
959
+ * @property {object} searchParams - same as URL API
960
+ * @property {string} hash - same as URL API
961
+ */
962
+ /**
963
+ * parse sanitized URL
964
+ *
965
+ * @param {string} url - URL input
966
+ * @returns {ParsedURL} - result with extended props based on URL API
967
+ */
968
+ parse(url) {
969
+ if (!isString(url)) {
970
+ throw new TypeError(`Expected String but got ${getType(url)}.`);
971
+ }
972
+ const sanitizedUrl = this.sanitize(url, {
973
+ allow: ["data", "file"]
974
+ });
975
+ const parsedUrl = /* @__PURE__ */ new Map([
976
+ ["input", url]
977
+ ]);
978
+ if (sanitizedUrl) {
979
+ const urlObj = new URL(sanitizedUrl);
980
+ const { pathname, protocol } = urlObj;
981
+ const schemeParts = protocol.replace(/:$/, "").split("+");
982
+ parsedUrl.set("valid", true);
983
+ if (schemeParts.includes("data")) {
984
+ const dataUrl = /* @__PURE__ */ new Map();
985
+ const [head, ...body] = pathname.split(",");
986
+ const data = `${body.join(",")}`;
987
+ const mediaType = head.split(";");
988
+ const isBase64 = mediaType[mediaType.length - 1] === "base64";
989
+ if (isBase64) {
990
+ mediaType.pop();
991
+ }
992
+ dataUrl.set("mime", mediaType.join(";"));
993
+ dataUrl.set("base64", isBase64);
994
+ dataUrl.set("data", data);
995
+ parsedUrl.set("data", Object.fromEntries(dataUrl));
996
+ } else {
997
+ parsedUrl.set("data", null);
998
+ }
999
+ for (const key in urlObj) {
1000
+ const value = urlObj[key];
1001
+ if (typeof value !== "function") {
1002
+ parsedUrl.set(key, value);
1003
+ }
1004
+ }
1005
+ } else {
1006
+ parsedUrl.set("valid", false);
1007
+ }
1008
+ return Object.fromEntries(parsedUrl);
1009
+ }
922
1010
  };
923
1011
  var urlSanitizer = new URLSanitizer();
924
1012
  var isUri = (uri) => urlSanitizer.isURI(uri);
@@ -928,7 +1016,8 @@ var isURI = async (uri) => {
928
1016
  };
929
1017
  var sanitizeUrl = (url, opt) => urlSanitizer.sanitize(url, opt ?? {
930
1018
  allow: [],
931
- deny: []
1019
+ deny: [],
1020
+ only: []
932
1021
  });
933
1022
  var sanitizeURL = async (url, opt) => {
934
1023
  const res = await sanitizeUrl(url, opt);