fix: both importers drop a link whose scheme the editor never opens
The render-time guard of the link mark was the only thing between a file's javascript: href and the page; the importers now read such a link as text.
This commit is contained in:
1 parent
efed52440d
commit
9f2da39048
3 files changed
+41
-3
No files matched your search
@@ -3,6 +3,7 @@ import { DocxStyles, parseRunProps, mergeRunProps, readNumPr, readTabStops, togg
|
||||
import { lengthToPt, WATERMARK_NAME } from './styleResolver';
|
||||
import { HEADING_STYLE_OVERRIDES, MAX_HEADING_LEVEL, normalizeColor } from '../export/odt';
|
||||
import { PLACEHOLDER_SDT_TAG } from '../export/docx';
|
||||
import { isAllowedUri } from '@tiptap/extension-link';
|
||||
import { FOLD_MARK_NAME } from '../storage/foldMarks';
|
||||
import { builtinStyleSheet, DEFAULT_STYLE, type ParaProps, type Style, type StyleSheet, type TextProps } from '../styles/styleSheet';
|
||||
import { DEFAULT_OUTLINE_LEVEL, MAX_OUTLINE_LEVELS, type OutlineNumbering } from '../styles/outlineNumbering';
|
||||
@@ -1661,7 +1662,9 @@ function convertInline(p: Element, ctx: Ctx, baseRun: RunProps, defaults: BlockD
|
||||
const rid = el.getAttributeNS(R, 'id');
|
||||
// No relationship id: an internal link to a bookmark in this document.
|
||||
const anchor = el.getAttributeNS(W, 'anchor');
|
||||
const href = rid ? ctx.rels.get(rid)?.target : anchor ? `#${anchor}` : undefined;
|
||||
let href = rid ? ctx.rels.get(rid)?.target : anchor ? `#${anchor}` : undefined;
|
||||
// A scheme the editor would never open (javascript:) stops here, not at render time.
|
||||
if (!isAllowedUri(href)) href = undefined;
|
||||
// A revision wraps its runs inside the link element, so walk both shapes.
|
||||
for (const child of Array.from(el.children)) {
|
||||
if (child.localName === 'r') handleRun(child, href);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { unzipSync, strFromU8 } from 'fflate';
|
||||
import { StyleResolver, NS, WATERMARK_NAME, lengthToPt, lengthToCm, layerTextProps, type PropMap } from './styleResolver';
|
||||
import { HEADING_STYLE_OVERRIDES, MAX_HEADING_LEVEL, ODF_LOOK_ATTRS, normalizeColor } from '../export/odt';
|
||||
import { isAllowedUri } from '@tiptap/extension-link';
|
||||
import { builtinStyleSheet, DEFAULT_STYLE, type ParaProps, type Style, type StyleSheet, type TextProps } from '../styles/styleSheet';
|
||||
import { DEFAULT_OUTLINE_LEVEL, MAX_OUTLINE_LEVELS, type OutlineLevel, type OutlineNumbering } from '../styles/outlineNumbering';
|
||||
import { LIST_LEVEL_STEP_CM, MAX_LIST_LEVELS, type ListLevelStyle, type ListStyle } from '../styles/listStyles';
|
||||
@@ -2242,9 +2243,10 @@ function convertInline(root: Element, ctx: Ctx, baseProps: PropMap, defaults: Bl
|
||||
}
|
||||
case 'a': {
|
||||
// ODF hyperlink → link mark on the contained text; an internal #bookmark
|
||||
// href is kept as-is and resolved against the document's bookmarks.
|
||||
// href is kept as-is and resolved against the document's bookmarks. A scheme
|
||||
// the editor would never open (javascript:) stops here, not at render time.
|
||||
const href = e.getAttributeNS(NS.xlink, 'href') ?? '';
|
||||
walk(e, props, href || linkHref);
|
||||
walk(e, props, isAllowedUri(href) ? href || linkHref : linkHref);
|
||||
continue;
|
||||
}
|
||||
case 'ruby': {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { importDocx } from '../../src/lib/import/docx';
|
||||
import { importOdt } from '../../src/lib/import/odt';
|
||||
import { buildDocx } from '../../src/lib/export/docx';
|
||||
import { buildOdt } from '../../src/lib/export/odt';
|
||||
|
||||
// A file can carry any href; one with a scheme the editor would never open is read as
|
||||
// plain text, so the boundary is the import and not a render-time guard.
|
||||
type N = { type: string; attrs?: any; content?: N[]; marks?: any[]; text?: string };
|
||||
|
||||
function links(node: N, out: string[] = []): string[] {
|
||||
for (const m of node.marks ?? []) if (m.type === 'link') out.push(m.attrs.href);
|
||||
for (const c of node.content ?? []) links(c, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
const doc: N = { type: 'doc', content: [{ type: 'paragraph', content: [
|
||||
{ type: 'text', text: 'run', marks: [{ type: 'link', attrs: { href: 'javascript:alert(1)' } }] },
|
||||
{ type: 'text', text: ' site', marks: [{ type: 'link', attrs: { href: 'https://example.com' } }] },
|
||||
{ type: 'text', text: ' here', marks: [{ type: 'link', attrs: { href: '#top' } }] },
|
||||
] }] };
|
||||
const margins = { top: 2, bottom: 2, left: 2, right: 2 };
|
||||
|
||||
describe('a link with a scheme the editor never opens is dropped on import', () => {
|
||||
it('through ODT', async () => {
|
||||
const back = importOdt(await buildOdt(doc as any, margins, 'portrait')).content as unknown as N;
|
||||
expect(links(back)).toEqual(['https://example.com', '#top']);
|
||||
});
|
||||
it('through DOCX', async () => {
|
||||
const back = importDocx(await buildDocx(doc as any, margins, 'portrait')).content as unknown as N;
|
||||
expect(links(back)).toEqual(['https://example.com', '#top']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user