diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx index 3e87d0404..686576800 100755 --- a/src/components/icon/icon.tsx +++ b/src/components/icon/icon.tsx @@ -2,8 +2,6 @@ import { Build, Component, Element, Host, Prop, State, Watch, h } from '@stencil import { getSvgContent, ioniconContent } from './request'; import { getName, getUrl, inheritAttributes, isRTL } from './utils'; -let parser: DOMParser; - @Component({ tag: 'ion-icon', assetsDirs: ['svg'], @@ -82,7 +80,7 @@ export class Icon { * @default true */ @Prop() sanitize = true; - + componentWillLoad() { this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']); } @@ -124,12 +122,12 @@ export class Icon { cb(); } } - + private hasAriaHidden = () => { const { el } = this; - + return el.hasAttribute('aria-hidden') && el.getAttribute('aria-hidden') === 'true'; - } + }; @Watch('name') @Watch('src') @@ -138,27 +136,12 @@ export class Icon { @Watch('md') loadIcon() { if (Build.isBrowser && this.isVisible) { - if (!parser) { - /** - * Create an instance of the DOM parser. This creates a single - * parser instance for the entire app, which is more efficient. - */ - parser = new DOMParser(); - } const url = getUrl(this); if (url) { if (ioniconContent.has(url)) { // sync if it's already loaded this.svgContent = ioniconContent.get(url); - } else if (url.startsWith('data:')) { - const doc = parser.parseFromString(url, 'text/html'); - const svgEl = doc.body.querySelector('svg'); - if (svgEl !== null) { - this.svgContent = svgEl.outerHTML; - } else { - this.svgContent = ''; - } } else { // async if it hasn't been loaded getSvgContent(url, this.sanitize).then(() => (this.svgContent = ioniconContent.get(url))); @@ -166,7 +149,7 @@ export class Icon { } } - const label = this.iconName = getName(this.name, this.icon, this.mode, this.ios, this.md); + const label = (this.iconName = getName(this.name, this.icon, this.mode, this.ios, this.md)); /** * Come up with a default label @@ -182,9 +165,7 @@ export class Icon { const mode = this.mode || 'md'; const flipRtl = this.flipRtl || - (iconName && - (iconName.indexOf('arrow') > -1 || iconName.indexOf('chevron') > -1) && - this.flipRtl !== false); + (iconName && (iconName.indexOf('arrow') > -1 || iconName.indexOf('chevron') > -1) && this.flipRtl !== false); /** * Only set the aria-label if a) we have generated diff --git a/src/components/icon/request.ts b/src/components/icon/request.ts index a8618bffd..00ae0cb88 100644 --- a/src/components/icon/request.ts +++ b/src/components/icon/request.ts @@ -1,29 +1,51 @@ -import { validateContent } from './validate'; +import { isEncodedDataUrl, isSvgDataUrl, validateContent } from './validate'; export const ioniconContent = new Map(); const requests = new Map>(); +let parser = new DOMParser(); + export const getSvgContent = (url: string, sanitize: boolean) => { // see if we already have a request for this url let req = requests.get(url); if (!req) { if (typeof fetch !== 'undefined' && typeof document !== 'undefined') { - // we don't already have a request - req = fetch(url).then((rsp) => { - if (rsp.ok) { - return rsp.text().then((svgContent) => { - if (svgContent && sanitize !== false) { - svgContent = validateContent(svgContent); - } - ioniconContent.set(url, svgContent || ''); - }); + /** + * If the url is a data url of an svg, then try to parse it + * with the DOMParser. This works with content security policies enabled. + */ + if (isSvgDataUrl(url) && isEncodedDataUrl(url)) { + if (!parser) { + /** + * Create an instance of the DOM parser. This creates a single + * parser instance for the entire app, which is more efficient. + */ + parser = new DOMParser(); + } + const doc = parser.parseFromString(url, 'text/html'); + const svg = doc.querySelector('svg'); + if (svg) { + ioniconContent.set(url, svg.outerHTML); } - ioniconContent.set(url, ''); - }); + return Promise.resolve(); + } else { + // we don't already have a request + req = fetch(url).then((rsp) => { + if (rsp.ok) { + return rsp.text().then((svgContent) => { + if (svgContent && sanitize !== false) { + svgContent = validateContent(svgContent); + } + ioniconContent.set(url, svgContent || ''); + }); + } + ioniconContent.set(url, ''); + }); + // cache for the same requests + requests.set(url, req); + } - // cache for the same requests - requests.set(url, req); } else { // set to empty for ssr scenarios and resolve promise ioniconContent.set(url, ''); diff --git a/src/components/icon/test/icon.e2e.ts-snapshots/icon-diff-Mobile-Chrome-linux.png b/src/components/icon/test/icon.e2e.ts-snapshots/icon-diff-Mobile-Chrome-linux.png index 8e3ac1ebe..ed1002be0 100644 Binary files a/src/components/icon/test/icon.e2e.ts-snapshots/icon-diff-Mobile-Chrome-linux.png and b/src/components/icon/test/icon.e2e.ts-snapshots/icon-diff-Mobile-Chrome-linux.png differ diff --git a/src/components/icon/test/icon.e2e.ts-snapshots/icon-diff-Mobile-Firefox-linux.png b/src/components/icon/test/icon.e2e.ts-snapshots/icon-diff-Mobile-Firefox-linux.png index 011a9509c..c078e9e0b 100644 Binary files a/src/components/icon/test/icon.e2e.ts-snapshots/icon-diff-Mobile-Firefox-linux.png and b/src/components/icon/test/icon.e2e.ts-snapshots/icon-diff-Mobile-Firefox-linux.png differ diff --git a/src/components/icon/test/icon.e2e.ts-snapshots/icon-diff-Mobile-Safari-linux.png b/src/components/icon/test/icon.e2e.ts-snapshots/icon-diff-Mobile-Safari-linux.png index 8dbde7ca1..140fe97b9 100644 Binary files a/src/components/icon/test/icon.e2e.ts-snapshots/icon-diff-Mobile-Safari-linux.png and b/src/components/icon/test/icon.e2e.ts-snapshots/icon-diff-Mobile-Safari-linux.png differ diff --git a/src/components/icon/test/validate.spec.ts b/src/components/icon/test/validate.spec.ts index 0135c81ea..68f4179a9 100644 --- a/src/components/icon/test/validate.spec.ts +++ b/src/components/icon/test/validate.spec.ts @@ -1,4 +1,4 @@ -import { isValid } from '../validate'; +import { isEncodedDataUrl, isSvgDataUrl, isValid } from '../validate'; describe('isValid', () => { @@ -24,9 +24,11 @@ describe('isValid', () => { }); it('invalid child SCRIPT elm', () => { - const el = { nodeType: 1, nodeName: 'svg', attributes: [], childNodes: [ - { nodeType: 1, nodeName: 'SCRIPT', attributes: [], childNodes: [] } - ] } as any; + const el = { + nodeType: 1, nodeName: 'svg', attributes: [], childNodes: [ + { nodeType: 1, nodeName: 'SCRIPT', attributes: [], childNodes: [] } + ] + } as any; expect(isValid(el)).toBe(false); }); @@ -41,9 +43,11 @@ describe('isValid', () => { }); it('is valid SVG elm', () => { - const el = { nodeType: 1, nodeName: 'SVG', attributes: [], childNodes: [ - { nodeType: 1, nodeName: 'line', attributes: [], childNodes: [] } - ] } as any; + const el = { + nodeType: 1, nodeName: 'SVG', attributes: [], childNodes: [ + { nodeType: 1, nodeName: 'line', attributes: [], childNodes: [] } + ] + } as any; expect(isValid(el)).toBe(true); }); @@ -53,3 +57,17 @@ describe('isValid', () => { }); }); + +it('isSvgDataUrl', () => { + expect(isSvgDataUrl('data:image/svg+xml;base64,xxx')).toBe(true); + expect(isSvgDataUrl('data:image/svg+xml;utf8,')).toBe(true); + expect(isSvgDataUrl('https://example.com/icon.svg')).toBe(false); + expect(isSvgDataUrl('http://example.com/icon.svg')).toBe(false); +}); + +it('isEncodedDataUrl', () => { + expect(isEncodedDataUrl('data:image/svg+xml;base64,xxx')).toBe(false); + expect(isEncodedDataUrl('data:image/svg+xml;utf8,')).toBe(true); + expect(isEncodedDataUrl('https://example.com/icon.svg')).toBe(false); + expect(isEncodedDataUrl('http://example.com/icon.svg')).toBe(false); +}); \ No newline at end of file diff --git a/src/components/icon/validate.ts b/src/components/icon/validate.ts index 27619a41c..d6c9ebd0c 100644 --- a/src/components/icon/validate.ts +++ b/src/components/icon/validate.ts @@ -48,3 +48,6 @@ export const isValid = (elm: HTMLElement) => { } return true; }; + +export const isSvgDataUrl = (url: string) => url.startsWith('data:image/svg+xml'); +export const isEncodedDataUrl = (url: string) => url.indexOf(';utf8,') !== -1; \ No newline at end of file diff --git a/src/index.html b/src/index.html index 9bf90fb11..d7291cec6 100644 --- a/src/index.html +++ b/src/index.html @@ -136,6 +136,10 @@

Sanitized (shouldn't show)

Not Sanitized (should show)

+

Base64 url

+ +

Cheatsheet