Merge pull request #6006 from backstage/iameap/bug-techdocs-ext-svg

[TechDocs] Fix external image rendering
This commit is contained in:
Eric Peterson
2021-06-17 09:52:18 +02:00
committed by GitHub
3 changed files with 83 additions and 6 deletions
@@ -14,17 +14,21 @@
* limitations under the License.
*/
import { waitFor } from '@testing-library/react';
import { createTestShadowDom } from '../../test-utils';
import { addBaseUrl } from '../transformers';
import { TechDocsStorageApi } from '../../api';
const DOC_STORAGE_URL = 'https://example-host.storage.googleapis.com';
const API_ORIGIN_URL = 'https://backstage.example.com/api/techdocs';
const techdocsStorageApi: TechDocsStorageApi = {
getBaseUrl: jest.fn(() => Promise.resolve(DOC_STORAGE_URL)),
getBaseUrl: jest.fn(o =>
Promise.resolve(new URL(o, DOC_STORAGE_URL).toString()),
),
getEntityDocs: () => new Promise(resolve => resolve('yes!')),
syncEntityDocs: () => new Promise(resolve => resolve(true)),
getApiOrigin: jest.fn(),
getApiOrigin: jest.fn(() => new Promise(resolve => resolve(API_ORIGIN_URL))),
getBuilder: jest.fn(),
getStorageUrl: jest.fn(),
};
@@ -96,7 +100,7 @@ describe('addBaseUrl', () => {
);
});
it('transforms svg img src to data uri', async () => {
it('inlines svg img src to data uri', async () => {
const svgContent = '<svg></svg>';
const expectedSrc = `data:image/svg+xml;base64,${Buffer.from(
svgContent,
@@ -117,10 +121,60 @@ describe('addBaseUrl', () => {
postTransformers: [],
});
await waitFor(() => {
const actualSrc = root.getElementById('x')?.getAttribute('src');
expect(expectedSrc).toEqual(actualSrc);
});
});
it('inlines absolute url svgs pointed at our backend', async () => {
const svgContent = '<svg></svg>';
const expectedSrc = `data:image/svg+xml;base64,${Buffer.from(
svgContent,
).toString('base64')}`;
(global.fetch as jest.Mock).mockReturnValue({
text: jest.fn().mockResolvedValue(svgContent),
});
const root = createTestShadowDom(
`<img id="x" src="${API_ORIGIN_URL}/test.svg" />`,
{
preTransformers: [
addBaseUrl({
techdocsStorageApi,
entityId: mockEntityId,
path: '',
}),
],
postTransformers: [],
},
);
await waitFor(() => {
const actualSrc = root.getElementById('x')?.getAttribute('src');
expect(expectedSrc).toEqual(actualSrc);
});
});
it('does not inline external svgs', async () => {
const expectedSrc = 'https://example.com/test.svg';
const root = createTestShadowDom(`<img id="x" src="${expectedSrc}" />`, {
preTransformers: [
addBaseUrl({
techdocsStorageApi,
entityId: mockEntityId,
path: '',
}),
],
postTransformers: [],
});
await new Promise<void>(done => {
process.nextTick(() => {
const actualSrc = root.getElementById('x')?.getAttribute('src');
expect(expectedSrc).toEqual(actualSrc);
const actualElem = root.getElementById('x');
expect(actualElem?.getAttribute('src')).toEqual(expectedSrc);
expect(actualElem?.getAttribute('alt')).toEqual(null);
done();
});
});
@@ -23,6 +23,22 @@ type AddBaseUrlOptions = {
path: string;
};
/**
* TechDocs backend serves SVGs with text/plain content-type for security. This
* helper determines if an SVG is being loaded from the backend, and thus needs
* inlining to be displayed properly.
*/
const isSvgNeedingInlining = (
attrName: string,
attrVal: string,
apiOrigin: string,
) => {
const isSrcToSvg = attrName === 'src' && attrVal.endsWith('.svg');
const isRelativeUrl = !attrVal.match(/^([a-z]*:)?\/\//i);
const pointsToOurBackend = attrVal.startsWith(apiOrigin);
return isSrcToSvg && (isRelativeUrl || pointsToOurBackend);
};
export const addBaseUrl = ({
techdocsStorageApi,
entityId,
@@ -45,7 +61,8 @@ export const addBaseUrl = ({
entityId,
path,
);
if (attributeName === 'src' && elemAttribute.endsWith('.svg')) {
const apiOrigin = await techdocsStorageApi.getApiOrigin();
if (isSvgNeedingInlining(attributeName, elemAttribute, apiOrigin)) {
try {
const svg = await fetch(newValue);
const svgContent = await svg.text();