Skip to content

Commit d9e5593

Browse files
authored
refactor: replace X-Frame-Options with CSP frame-ancestors (#1059)
* refactor: replace X-Frame-Options with CSP frame-ancestors * feat: remove table headers from copyed html * fix: clipboard unit tests
1 parent 15b79dd commit d9e5593

File tree

3 files changed

+26
-27
lines changed

3 files changed

+26
-27
lines changed

apps/nextjs-app/src/features/app/utils/clipboard.spec.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,18 @@ vi.mock('@teable/core', () => {
2525
};
2626
});
2727

28+
vi.mock('zod', () => {
29+
return {
30+
z: {
31+
array: () => ({
32+
safeParse: (data: string) => ({ success: true, data }),
33+
}),
34+
},
35+
};
36+
});
37+
2838
describe('clipboard', () => {
29-
const html =
30-
'<meta charset="utf-8"><table data-teable-html-marker="1"><thead><tr><th id="fldziUf9QuQjkbfMuG5" data-field="%7B%22id%22%3A%22fldziUf9QuQjkbfMuG5%22%2C%22name%22%3A%22Name%22%2C%22isPrimary%22%3Atrue%2C%22columnMeta%22%3A%7B%22viwE0sl0GqGdWaBqwFi%22%3A%7B%22order%22%3A0.5%7D%7D%2C%22dbFieldName%22%3A%22Name_fldziUf9QuQjkbfMuG5%22%2C%22dbFieldType%22%3A%22TEXT%22%2C%22type%22%3A%22singleLineText%22%2C%22options%22%3A%7B%7D%2C%22cellValueType%22%3A%22string%22%7D">Name</th><th id="fldpsQvHI4ugP2luizP" data-field="%7B%22id%22%3A%22fldpsQvHI4ugP2luizP%22%2C%22name%22%3A%22Count%22%2C%22columnMeta%22%3A%7B%22viwE0sl0GqGdWaBqwFi%22%3A%7B%22order%22%3A1%7D%7D%2C%22dbFieldName%22%3A%22Count_fldpsQvHI4ugP2luizP%22%2C%22dbFieldType%22%3A%22REAL%22%2C%22type%22%3A%22number%22%2C%22options%22%3A%7B%22formatting%22%3A%7B%22type%22%3A%22decimal%22%2C%22precision%22%3A0%7D%7D%2C%22cellValueType%22%3A%22number%22%7D">Count</th><th id="fldGTKfZvXNXeMJ6nqu" data-field="%7B%22id%22%3A%22fldGTKfZvXNXeMJ6nqu%22%2C%22name%22%3A%22Status%22%2C%22columnMeta%22%3A%7B%22viwE0sl0GqGdWaBqwFi%22%3A%7B%22order%22%3A2%7D%7D%2C%22dbFieldName%22%3A%22Status_fldGTKfZvXNXeMJ6nqu%22%2C%22dbFieldType%22%3A%22TEXT%22%2C%22options%22%3A%7B%22choices%22%3A%5B%7B%22name%22%3A%22light%22%2C%22id%22%3A%22cho2caYhPrI%22%2C%22color%22%3A%22grayBright%22%7D%2C%7B%22name%22%3A%22medium%22%2C%22id%22%3A%22chor2ob8aU7%22%2C%22color%22%3A%22yellowBright%22%7D%2C%7B%22name%22%3A%22heavy%22%2C%22id%22%3A%22choArPr57sO%22%2C%22color%22%3A%22tealBright%22%7D%5D%7D%2C%22type%22%3A%22singleSelect%22%2C%22cellValueType%22%3A%22string%22%7D">Status</th></tr></thead><tbody><tr><td>John</td><td>20</td><td>light</td></tr><tr><td>Tom</td><td>30</td><td>medium</td></tr><tr><td>Bob</td><td>40</td><td>heavy</td></tr></tbody></table>';
39+
const html = `<meta charset="utf-8"><table data-teable-html-marker="1" data-teable-html-header="%5B%7B%22id%22%3A%22fldziUf9QuQjkbfMuG5%22%2C%22name%22%3A%22Name%22%2C%22isPrimary%22%3Atrue%2C%22columnMeta%22%3A%7B%22viwE0sl0GqGdWaBqwFi%22%3A%7B%22order%22%3A0.5%7D%7D%2C%22dbFieldName%22%3A%22Name_fldziUf9QuQjkbfMuG5%22%2C%22dbFieldType%22%3A%22TEXT%22%2C%22type%22%3A%22singleLineText%22%2C%22options%22%3A%7B%7D%2C%22cellValueType%22%3A%22string%22%7D%2C%7B%22id%22%3A%22fldpsQvHI4ugP2luizP%22%2C%22name%22%3A%22Count%22%2C%22columnMeta%22%3A%7B%22viwE0sl0GqGdWaBqwFi%22%3A%7B%22order%22%3A1%7D%7D%2C%22dbFieldName%22%3A%22Count_fldpsQvHI4ugP2luizP%22%2C%22dbFieldType%22%3A%22REAL%22%2C%22type%22%3A%22number%22%2C%22options%22%3A%7B%22formatting%22%3A%7B%22type%22%3A%22decimal%22%2C%22precision%22%3A0%7D%7D%2C%22cellValueType%22%3A%22number%22%7D%2C%7B%22id%22%3A%22fldGTKfZvXNXeMJ6nqu%22%2C%22name%22%3A%22Status%22%2C%22columnMeta%22%3A%7B%22viwE0sl0GqGdWaBqwFi%22%3A%7B%22order%22%3A2%7D%7D%2C%22dbFieldName%22%3A%22Status_fldGTKfZvXNXeMJ6nqu%22%2C%22dbFieldType%22%3A%22TEXT%22%2C%22options%22%3A%7B%22choices%22%3A%5B%7B%22name%22%3A%22light%22%2C%22id%22%3A%22cho2caYhPrI%22%2C%22color%22%3A%22grayBright%22%7D%2C%7B%22name%22%3A%22medium%22%2C%22id%22%3A%22chor2ob8aU7%22%2C%22color%22%3A%22yellowBright%22%7D%2C%7B%22name%22%3A%22heavy%22%2C%22id%22%3A%22choArPr57sO%22%2C%22color%22%3A%22tealBright%22%7D%5D%7D%2C%22type%22%3A%22singleSelect%22%2C%22cellValueType%22%3A%22string%22%7D%5D"><tbody><tr><td>John</td><td>20</td><td>light</td></tr><tr><td>Tom</td><td>30</td><td>medium</td></tr><tr><td>Bob</td><td>40</td><td>heavy</td></tr></tbody></table>`;
3140

3241
const expectedHeader = [
3342
{

apps/nextjs-app/src/features/app/utils/clipboard.ts

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { FieldType, fieldVoSchema, parseClipboardText, type IFieldVo } from '@teable/core';
2+
import { z } from 'zod';
23
import { fromZodError } from 'zod-validation-error';
34

45
const teableHtmlMarker = 'data-teable-html-marker';
6+
const teableHeader = 'data-teable-html-header';
57

68
export const serializerHtml = (data: string, headers: IFieldVo[]) => {
79
const tableData = parseClipboardText(data);
@@ -18,13 +20,8 @@ export const serializerHtml = (data: string, headers: IFieldVo[]) => {
1820
.join('')}</tr>`;
1921
})
2022
.join('');
21-
const headerContent = headers
22-
.map((header) => {
23-
return `<th id="${header.id}" data-field="${encodeURIComponent(JSON.stringify(header))}">${header.name}</th>`;
24-
})
25-
.join('');
2623

27-
return `<meta charset="utf-8"><table ${teableHtmlMarker}="1"><thead><tr>${headerContent}</tr></thead><tbody>${bodyContent}</tbody></table>`;
24+
return `<meta charset="utf-8"><table ${teableHtmlMarker}="1" ${teableHeader}="${encodeURIComponent(JSON.stringify(headers))}"><tbody>${bodyContent}</tbody></table>`;
2825
};
2926

3027
export const extractTableHeader = (html?: string) => {
@@ -34,23 +31,16 @@ export const extractTableHeader = (html?: string) => {
3431
const parser = new DOMParser();
3532
const doc = parser.parseFromString(html, 'text/html');
3633
const table = doc.querySelector('table');
37-
const headerRow = table?.querySelector('thead tr');
38-
const headerCells = headerRow?.querySelectorAll('th') || [];
39-
40-
const headers = Array.from(headerCells);
41-
let error = '';
42-
const result = headers.map((cell) => {
43-
const fieldVoStr = cell.getAttribute('data-field');
44-
const fieldVo = fieldVoStr ? JSON.parse(decodeURIComponent(fieldVoStr)) : undefined;
45-
46-
const validate = fieldVoSchema.safeParse(fieldVo);
47-
if (validate.success) {
48-
return fieldVo;
49-
}
50-
error = fromZodError(validate.error).message;
51-
return undefined;
52-
}) as IFieldVo[];
53-
return error ? { result: undefined, error } : { result };
34+
const headerStr = table?.getAttribute(teableHeader);
35+
const headers = headerStr ? JSON.parse(decodeURIComponent(headerStr)) : undefined;
36+
if (!headers) {
37+
return { result: undefined };
38+
}
39+
const validate = z.array(fieldVoSchema).safeParse(headers);
40+
if (!validate.success) {
41+
return { result: undefined, error: fromZodError(validate.error).message };
42+
}
43+
return { result: validate.data };
5444
};
5545

5646
export const isTeableHTML = (html: string) => {

plugins/next.config.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ const nextConfig = {
2929
}
3030
}),
3131
{
32-
key: 'X-Frame-Options',
33-
value: 'SAMEORIGIN'
32+
key: 'Content-Security-Policy',
33+
value: 'frame-ancestors *'
3434
},
3535
{ key: 'Cross-Origin-Opener-Policy', value: isProd ? 'same-origin' : 'unsafe-none' },
3636
{ key: 'Cross-Origin-Embedder-Policy', value: isProd ? 'same-origin' : 'unsafe-none' }

0 commit comments

Comments
 (0)