From dd8ef4058f818a6ace315cbedcb5b31a21621267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Thu, 18 Jan 2024 17:41:57 +0100 Subject: [PATCH] Handle encoding image path before displaying in the app, especially for local files --- newIDE/app/src/UI/CorsAwareImage.js | 46 ++++++++-- newIDE/app/src/UI/CorsAwareImage.spec.js | 104 +++++++++++++++++++++++ 2 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 newIDE/app/src/UI/CorsAwareImage.spec.js diff --git a/newIDE/app/src/UI/CorsAwareImage.js b/newIDE/app/src/UI/CorsAwareImage.js index 342136deaac2..65fce7b42696 100644 --- a/newIDE/app/src/UI/CorsAwareImage.js +++ b/newIDE/app/src/UI/CorsAwareImage.js @@ -12,18 +12,50 @@ type Props = {| onLoad?: (e: any) => void, |}; -const addSearchParameterToUrl = ( +export const encodeUrlAndAddEncodedSearchParameter = ( url: string, urlEncodedParameterName: string, urlEncodedValue: string ) => { - if (url.startsWith('data:') || url.startsWith('blob:')) { - // blob/data protocol does not support search parameters, which are useless anyway. - return url; + console.log(url); + if ( + url.startsWith('http://') || + url.startsWith('https://') || + url.startsWith('ftp://') + ) { + const urlObject = new URL(url); + urlObject.searchParams.set(urlEncodedParameterName, urlEncodedValue); + return urlObject.toString(); } - const separator = url.indexOf('?') === -1 ? '?' : '&'; - return url + separator + urlEncodedParameterName + '=' + urlEncodedValue; + if (url.startsWith('file://')) { + // Local files and folders can contain special characters, which is handled badly when using + // new URL() constructor. So we do it manually. + const searchParams = url.indexOf('?') === -1 ? '' : url.split('?')[1]; + const urlWithoutSearchParams = searchParams ? url.split('?')[0] : url; + const urlWithoutSearchParamsAndFileProtocol = urlWithoutSearchParams.slice( + 'file://'.length + ); + const encodedUrlComponents = urlWithoutSearchParamsAndFileProtocol + .split('/') + .map(component => encodeURIComponent(component)); + const encodedUrlWithoutSearchParamsAndFileProtocol = encodedUrlComponents.join( + '/' + ); + const newSearchParam = urlEncodedParameterName + '=' + urlEncodedValue; + const searchParamsWithNewParam = searchParams + ? searchParams + '&' + newSearchParam + : newSearchParam; + return ( + 'file://' + + encodedUrlWithoutSearchParamsAndFileProtocol + + '?' + + searchParamsWithNewParam + ); + } + + // blob/data protocol or static images do not support search parameters, which are useless anyway. + return url; }; /** @@ -58,7 +90,7 @@ export const CorsAwareImage = (props: Props) => ( // // Search for "cors-cache-workaround" in the codebase for the same workarounds. props.src - ? addSearchParameterToUrl(props.src, 'gdUsage', 'img') + ? encodeUrlAndAddEncodedSearchParameter(props.src, 'gdUsage', 'img') : undefined } /> diff --git a/newIDE/app/src/UI/CorsAwareImage.spec.js b/newIDE/app/src/UI/CorsAwareImage.spec.js new file mode 100644 index 000000000000..7c6bae5887ec --- /dev/null +++ b/newIDE/app/src/UI/CorsAwareImage.spec.js @@ -0,0 +1,104 @@ +// @flow +import { encodeUrlAndAddEncodedSearchParameter } from './CorsAwareImage'; + +describe('encodeUrlAndAddEncodedSearchParameter', () => { + it('should add search parameter to http url', () => { + const url = 'http://resources.gdevelop-app.com/file.png'; + const result = encodeUrlAndAddEncodedSearchParameter(url, 'param', 'value'); + expect(result).toBe( + 'http://resources.gdevelop-app.com/file.png?param=value' + ); + }); + + it('should add search parameter to http url with existing params', () => { + const url = 'http://resources.gdevelop-app.com/file.png?existing=param'; + const result = encodeUrlAndAddEncodedSearchParameter(url, 'param', 'value'); + expect(result).toBe( + 'http://resources.gdevelop-app.com/file.png?existing=param¶m=value' + ); + }); + + it('should encode properly the http url', () => { + const url = + 'http://resources.gdevelop-app.com/folder/my file.png?existing=param'; + const result = encodeUrlAndAddEncodedSearchParameter(url, 'param', 'value'); + expect(result).toBe( + 'http://resources.gdevelop-app.com/folder/my%20file.png?existing=param¶m=value' + ); + }); + + it('should add search parameter to https url', () => { + const url = 'https://resources.gdevelop-app.com/file.png'; + const result = encodeUrlAndAddEncodedSearchParameter(url, 'param', 'value'); + expect(result).toBe( + 'https://resources.gdevelop-app.com/file.png?param=value' + ); + }); + + it('should add search parameter to https url with existing params', () => { + const url = 'https://resources.gdevelop-app.com/file.png?existing=param'; + const result = encodeUrlAndAddEncodedSearchParameter(url, 'param', 'value'); + expect(result).toBe( + 'https://resources.gdevelop-app.com/file.png?existing=param¶m=value' + ); + }); + + it('should encode properly the https url', () => { + const url = + 'https://resources.gdevelop-app.com/my folder/my file.png?existing=param'; + const result = encodeUrlAndAddEncodedSearchParameter(url, 'param', 'value'); + expect(result).toBe( + 'https://resources.gdevelop-app.com/my%20folder/my%20file.png?existing=param¶m=value' + ); + }); + + it('should add search parameter to ftp url', () => { + const url = 'ftp://example.com/file.png'; + const result = encodeUrlAndAddEncodedSearchParameter(url, 'param', 'value'); + expect(result).toBe('ftp://example.com/file.png?param=value'); + }); + + it('should add search parameter to ftp url with existing params', () => { + const url = 'ftp://example.com/file.png?existing=param'; + const result = encodeUrlAndAddEncodedSearchParameter(url, 'param', 'value'); + expect(result).toBe( + 'ftp://example.com/file.png?existing=param¶m=value' + ); + }); + + it('should encode properly the ftp url', () => { + const url = 'ftp://example.com/my folder/my file.png?existing=param'; + const result = encodeUrlAndAddEncodedSearchParameter(url, 'param', 'value'); + expect(result).toBe( + 'ftp://example.com/my%20folder/my%20file.png?existing=param¶m=value' + ); + }); + + it('should add search parameter to file url', () => { + const url = 'file://User/My folder/file.png'; + const result = encodeUrlAndAddEncodedSearchParameter(url, 'param', 'value'); + expect(result).toBe('file://User/My%20folder/file.png?param=value'); + }); + + it('should add search parameter to file url with existing params', () => { + const url = 'file://User/My folder/file.png?existing=param'; + const result = encodeUrlAndAddEncodedSearchParameter(url, 'param', 'value'); + expect(result).toBe( + 'file://User/My%20folder/file.png?existing=param¶m=value' + ); + }); + + it('should encode properly the file url', () => { + const url = 'file://User/My #folder/my #file.png?existing=param'; + const result = encodeUrlAndAddEncodedSearchParameter(url, 'param', 'value'); + expect(result).toBe( + 'file://User/My%20%23folder/my%20%23file.png?existing=param¶m=value' + ); + }); + + it('should return original url for blob/data protocol or static images', () => { + const url = 'blob://example.com/file.png'; + const result = encodeUrlAndAddEncodedSearchParameter(url, 'param', 'value'); + expect(result).toBe(url); + }); +});