Repository: sindresorhus/normalize-url
Pull request #97 (2019-09-20 10:30:58)

Fix data URL should not support query strings

fisker: ready

Patch:
diff --git a/index.js b/index.js
index 20ab9de..4c99af1 100644
--- a/index.js
+++ b/index.js
@@ -6,8 +6,8 @@ const testParameter = (name, filters) => {
 	return filters.some(filter => filter instanceof RegExp ? filter.test(name) : filter === name);
 };
 
-const normalizeDataURL = urlString => {
-	const parts = urlString.trim().match(/^data:(.*?),(.*)$/);
+const normalizeDataURL = (urlString, {stripHash}) => {
+	const parts = urlString.match(/^data:(.*?),(.*?)(?:#(.*))?$/);
 
 	if (!parts) {
 		throw new Error(`Invalid URL: ${urlString}`);
@@ -15,6 +15,7 @@ const normalizeDataURL = urlString => {
 
 	const mediaType = parts[1].split(';');
 	const body = parts[2];
+	const hash = stripHash ? '' : parts[3];
 
 	let base64 = false;
 
@@ -35,7 +36,7 @@ const normalizeDataURL = urlString => {
 				value = value.toLowerCase();
 			}
 
-			return `${key}=${value}`;
+			return `${key}${value ? `=${value}` : ''}`;
 		});
 
 	const normalizedMediaType = [
@@ -50,7 +51,7 @@ const normalizeDataURL = urlString => {
 		normalizedMediaType.unshift(mimeType);
 	}
 
-	return `data:${normalizedMediaType.join(';')},${base64 ? body.trim() : body}`;
+	return `data:${normalizedMediaType.join(';')},${base64 ? body.trim() : body}${hash ? `#${hash}` : ''}`;
 };
 
 const normalizeUrl = (urlString, options) => {
@@ -84,11 +85,16 @@ const normalizeUrl = (urlString, options) => {
 
 	urlString = urlString.trim();
 
+	// Data URL
+	if (/^data:/i.test(urlString)) {
+		return normalizeDataURL(urlString, options);
+	}
+
 	const hasRelativeProtocol = urlString.startsWith('//');
 	const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString);
 
 	// Prepend protocol
-	if (!isRelativeUrl && !/^data:/i.test(urlString)) {
+	if (!isRelativeUrl) {
 		urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol);
 	}
 
@@ -177,12 +183,6 @@ const normalizeUrl = (urlString, options) => {
 		urlObj.searchParams.sort();
 	}
 
-	// Data URL
-	if (urlObj.protocol === 'data:') {
-		const url = normalizeDataURL(`${urlObj.protocol}${urlObj.pathname}`);
-		return `${url}${urlObj.search}${urlObj.hash}`;
-	}
-
 	if (options.removeTrailingSlash) {
 		urlObj.pathname = urlObj.pathname.replace(/\/$/, '');
 	}


Test patch:
diff --git a/test.js b/test.js
index b65347f..0b42e81 100644
--- a/test.js
+++ b/test.js
@@ -205,6 +205,12 @@ test('remove duplicate pathname slashes', t => {
 	t.is(normalizeUrl('http://sindresorhus.com//foo'), 'http://sindresorhus.com/foo');
 });
 
+test('deprecated options', t => {
+	t.throws(() => normalizeUrl('', {normalizeHttps: true}), 'options.normalizeHttps is renamed to options.forceHttp');
+	t.throws(() => normalizeUrl('', {normalizeHttp: true}), 'options.normalizeHttp is renamed to options.forceHttps');
+	t.throws(() => normalizeUrl('', {stripFragment: true}), 'options.stripFragment is renamed to options.stripHash');
+});
+
 test('data URL', t => {
 	// Invalid URL.
 	t.throws(() => normalizeUrl('data:'), 'Invalid URL: data:');
@@ -221,20 +227,24 @@ test('data URL', t => {
 	// Lowercase the MIME type.
 	t.is(normalizeUrl('data:TEXT/plain,foo'), 'data:text/plain,foo');
 
+	// Strip empty hash.
+	t.is(normalizeUrl('data:,foo# '), 'data:,foo');
+
+	// Key only mediaType attribute.
+	t.is(normalizeUrl('data:text/plain;foo=,'), 'data:text/plain;foo,');
+	t.is(normalizeUrl('data:text/plain; foo,'), 'data:text/plain;foo,');
+
 	// Lowercase the charset.
 	t.is(normalizeUrl('data:text/plain;charset=UTF-8,foo'), 'data:text/plain;charset=utf-8,foo');
 
 	// Remove spaces after the comma when it's base64.
-	t.is(normalizeUrl('data:image/gif;base64, R0lGODlhAQABAAAAACw= ?foo=bar'), 'data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar');
+	t.is(normalizeUrl('data:image/gif;base64, R0lGODlhAQABAAAAACw= #foo #bar'), 'data:image/gif;base64,R0lGODlhAQABAAAAACw=#foo #bar');
 
 	// Keep spaces when it's not base64.
-	t.is(normalizeUrl('data:text/plain;charset=utf-8, foo ?foo=bar'), 'data:text/plain;charset=utf-8, foo?foo=bar');
-
-	// Data URL with query and hash.
-	t.is(normalizeUrl('data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar#baz'), 'data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar#baz');
+	t.is(normalizeUrl('data:text/plain;charset=utf-8, foo #bar'), 'data:text/plain;charset=utf-8, foo #bar');
 
 	// Options.
-	t.is(normalizeUrl('data:text/plain;charset=utf-8,www.foo/index.html?foo=bar&a=a&utm_medium=test#baz', {
+	const options = {
 		defaultProtocol: 'http:',
 		normalizeProtocol: true,
 		forceHttp: true,
@@ -245,5 +255,10 @@ test('data URL', t => {
 		sortQueryParameters: true,
 		removeTrailingSlash: true,
 		removeDirectoryIndex: true
-	}), 'data:text/plain;charset=utf-8,www.foo/index.html?a=a&foo=bar');
+	};
+	t.is(normalizeUrl('data:,sindresorhus.com/', options), 'data:,sindresorhus.com/');
+	t.is(normalizeUrl('data:,sindresorhus.com/index.html', options), 'data:,sindresorhus.com/index.html');
+	t.is(normalizeUrl('data:,sindresorhus.com?foo=bar&a=a&utm_medium=test', options), 'data:,sindresorhus.com?foo=bar&a=a&utm_medium=test');
+	t.is(normalizeUrl('data:,foo#bar', options), 'data:,foo');
+	t.is(normalizeUrl('data:,www.sindresorhus.com', options), 'data:,www.sindresorhus.com');
 });