fix: csp regressions (#38047)

fix #37257 , all details are in the comments
This commit is contained in:
wxiaoguang
2026-06-12 08:36:05 +08:00
committed by GitHub
parent e473505d64
commit 4f4a0a79ac
27 changed files with 159 additions and 159 deletions
-1
View File
@@ -52,7 +52,6 @@
@import "./markup/content.css";
@import "./markup/codeblock.css";
@import "./markup/codepreview.css";
@import "./markup/asciicast.css";
@import "./font_i18n.css";
@import "./base.css";
-10
View File
@@ -1,10 +0,0 @@
.asciinema-player-container {
width: 100%;
height: auto;
}
/* Related: https://github.com/asciinema/asciinema-player/blob/develop/src/components/Terminal.js : <div class="ap-term" ...>
Old PR: Fix UI regression of asciinema player https://github.com/go-gitea/gitea/pull/26159 */
.ap-term {
overflow: hidden !important;
}
-4
View File
@@ -210,10 +210,6 @@ td .commit-summary {
overflow: auto;
}
.non-diff-file-content .asciicast {
padding: 0 !important;
}
.repo-editor-header {
/* it should match ".repo-button-row" so the tree toggle button stays aligned */
margin: 8px 0;
+11 -5
View File
@@ -8,6 +8,7 @@ type LazyLoadFunc = () => Promise<{frontendRender: FrontendRenderFunc}>;
const frontendPlugins: Record<string, LazyLoadFunc> = {
'viewer-3d': () => import('./render/plugins/frontend-viewer-3d.ts'),
'openapi-swagger': () => import('./render/plugins/frontend-openapi-swagger.ts'),
'asciicast': () => import('./render/plugins/frontend-asciicast.ts'),
};
class Options implements FrontendRenderOptions {
@@ -44,23 +45,28 @@ async function initFrontendExternalRender() {
const viewerContainer = document.querySelector<HTMLElement>('#frontend-render-viewer')!;
const renderNames = viewerContainer.getAttribute('data-frontend-renders')!.split(' ');
const fileTreePath = viewerContainer.getAttribute('data-file-tree-path')!;
viewerContainer.setAttribute('data-window-origin', window.origin); // mainly for testing purpose
const fileDataElem = document.querySelector<HTMLTextAreaElement>('#frontend-render-data')!;
fileDataElem.remove();
const fileDataContent = fileDataElem.value;
const fileDataEncoding = fileDataElem.getAttribute('data-content-encoding')!;
const opts = new Options(viewerContainer, fileTreePath, fileDataEncoding, fileDataContent);
let found = false;
let renderName = '', rendered = false;
for (const name of renderNames) {
if (!(name in frontendPlugins)) continue;
const plugin = await frontendPlugins[name]();
found = true;
if (await plugin.frontendRender(opts)) break;
renderName = name;
rendered = await plugin.frontendRender(opts);
if (rendered) break;
}
if (!found) {
if (!renderName) {
viewerContainer.textContent = 'No frontend render plugin found for this file, but backend declares that there must be one, there must be a bug';
} else if (!rendered) {
viewerContainer.textContent = `Failed to render by ${renderName}`;
} else {
viewerContainer.setAttribute('data-frontend-render-name', renderName); // succeeded render, mainly for testing purpose
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import './external-render-helper.ts';
test('isValidCssColor', async () => {
const isValidCssColor = window.testModules.externalRenderHelper!.isValidCssColor;
const isValidCssColor = window.giteaExternalRenderHelper!.isValidCssColor;
expect(isValidCssColor(null)).toBe(false);
expect(isValidCssColor('')).toBe(false);
+6 -8
View File
@@ -50,12 +50,12 @@ body { background: ${backgroundColor}; }
}
const iframeId = queryParams.get('gitea-iframe-id');
if (iframeId) {
// iframe is in different origin, so we need to use postMessage to communicate
const postIframeMsg = (cmd: string, data: Record<string, any> = {}) => {
window.parent.postMessage({giteaIframeCmd: cmd, giteaIframeId: iframeId, ...data}, '*');
};
// iframe is in different origin, so we need to use postMessage to communicate
const postIframeMsg = (cmd: string, data: Record<string, any> = {}) => {
window.parent.postMessage({giteaIframeCmd: cmd, giteaIframeId: iframeId, ...data}, '*');
};
if (iframeId) {
const updateIframeHeight = () => {
if (!document.body) return; // the body might not be available when this function is called
// Use scrollHeight to get the full content height, even when CSS sets html/body to height:100%
@@ -90,6 +90,4 @@ if (iframeId) {
});
}
if (window.testModules) {
window.testModules.externalRenderHelper = {isValidCssColor};
}
window.giteaExternalRenderHelper = {isValidCssColor, queryParams, postIframeMsg};
+5 -5
View File
@@ -68,13 +68,13 @@ interface Window {
turnstile: any,
hcaptcha: any,
// Make IIFE private functions can be tested in unit tests, without exposing the IIFE module to global scope.
// Make IIFE private functions can be managed by us in our scope, without exposing the IIFE module to global scope.
// Otherwise, when using "export" in IIFE code, the compiled JS will inject global "var externalRenderHelper = ..."
// which is not expected and may cause conflicts with other modules.
testModules: {
externalRenderHelper?: {
isValidCssColor(s: string | null): boolean,
}
giteaExternalRenderHelper?: {
isValidCssColor(s: string | null): boolean,
queryParams: URLSearchParams,
postIframeMsg(cmd: string, data: Record<string, any> = {}),
}
// do not add more properties here unless it is a must
-16
View File
@@ -1,16 +0,0 @@
import {queryElems} from '../utils/dom.ts';
export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise<void> {
queryElems(elMarkup, '.asciinema-player-container', async (el) => {
const [player] = await Promise.all([
import('asciinema-player'),
import('asciinema-player/dist/bundle/asciinema-player.css'),
]);
player.create(el.getAttribute('data-asciinema-player-src')!, el, {
// poster (a preview frame) to display until the playback is started.
// Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more.
poster: 'npt:1:0:0',
});
});
}
-2
View File
@@ -1,7 +1,6 @@
import {initMarkupCodeMermaid} from './mermaid.ts';
import {initMarkupCodeMath} from './math.ts';
import {initMarkupCodeCopy} from './codecopy.ts';
import {initMarkupRenderAsciicast} from './asciicast.ts';
import {initMarkupTasklist} from './tasklist.ts';
import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts';
import {initExternalRenderIframe} from './render-iframe.ts';
@@ -24,6 +23,5 @@ export function initMarkupContent(): void {
initMarkupTasklist(el);
initMarkupCodeMermaid(el);
initMarkupCodeMath(el);
initMarkupRenderAsciicast(el);
});
}
+27 -6
View File
@@ -1,7 +1,6 @@
import {generateElemId} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
import {isDarkTheme} from '../utils.ts';
import {GET} from '../modules/fetch.ts';
function safeRenderIframeLink(link: any): string | null {
try {
@@ -65,9 +64,31 @@ export async function initExternalRenderIframe(iframe: HTMLIFrameElement) {
u.searchParams.set('gitea-iframe-id', iframe.id);
u.searchParams.set('gitea-iframe-bgcolor', getRealBackgroundColor(iframe));
// It must use "srcdoc" here, because our backend always sends CSP sandbox directive for the rendered content
// (to protect from XSS risks), so we can't use "src" to load the content directly, otherwise there will be console errors like:
// Unsafe attempt to load URL http://localhost:3000/test from frame with URL http://localhost:3000/test
const resp = await GET(u.href);
iframe.srcdoc = await resp.text();
// There are 3 kinds of external render modes:
// * external frontend render:
// * parent page creates iframe, iframe navigates to render page
// * render generates frame page with external-render-helper (injected), external-render-frontend and file content (hidden textarea)
// * frame page executes external-render-frontend JS code to finds a frontend plugin to render
// * external backend render (HTML)
// * parent page creates iframe, iframe navigates to render page
// * render executes command to generate rendered HTML content with external-render-helper (injected)
// * frame page displays the rendered content
// * external backend render (non-HTML, e.g.: PDF, image)
// * parent page creates iframe, iframe navigates to render page
// * render executes command to generate rendered content
// * response header is automatically detected from rendered content
// It must use "src" here, because the frame content should not inherit parent's CSP.
// Otherwise, "srcdoc" makes the frame content inherit the parent's CSP,
// then some renders like "asciicast (asciinema)" which require "unsafe-eval" won't work.
//
// When using "src", Chrome can report false-alarm error like:
// * Unsafe attempt to load URL http://localhost/owner/repo/render/branch/main/file from frame with URL http://localhost/owner/repo/render/branch/main/file. Domains, protocols and ports must match.
// (only for the first time that the developer opens the browser console)
// Such error log can also appear even if you access the link "http://.../owner/repo/render/branch/main/file" directly.
// Everything just works, it is just a false-alarm caused by Chrome's Developer Tools, so such error log can be ignored.
//
// Another reason for why "src" is a must: if the render outputs non-HTML contents like PDF or image,
// Only "src" can correctly load and display the rendered content, "srcdoc" won't work.
iframe.src = u.href;
}
@@ -0,0 +1,23 @@
import type {FrontendRenderFunc} from '../plugin.ts';
export const frontendRender: FrontendRenderFunc = async (opts): Promise<boolean> => {
try {
const [player] = await Promise.all([
import('asciinema-player'),
import('asciinema-player/dist/bundle/asciinema-player.css'),
]);
player.create({data: opts.contentString()}, opts.container, {
// poster (a preview frame) to display until the playback is started.
// Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more.
poster: 'npt:1:0:0',
});
// Related: https://github.com/asciinema/asciinema-player/blob/develop/src/components/Terminal.js : <div class="ap-term" ...>
// Old PR: Fix UI regression of asciinema player https://github.com/go-gitea/gitea/pull/26159
opts.container.querySelector<HTMLElement>('.ap-term')!.style.overflow = 'hidden';
opts.container.querySelector<HTMLElement>('.ap-player')!.style.borderRadius = '0';
return true;
} catch (error) {
console.error(error);
return false;
}
};
-2
View File
@@ -14,5 +14,3 @@ window.config = {
i18n: {},
frontendInited: false,
};
window.testModules = {};