Merge pull request #6006 from backstage/iameap/bug-techdocs-ext-svg
[TechDocs] Fix external image rendering
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user