Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions web_src/js/markup/render-iframe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {navigateToIframeLink, safeLinkHref} from './render-iframe.ts';

test('safeLinkHref', () => {
expect(safeLinkHref('http://example.com')).toBe('http://example.com/');
expect(safeLinkHref('https://example.com')).toBe('https://example.com/');
expect(safeLinkHref('/path')).toBe('http://localhost:3000/path');
// eslint-disable-next-line no-script-url
expect(safeLinkHref('javascript:void(0);')).toBeNull();
expect(safeLinkHref('data:image/svg+xml;utf8,<svg></svg>')).toBeNull();
// for safety and consistency, it converts non-string input to string, just like `window.location.href = 0`
expect(safeLinkHref(0)).toBe('http://localhost:3000/0');
expect(safeLinkHref({})).toBe('http://localhost:3000/[object%20Object]');
expect(safeLinkHref(null)).toBe('http://localhost:3000/null');
});

describe('navigateToIframeLink', () => {
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
const assignSpy = vi.spyOn(window.location, 'assign').mockImplementation(() => undefined);

test('safe links', () => {
navigateToIframeLink('http://example.com', '_blank');
expect(openSpy).toHaveBeenCalledWith('http://example.com/', '_blank', 'noopener,noreferrer');
vi.clearAllMocks();

navigateToIframeLink('https://example.com', '_self');
expect(assignSpy).toHaveBeenCalledWith('https://example.com/');
vi.clearAllMocks();

navigateToIframeLink('https://example.com', null);
expect(assignSpy).toHaveBeenCalledWith('https://example.com/');
vi.clearAllMocks();

navigateToIframeLink('/path', '');
expect(assignSpy).toHaveBeenCalledWith('http://localhost:3000/path');
vi.clearAllMocks();

// input can be any type & any value, keep the same behavior as `window.location.href = 123`
navigateToIframeLink(123, {});
expect(assignSpy).toHaveBeenCalledWith('http://localhost:3000/123');
vi.clearAllMocks();
});

test('unsafe links', () => {
window.location.href = 'http://localhost:3000/';

// eslint-disable-next-line no-script-url
navigateToIframeLink('javascript:void(0);', '_blank');
expect(openSpy).toHaveBeenCalledTimes(0);
expect(assignSpy).toHaveBeenCalledTimes(0);
expect(window.location.href).toBe('http://localhost:3000/');
vi.clearAllMocks();

navigateToIframeLink('data:image/svg+xml;utf8,<svg></svg>', '');
expect(openSpy).toHaveBeenCalledTimes(0);
expect(assignSpy).toHaveBeenCalledTimes(0);
expect(window.location.href).toBe('http://localhost:3000/');
vi.clearAllMocks();
});
});
34 changes: 29 additions & 5 deletions web_src/js/markup/render-iframe.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,47 @@
import {generateElemId, queryElemChildren} from '../utils/dom.ts';
import {isDarkTheme} from '../utils.ts';

// arguments can be any type & any value, they are from "message" event's data
export function safeLinkHref(link: any): string | null {
try {
const url = new URL(`${link}`, window.location.href);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
console.error(`Unsupported link protocol: ${link}`);
return null;
}
return url.href;
} catch (e) {
console.error(`Failed to parse link: ${link}, error: ${e}`);
return null;
}
}

// arguments can be any type & any value, they are from "message" event's data
export function navigateToIframeLink(unsafeLink: any, target: any) {
const linkHref = safeLinkHref(unsafeLink);
if (linkHref === null) return;
if (target === '_blank') {
window.open(linkHref, '_blank', 'noopener,noreferrer');
return;
}
// treat all other targets including ("_top", "_self", etc) as same tab navigation
window.location.assign(linkHref);
}

async function loadRenderIframeContent(iframe: HTMLIFrameElement) {
const iframeSrcUrl = iframe.getAttribute('data-src')!;
if (!iframe.id) iframe.id = generateElemId('gitea-iframe-');

window.addEventListener('message', (e) => {
if (e.source !== iframe.contentWindow) return;
if (!e.data?.giteaIframeCmd || e.data?.giteaIframeId !== iframe.id) return;
const cmd = e.data.giteaIframeCmd;
if (cmd === 'resize') {
// TODO: sometimes the reported iframeHeight is not the size we need, need to figure why. Example: openapi swagger.
// As a workaround, add some pixels here.
iframe.style.height = `${e.data.iframeHeight + 2}px`;
} else if (cmd === 'open-link') {
if (e.data.anchorTarget === '_blank') {
window.open(e.data.openLink, '_blank');
} else {
window.location.href = e.data.openLink;
}
navigateToIframeLink(e.data.openLink, e.data.anchorTarget);
} else {
throw new Error(`Unknown gitea iframe cmd: ${cmd}`);
}
Expand Down