From b2a8a2f99e769638b1bb647e7f9fdc6ba5f1467b Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 7 Nov 2020 12:39:14 -0500 Subject: [PATCH] Fix html validation for Image component (#18903) This PR fixes two bugs causing HTML validators to complain. - Error: Bad value data:image/svg+xml;charset=utf-8, for attribute src on element img: Illegal character in scheme data: < is not allowed. - Fixed by using base64 for svg during `layout=intrinsic` to avoid angle brackets - Error: Element img is missing required attribute src. - Fixed by using base64 transparent gif for `loading=lazy` placeholder Fixes #18850 --- packages/next/client/image.tsx | 18 ++++++++---------- packages/next/next-server/lib/to-base-64.ts | 10 ++++++++++ .../image-component/basic/test/index.test.js | 16 +++++++++------- 3 files changed, 27 insertions(+), 17 deletions(-) create mode 100644 packages/next/next-server/lib/to-base-64.ts diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 088ad92a1697e..c8d5318226bee 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -1,5 +1,6 @@ import React, { ReactElement } from 'react' import Head from '../next-server/lib/head' +import { toBase64 } from '../next-server/lib/to-base-64' import { useIntersection } from './use-intersection' const VALID_LOADING_VALUES = ['lazy', 'eager', undefined] as const @@ -396,17 +397,14 @@ export default function Image({ quality: qualityInt, }) - let imgAttributes: - | Pick - | undefined + const imgAttributes: Pick = { + src: + 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', + } if (isVisible) { - imgAttributes = { - src: imgSrc, - } - if (imgSrcSet) { - imgAttributes.srcSet = imgSrcSet - } + imgAttributes.src = imgSrc + imgAttributes.srcSet = imgSrcSet } // No need to add preloads on the client side--by the time the application is hydrated, @@ -438,7 +436,7 @@ export default function Image({ alt="" aria-hidden={true} role="presentation" - src={`data:image/svg+xml;charset=utf-8,${sizerSvg}`} + src={`data:image/svg+xml;base64,${toBase64(sizerSvg)}`} /> ) : null} diff --git a/packages/next/next-server/lib/to-base-64.ts b/packages/next/next-server/lib/to-base-64.ts new file mode 100644 index 0000000000000..f1fc94e1dbd87 --- /dev/null +++ b/packages/next/next-server/lib/to-base-64.ts @@ -0,0 +1,10 @@ +/** + * Isomorphic base64 that works on the server and client + */ +export function toBase64(str: string) { + if (typeof window === 'undefined') { + return Buffer.from(str).toString('base64') + } else { + return window.btoa(str) + } +} diff --git a/test/integration/image-component/basic/test/index.test.js b/test/integration/image-component/basic/test/index.test.js index c02ca2d569c4e..ff61c405fd64f 100644 --- a/test/integration/image-component/basic/test/index.test.js +++ b/test/integration/image-component/basic/test/index.test.js @@ -17,6 +17,8 @@ const appDir = join(__dirname, '../') let appPort let app let browser +const emptyImage = + 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' function runTests() { it('should render an image tag', async () => { @@ -97,9 +99,9 @@ function lazyLoadingTests() { ) }) it('should not have loaded the second image immediately', async () => { - expect( - await browser.elementById('lazy-mid').getAttribute('src') - ).toBeFalsy() + expect(await browser.elementById('lazy-mid').getAttribute('src')).toBe( + emptyImage + ) expect( await browser.elementById('lazy-mid').getAttribute('srcset') ).toBeFalsy() @@ -128,9 +130,9 @@ function lazyLoadingTests() { }, 'https://example.com/myaccount/foo2.jpg?auto=format&fit=max&w=480 1x, https://example.com/myaccount/foo2.jpg?auto=format&fit=max&w=1024 2x') }) it('should not have loaded the third image after scrolling down', async () => { - expect( - await browser.elementById('lazy-bottom').getAttribute('src') - ).toBeFalsy() + expect(await browser.elementById('lazy-bottom').getAttribute('src')).toBe( + emptyImage + ) expect( await browser.elementById('lazy-bottom').getAttribute('srcset') ).toBeFalsy() @@ -155,7 +157,7 @@ function lazyLoadingTests() { it('should load the fourth image lazily after scrolling down', async () => { expect( await browser.elementById('lazy-without-attribute').getAttribute('src') - ).toBeFalsy() + ).toBe(emptyImage) expect( await browser.elementById('lazy-without-attribute').getAttribute('srcset') ).toBeFalsy()