From fb92bcb1a80ce916702e40cf6fab9629507ca83f Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Sat, 21 Sep 2019 01:10:13 +0800 Subject: [PATCH] Fix data URL should not support query strings (#97) --- index.js | 22 +++++++++++----------- test.js | 29 ++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 18 deletions(-) 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(/\/$/, ''); } 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'); });