diff --git a/karma.config.js b/karma.config.js index e83bdd7b5..6981d737a 100644 --- a/karma.config.js +++ b/karma.config.js @@ -22,6 +22,11 @@ module.exports = config => config.set({ { pattern: 'test/**/*.test.js', watched: false } ], + proxies: { + // useful when the contents of a fake asset do not matter + '/_/': 'localhost/' + }, + // create dedicated bundles for src, test helpers, and each test suite preprocessors: { 'src/index.js': ['rollup'], diff --git a/packages/dom/src/serialize-frames.js b/packages/dom/src/serialize-frames.js index d6ef7c6bd..44d346cad 100644 --- a/packages/dom/src/serialize-frames.js +++ b/packages/dom/src/serialize-frames.js @@ -1,5 +1,55 @@ import serializeDOM from './serialize-dom'; +// List of attributes that accept URIs that should be transformed +const URI_ATTRS = ['href', 'src', 'srcset', 'poster', 'background']; +const URI_SELECTOR = URI_ATTRS.map(a => `[${a}]`).join(',') + ( + ',[style*="url("]'); // include elements with url style attributes + +// A loose srcset image candidate regex split into capture groups +// https://html.spec.whatwg.org/multipage/images.html#srcset-attribute +const SRCSET_REGEX = /(\s*)([^,]\S*[^,])((?:\s+[^,]+)*\s*(?:,|$))/g; + +// A loose CSS url() regex split into capture groups +const CSS_URL_REGEX = /(url\((["']?))((?:\\.|(?!\2).|[^)])+)(\2\))/g; + +// Transforms URL attributes within a document to be fully qualified URLs. This is necessary when +// embedded documents are serialized and their contents become root-relative. +function transformRelativeUrls(dom) { + // transform style elements that might contain URLs + for (let style of dom.querySelectorAll('style')) { + style.innerHTML &&= style.innerHTML + .replace(CSS_URL_REGEX, (_, $1, $2, uri, $4) => ( + `${$1}${new URL(uri, style.baseURI).href}${$4}` + )); + } + + // transform element attributes that might contain URLs + for (let el of dom.querySelectorAll(URI_SELECTOR)) { + for (let attr of URI_ATTRS.concat('style')) { + if (!(attr in el) || !el[attr] || !el.hasAttribute(attr)) continue; + let value = el[attr]; + + if (attr === 'style') { + // transform inline style url() usage + value = el.getAttribute('style') + .replace(CSS_URL_REGEX, (_, $1, $2, uri, $4) => ( + `${$1}${new URL(uri, el.baseURI).href}${$4}` + )); + } else if (attr === 'srcset') { + // transform each srcset URL + value = value.replace(SRCSET_REGEX, (_, $1, uri, $3) => ( + `${$1}${new URL(uri, el.baseURI).href}${$3}` + )); + } else { + // resolve the URL with the node's base URI + value = new URL(value, el.baseURI).href; + } + + el.setAttribute(attr, value); + } + } +} + // Recursively serializes iframe documents into srcdoc attributes. export default function serializeFrames(dom, clone, { enableJavaScript }) { for (let frame of dom.querySelectorAll('iframe')) { @@ -22,6 +72,7 @@ export default function serializeFrames(dom, clone, { enableJavaScript }) { // recersively serialize contents let serialized = serializeDOM({ + domTransformation: transformRelativeUrls, dom: frame.contentDocument, enableJavaScript }); diff --git a/packages/dom/test/serialize-frames.test.js b/packages/dom/test/serialize-frames.test.js index fc7fa1aa5..c99feee8f 100644 --- a/packages/dom/test/serialize-frames.test.js +++ b/packages/dom/test/serialize-frames.test.js @@ -26,6 +26,16 @@ describe('serializeFrames', () => { + `); let $frameInput = await getFrame('frame-input'); @@ -82,6 +92,21 @@ describe('serializeFrames', () => { ].join(''))); }); + it('serializes iframes internal URLs', () => { + let url = uri => new URL(uri, document.URL).href; + + expect($('#frame-with-urls')[0].getAttribute('srcdoc')).toBe([ + ``, + ``, + `

Testing

`, + ``, + `', + `` + // account for indentation in the fixture + ].join(' '.repeat(12)) + ''); + }); + it('does not serialize iframes with CORS', () => { expect($('#frame-external')[0].getAttribute('src')).toBe('https://example.com'); expect($('#frame-external-fail')[0].getAttribute('src')).toBe('https://google.com');