mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-26 05:19:44 +09:00
fix: csp regressions (#38047)
fix #37257 , all details are in the comments
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
Vendored
+5
-5
@@ -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
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -14,5 +14,3 @@ window.config = {
|
||||
i18n: {},
|
||||
frontendInited: false,
|
||||
};
|
||||
|
||||
window.testModules = {};
|
||||
|
||||
Reference in New Issue
Block a user