Skip to content

Commit 0d2ff58

Browse files
committed
address some concerns regarding rendering svg and html content
1 parent e7c34e6 commit 0d2ff58

File tree

4 files changed

+98
-35
lines changed

4 files changed

+98
-35
lines changed

src/packages/frontend/client/project.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import httpApi from "./api";
5454
import { WebappClient } from "./client";
5555
import { throttle } from "lodash";
5656
import { writeFile, type WriteFileOptions } from "@cocalc/nats/files/write";
57+
import { readFile, type ReadFileOptions } from "@cocalc/nats/files/read";
5758

5859
export class ProjectClient {
5960
private client: WebappClient;
@@ -99,6 +100,18 @@ export class ProjectClient {
99100
return await writeFile(opts);
100101
};
101102

103+
// readFile -- read **arbitrarily large text or binary files**
104+
// from a project via a readable stream.
105+
// Look at the code below if you want to stream a file for memory
106+
// efficiency...
107+
readFile = async (opts: ReadFileOptions): Promise<Buffer> => {
108+
const chunks: Uint8Array[] = [];
109+
for await (const chunk of await readFile(opts)) {
110+
chunks.push(chunk);
111+
}
112+
return Buffer.concat(chunks);
113+
};
114+
102115
public async read_text_file({
103116
project_id,
104117
path,

src/packages/frontend/frame-editors/html-editor/iframe-html.tsx

Lines changed: 68 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import { debounce } from "lodash";
2626
import { React, ReactDOM, Rendered, CSS } from "../../app-framework";
2727
import { use_font_size_scaling } from "../frame-tree/hooks";
2828
import { EditorState } from "../frame-tree/types";
29-
import { raw_url } from "../frame-tree/util";
29+
import { useEffect, useRef, useState } from "react";
30+
import { webapp_client } from "@cocalc/frontend/webapp-client";
31+
import { Spin } from "antd";
3032

3133
interface Props {
3234
id: string;
@@ -42,6 +44,7 @@ interface Props {
4244
mode: "rmd" | undefined;
4345
style?: any; // style should be static; change does NOT cause update.
4446
derived_file_types: Set<string>;
47+
value?: string;
4548
}
4649

4750
function should_memoize(prev, next) {
@@ -73,15 +76,56 @@ export const IFrameHTML: React.FC<Props> = React.memo((props: Props) => {
7376
style,
7477
derived_file_types,
7578
tab_is_visible,
79+
value,
7680
} = props;
7781

78-
const rootEl = React.useRef(null);
79-
const iframe = React.useRef(null);
80-
const mounted = React.useRef(false);
82+
// during init definitely nothing available to show users; this
83+
// is only needed for rmd mode where an aux file loaded from server.
84+
const [init, setInit] = useState<boolean>(mode == "rmd");
85+
const [srcDoc, setSrcDoc] = useState<string | null>(null);
86+
87+
useEffect(() => {
88+
if (mode != "rmd") {
89+
setInit(false);
90+
return;
91+
}
92+
let actual_path = path;
93+
if (mode == "rmd" && derived_file_types != undefined) {
94+
if (derived_file_types.contains("html")) {
95+
// keep path as it is; don't remove this case though because of the else
96+
} else if (derived_file_types.contains("nb.html")) {
97+
actual_path = change_filename_extension(path, "nb.html");
98+
} else {
99+
setSrcDoc(null);
100+
}
101+
}
102+
103+
// read actual_path and set srcDoc to it.
104+
(async () => {
105+
let buf;
106+
try {
107+
buf = await webapp_client.project_client.readFile({
108+
project_id,
109+
path: actual_path,
110+
});
111+
} catch (err) {
112+
actions.set_error(`${err}`);
113+
return;
114+
} finally {
115+
// done -- we tried
116+
setInit(false);
117+
}
118+
setSrcDoc(buf.toString("utf8"));
119+
})();
120+
}, [reload, mode, path, derived_file_types]);
121+
122+
const rootEl = useRef(null);
123+
const iframe = useRef(null);
124+
const mounted = useRef(false);
81125
const scaling = use_font_size_scaling(font_size);
82126

83127
// once after mounting
84-
React.useEffect(function () {
128+
useEffect(function () {
85129
mounted.current = true;
86130
reload_iframe();
87131
set_iframe_style(scaling);
@@ -90,18 +134,18 @@ export const IFrameHTML: React.FC<Props> = React.memo((props: Props) => {
90134
};
91135
}, []);
92136

93-
React.useEffect(
137+
useEffect(
94138
function () {
95139
if (tab_is_visible) restore_scroll();
96140
},
97-
[tab_is_visible]
141+
[tab_is_visible],
98142
);
99143

100-
React.useEffect(
144+
useEffect(
101145
function () {
102146
set_iframe_style(scaling);
103147
},
104-
[scaling]
148+
[scaling],
105149
);
106150

107151
function click_iframe(): void {
@@ -132,7 +176,7 @@ export const IFrameHTML: React.FC<Props> = React.memo((props: Props) => {
132176
if (node != null && node.contentDocument != null) {
133177
node.contentDocument.addEventListener(
134178
"scroll",
135-
debounce(() => on_scroll(), 150)
179+
debounce(() => on_scroll(), 150),
136180
);
137181
}
138182
}
@@ -156,27 +200,25 @@ export const IFrameHTML: React.FC<Props> = React.memo((props: Props) => {
156200
}
157201

158202
function render_iframe() {
159-
let actual_path = path;
160-
if (mode == "rmd" && derived_file_types != undefined) {
161-
if (derived_file_types.contains("html")) {
162-
// keep path as it is; don't remove this case though because of the else
163-
} else if (derived_file_types.contains("nb.html")) {
164-
actual_path = change_filename_extension(path, "nb.html");
165-
} else {
166-
return render_no_html();
167-
}
203+
if (init) {
204+
// in the init phase.
205+
return (
206+
<div style={{ margin: "15px auto" }}>
207+
<Spin />
208+
</div>
209+
);
210+
}
211+
if (mode == "rmd" && srcDoc == null) {
212+
return render_no_html();
168213
}
169-
170-
// param below is just to avoid caching.
171-
const src = `${raw_url(project_id, actual_path)}?param=${reload}`;
172-
173214
return (
174215
<iframe
175216
ref={iframe}
176-
src={src}
217+
srcDoc={mode != "rmd" ? value : (srcDoc ?? "")}
218+
sandbox="allow-forms allow-scripts allow-presentation"
177219
width={"100%"}
178220
height={"100%"}
179-
style={{ border: 0, opacity: 0, ...style }}
221+
style={{ border: 0, ...style }}
180222
onLoad={iframe_loaded}
181223
/>
182224
);
@@ -208,7 +250,7 @@ export const IFrameHTML: React.FC<Props> = React.memo((props: Props) => {
208250
return (
209251
<div>
210252
<p>There is no rendered HTML file available.</p>
211-
{derived_file_types.size > 0 ? (
253+
{(derived_file_types?.size ?? 0) > 0 ? (
212254
<p>
213255
Instead, you might want to switch to the{" "}
214256
{list_alternatives(derived_file_types)} view by selecting it via the

src/packages/hub/proxy/handle-request.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { once } from "@cocalc/util/async-utils";
1616
import hasAccess from "./check-for-access-to-project";
1717
import mime from "mime-types";
1818

19+
const DANGEROUS_CONTENT_TYPE = new Set(["image/svg+xml", "text/html"]);
20+
1921
const logger = getLogger("proxy:handle-request");
2022

2123
interface Options {
@@ -103,7 +105,11 @@ export default function init({ projectControl, isPersonal }: Options) {
103105
const path = decodeURIComponent(url.slice(i + "files/".length, j));
104106
dbg("NATs: get", { project_id, path, compute_server_id, url });
105107
const fileName = path_split(path).tail;
106-
if (req.query.download != null) {
108+
const contentType = mime.lookup(fileName);
109+
if (
110+
req.query.download != null ||
111+
DANGEROUS_CONTENT_TYPE.has(contentType)
112+
) {
107113
const fileNameEncoded = encodeURIComponent(fileName)
108114
.replace(/['()]/g, escape)
109115
.replace(/\*/g, "%2A");
@@ -112,7 +118,7 @@ export default function init({ projectControl, isPersonal }: Options) {
112118
`attachment; filename*=UTF-8''${fileNameEncoded}`,
113119
);
114120
}
115-
res.setHeader("Content-type", mime.lookup(fileName));
121+
res.setHeader("Content-type", contentType);
116122
for await (const chunk of await readProjectFile({
117123
project_id,
118124
compute_server_id,

src/packages/nats/files/read.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,19 +137,21 @@ async function sendData(mesg, createReadStream) {
137137
}
138138
}
139139

140+
export interface ReadFileOptions {
141+
project_id: string;
142+
compute_server_id?: number;
143+
path: string;
144+
name?: string;
145+
maxWait?: number;
146+
}
147+
140148
export async function* readFile({
141149
project_id,
142150
compute_server_id = 0,
143151
path,
144152
name = "",
145153
maxWait = 1000 * 60 * 10, // 10 minutes
146-
}: {
147-
project_id: string;
148-
compute_server_id?: number;
149-
path: string;
150-
name?: string;
151-
maxWait?: number;
152-
}) {
154+
}: ReadFileOptions) {
153155
const { nc, jc } = await getEnv();
154156
const subject = getSubject({
155157
project_id,

0 commit comments

Comments
 (0)