@cocreate/elements 1.42.8 → 1.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/fetchSrc.js CHANGED
@@ -2,286 +2,313 @@ import observer from "@cocreate/observer";
2
2
  import { getRelativePath, ObjectId } from "@cocreate/utils";
3
3
 
4
4
  const selector =
5
- "[src]:not(img, video, audio, script, input, iframe, frame, link, source), [source], [stream]";
6
- const textExtensions = ['html','htm','xhtml','css','js','mjs','cjs','json','jsonc','json5','xml','xsd','xsl','xslt','svg','md','markdown','scss','sass','less','vue','jsx','tsx','graphql','gql','htc','webmanifest','txt','csv','tsv','yaml','yml','toml','ini','log','dat','env','c','cc','cpp','cxx','h','hh','hpp','hxx','m','mm','cs','java','jsp','properties','py','pyc','pyd','pyo','rb','erb','php','phtml','php3','php4','php5','php7','phps','go','rs','swift','kt','kts','ts','pl','pm','pod','sh','bash','zsh','ksh','csh','tcsh','ps1','psm1','psd1','bat','cmd','sql','r','Rprofile','Rmd','lua','hs','lhs','erl','hrl','scala','sbt','d','f','for','f77','f90','f95','f03','f08','adb','ads','cbl','cob','cpy','lisp','lsp','scm','ss','pro','plg','vb','vbs','dart','asm','s','conf','cfg','config','ini','env','npmrc','yarnrc','gitattributes','gitignore','gitmodules','gitkeep','editorconfig','prettierrc','eslintrc','eslintignore','browserslistrc','package.json','package-lock.json','tsconfig.json','babelrc','vite.config.js','rollup.config.js','webpack.config.js','procfile','dockerfile','dockerignore','makefile','gradle','groovy','pom','properties','service','target','rst','adoc','asciidoc','tex','latex','textile','org','wiki','nfo','rtf','gcode','patch','diff','log','reg','map','sub','srt','dic','wml','rss','atom','gpx','kml','kmz','vcf','vcard','ics','ical','ps','eps','mf','lic','license','pem','key','crt','cert','pub','tpl','liquid','mustache','hbs','twig','blade.php','ejs','coffee','litcoffee','jl','nim','zig','wgsl']; const initializing = new Map();
5
+ "[src]:not(img, video, audio, script, input, iframe, frame, link, source), [source], [stream]";
6
+
7
+ const initializing = new WeakMap();
8
+
9
+ /**
10
+ * Validates whether a given URL string points to the current origin.
11
+ * Relative paths (e.g., "/partial.html") resolve to the same origin.
12
+ * @param {string} urlStr - The URL to validate
13
+ * @returns {boolean} True if safe/same-origin, false otherwise
14
+ */
15
+ function isSameOrigin(urlStr) {
16
+ try {
17
+ // Resolve the URL against the current document's origin.
18
+ const url = new URL(urlStr, window.location.origin);
19
+ return url.origin === window.location.origin;
20
+ } catch (err) {
21
+ console.error("Invalid URL format:", urlStr, err);
22
+ return false;
23
+ }
24
+ }
7
25
 
8
26
  function init(elements) {
9
- if (elements && !Array.isArray(elements)) elements = [elements];
27
+ if (elements && !Array.isArray(elements)) elements = [elements];
10
28
 
11
- if (!elements) elements = document.querySelectorAll(selector);
29
+ if (!elements) elements = document.querySelectorAll(selector);
12
30
 
13
- initElements(elements);
31
+ initElements(elements);
14
32
  }
15
33
 
16
34
  async function initElements(elements) {
17
- for (let element of elements) {
18
- let src =
19
- element.getAttribute("src") ||
20
- element.getAttribute("source") ||
21
- element.getAttribute("stream");
22
-
23
- if (!src || /{{\s*([\w\W]+)\s*}}/g.test(src)) continue;
24
-
25
- let initialize = initializing.get(element);
26
- if (initialize && initialize.src === src) continue;
27
-
28
- initializing.set(element, { src });
29
- try {
30
- let state = localStorage.getItem("state"); // Assume this is your state object
31
- let response = await fetch(src, {
32
- headers: {
33
- "X-State": state // Custom header to send state
34
- }
35
- });
36
-
37
- if (element.tagName === "AUDIO" || element.tagName === "VIDEO") {
38
- initMediaSource(element, src);
39
- } else {
40
- if (src && !textExtensions.includes(src.split('.').pop())) continue
41
-
42
- if (element.hasAttribute("rendered")) {
43
- element.removeAttribute("rendered");
44
- // continue;
45
- }
46
- let text = await response.text();
47
- if (text) {
48
- let path = "";
49
- let pathElement = element.closest("[path]");
50
- if (pathElement) {
51
- path =
52
- pathElement.getAttribute("path") ||
53
- getRelativePath();
54
- } else {
55
- path = getRelativePath();
56
- }
57
- // Replace $relativePath in the fetched text
58
- if (path) {
59
- text = text.replaceAll(/\$relativePath\/?/g, path);
60
- }
61
- // Replace ObjectId() with a new ObjectId
62
- text = text.replaceAll("ObjectId()", () => {
63
- // Generate a NEW ObjectId inside the function
64
- return ObjectId().toString(); // Return its string representation
65
- });
66
- element.setValue(text);
67
- }
68
- }
69
- } catch (err) {
70
- console.log("FetchSrc error:" + err);
71
- }
72
- }
35
+ for (let element of elements) {
36
+ let src =
37
+ element.getAttribute("src") ||
38
+ element.getAttribute("source") ||
39
+ element.getAttribute("stream");
40
+
41
+ if (!src || /{{\s*([\w\W]+)\s*}}/g.test(src)) continue;
42
+
43
+ let initialize = initializing.get(element);
44
+ if (initialize && initialize.src === src) continue;
45
+
46
+ initializing.set(element, { src });
47
+ try {
48
+ // --- SECURITY FIX: Conditional Sensitive Headers ---
49
+ // Only attach the local state header if fetching from the same origin.
50
+ let headers = {};
51
+ if (isSameOrigin(src)) {
52
+ let state = localStorage.getItem("state");
53
+ if (state) {
54
+ headers["X-State"] = state;
55
+ }
56
+ }
57
+
58
+ let response = await fetch(src, { headers });
59
+
60
+ if (response.status === 404 || response.status === 403) {
61
+ element.setAttribute("error", response.status);
62
+ continue;
63
+ }
64
+
65
+ if (element.tagName === "AUDIO" || element.tagName === "VIDEO") {
66
+ // Media streams can safely load cross-origin without risk of DOM-based XSS
67
+ initMediaSource(element, src);
68
+ } else {
69
+ // --- SECURITY FIX: Cross-Origin HTML Injection Prevention ---
70
+ // Only allow rendering the fetched response directly into the HTML tree if same-origin.
71
+ if (!isSameOrigin(src)) {
72
+ console.warn(`[CoCreateSrc] Blocked inserting fetched text into HTML from cross-origin: ${src}`);
73
+ element.setAttribute("error", "403-forbidden-cross-origin-html");
74
+ continue;
75
+ }
76
+
77
+ if (element.hasAttribute("rendered")) {
78
+ element.removeAttribute("rendered");
79
+ }
80
+ let text = await response.text();
81
+ if (text) {
82
+ let path = "";
83
+ let pathElement = element.closest("[path]");
84
+ if (pathElement) {
85
+ path =
86
+ pathElement.getAttribute("path") ||
87
+ getRelativePath();
88
+ } else {
89
+ path = getRelativePath();
90
+ }
91
+ // Replace $relativePath in the fetched text
92
+ if (path) {
93
+ text = text.replaceAll(/\$relativePath\/?/g, path);
94
+ }
95
+ // Replace ObjectId() with a new ObjectId
96
+ text = text.replaceAll("ObjectId()", () => {
97
+ return ObjectId().toString();
98
+ });
99
+ element.setValue(text);
100
+ }
101
+ }
102
+ } catch (err) {
103
+ console.log("FetchSrc error:" + err);
104
+ }
105
+ }
73
106
  }
74
107
 
75
108
  async function initMediaSource(element, src) {
76
- try {
77
- let mediaConfig = await response.json();
78
- let sourceBuffer;
79
-
80
- const mediaSource = new MediaSource();
81
- element.src = URL.createObjectURL(mediaSource);
82
- mediaSource.addEventListener("sourceopen", async () => {
83
- sourceBuffer = mediaSource.addSourceBuffer(
84
- `${mediaConfig["content-type"]}; codecs="${mediaConfig.codecs}"`
85
- );
86
-
87
- sourceBuffer.addEventListener("updateend", () => {
88
- console.log(
89
- "Update ended, buffered ranges:",
90
- sourceBuffer.buffered
91
- );
92
- if (
93
- !sourceBuffer.updating &&
94
- mediaSource.readyState === "open"
95
- ) {
96
- mediaSource.endOfStream();
97
-
98
- element.addEventListener("ended", () => {
99
- URL.revokeObjectURL(element.src);
100
- });
101
- }
102
- });
103
-
104
- sourceBuffer.addEventListener("error", (e) => {
105
- console.error("SourceBuffer error event:", e);
106
- console.log("MediaSource readyState:", mediaSource.readyState);
107
- console.log("SourceBuffer updating:", sourceBuffer.updating);
108
- console.log(
109
- "SourceBuffer buffered ranges:",
110
- sourceBuffer.buffered
111
- );
112
- // Any other state information you can log
113
- });
114
-
115
- getSegment(sourceBuffer, mediaConfig.segments[0]);
116
- });
117
-
118
- mediaSource.addEventListener("sourceended", () => {
119
- console.log("MediaSource ended event fired");
120
- });
121
-
122
- mediaSource.addEventListener("sourceclose", () => {
123
- console.log("MediaSource close event fired");
124
- });
125
-
126
- // Event listener for seeking
127
- element.addEventListener("seeking", () => {
128
- let currentTime = element.currentTime;
129
- let chunkIndex = Math.floor(currentTime / chunkDuration);
130
- });
131
-
132
- // Event listener for seeked
133
- element.addEventListener("seeked", () => {
134
- let currentTime = element.currentTime;
135
- if (isBuffered(element, currentTime)) return;
136
-
137
- let query = {
138
- start: { $lte: currentTime },
139
- end: { $gte: currentTime }
140
- };
141
-
142
- let segments = queryData(mediaConfig.segments, query);
143
- for (let segment of segments) {
144
- getSegment(sourceBuffer, segment);
145
- }
146
- });
147
-
148
- element.addEventListener("timeupdate", function () {
149
- const bufferEnd = element.buffered.end(0);
150
- const currentTime = element.currentTime;
151
- const threshold = 10; // seconds before buffer end to fetch the next segment
152
-
153
- if (bufferEnd - currentTime < threshold) {
154
- // Time to fetch the next segment
155
- getSegment(sourceBuffer, mediaConfig);
156
- }
157
- });
158
- } catch (error) {
159
- element.src = src;
160
- // let blob = await response.json();
161
- // URL.createObjectURL(blob);
162
- }
109
+ try {
110
+ let response = await fetch(src);
111
+ let mediaConfig = await response.json();
112
+ let sourceBuffer;
113
+
114
+ const mediaSource = new MediaSource();
115
+ element.src = URL.createObjectURL(mediaSource);
116
+ mediaSource.addEventListener("sourceopen", async () => {
117
+ sourceBuffer = mediaSource.addSourceBuffer(
118
+ `${mediaConfig["content-type"]}; codecs="${mediaConfig.codecs}"`
119
+ );
120
+
121
+ sourceBuffer.addEventListener("updateend", () => {
122
+ console.log(
123
+ "Update ended, buffered ranges:",
124
+ sourceBuffer.buffered
125
+ );
126
+ if (
127
+ !sourceBuffer.updating &&
128
+ mediaSource.readyState === "open"
129
+ ) {
130
+ mediaSource.endOfStream();
131
+
132
+ element.addEventListener("ended", () => {
133
+ URL.revokeObjectURL(element.src);
134
+ });
135
+ }
136
+ });
137
+
138
+ sourceBuffer.addEventListener("error", (e) => {
139
+ console.error("SourceBuffer error event:", e);
140
+ console.log("MediaSource readyState:", mediaSource.readyState);
141
+ console.log("SourceBuffer updating:", sourceBuffer.updating);
142
+ console.log(
143
+ "SourceBuffer buffered ranges:",
144
+ sourceBuffer.buffered
145
+ );
146
+ });
147
+
148
+ getSegment(sourceBuffer, mediaConfig.segments[0]);
149
+ });
150
+
151
+ mediaSource.addEventListener("sourceended", () => {
152
+ console.log("MediaSource ended event fired");
153
+ });
154
+
155
+ mediaSource.addEventListener("sourceclose", () => {
156
+ console.log("MediaSource close event fired");
157
+ });
158
+
159
+ // Event listener for seeking
160
+ element.addEventListener("seeking", () => {
161
+ let currentTime = element.currentTime;
162
+ let chunkIndex = Math.floor(currentTime / chunkDuration);
163
+ });
164
+
165
+ // Event listener for seeked
166
+ element.addEventListener("seeked", () => {
167
+ let currentTime = element.currentTime;
168
+ if (isBuffered(element, currentTime)) return;
169
+
170
+ let query = {
171
+ start: { $lte: currentTime },
172
+ end: { $gte: currentTime }
173
+ };
174
+
175
+ let segments = queryData(mediaConfig.segments, query);
176
+ for (let segment of segments) {
177
+ getSegment(sourceBuffer, segment);
178
+ }
179
+ });
180
+
181
+ element.addEventListener("timeupdate", function () {
182
+ const bufferEnd = element.buffered.end(0);
183
+ const currentTime = element.currentTime;
184
+ const threshold = 10;
185
+
186
+ if (bufferEnd - currentTime < threshold) {
187
+ getSegment(sourceBuffer, mediaConfig);
188
+ }
189
+ });
190
+ } catch (error) {
191
+ element.src = src;
192
+ }
163
193
  }
164
194
 
165
195
  async function getSegment(sourceBuffer, segment) {
166
- // TODO: use socket/crud/file with room using the url to get one or more segments and append
167
- let segments = [];
168
- let data;
169
- if (segment.array && segment.object) {
170
- data = { method: "object.read", ...segment };
171
- data = await crud.send(data);
172
- if (segment.key) {
173
- // TODO: utils.getValuefromObject
174
- if (Array.isArray(data.object[0][key]))
175
- segments.push(...data.object[0][key]);
176
- else segments.push(data.object[0][key]);
177
- } else segments.push(...data.object);
178
- segment.src = segments[0];
179
- segment.src = await segment.src.text();
180
- // TODO: handle segment array as
181
- } else if (segment.src) {
182
- // TODO: handle 404, or whether src was fetched or not
183
- segment.src = await fetch(segment.src);
184
- segment.src = await segment.src.text();
185
- }
186
-
187
- appendSegment(sourceBuffer, segment);
196
+ let segments = [];
197
+ let data;
198
+ if (segment.array && segment.object) {
199
+ data = { method: "object.read", ...segment };
200
+ data = await crud.send(data);
201
+ if (segment.key) {
202
+ if (Array.isArray(data.object[0][key]))
203
+ segments.push(...data.object[0][key]);
204
+ else segments.push(data.object[0][key]);
205
+ } else segments.push(...data.object);
206
+ segment.src = segments[0];
207
+ segment.src = await segment.src.text();
208
+ } else if (segment.src) {
209
+ // Media segments are safe to fetch cross-origin as they're not processed as HTML
210
+ let response = await fetch(segment.src);
211
+ if (response.status === 404 || response.status === 403) {
212
+ console.warn(`Segment fetch failed with status: ${response.status}`);
213
+ return;
214
+ }
215
+ segment.src = await response.text();
216
+ }
217
+
218
+ appendSegment(sourceBuffer, segment);
188
219
  }
189
220
 
190
221
  async function appendSegment(sourceBuffer, segment) {
191
- let arrayBuffer = segment.src;
192
- if (typeof segment.src === "string") {
193
- try {
194
- arrayBuffer = base64ToArrayBuffer(segment.src);
195
- } catch (e) {
196
- console.error("Base64 decode error:", e);
197
- }
198
- } else if (segment.src instanceof ArrayBuffer) {
199
- arrayBuffer = segment.src;
200
- } else if (segment.src instanceof Blob) {
201
- arrayBuffer = await segment.src.arrayBuffer();
202
- } else {
203
- arrayBuffer = base64ToArrayBuffer(segment.src);
204
-
205
- // arrayBuffer = await segment.src.arrayBuffer();
206
- }
207
-
208
- // Wait for the source buffer to be ready for updates
209
- if (sourceBuffer.updating)
210
- await new Promise((resolve) =>
211
- sourceBuffer.addEventListener("updateend", resolve, { once: true })
212
- );
213
-
214
- sourceBuffer.appendBuffer(arrayBuffer);
215
- console.log(sourceBuffer);
222
+ let arrayBuffer = segment.src;
223
+ if (typeof segment.src === "string") {
224
+ try {
225
+ arrayBuffer = base64ToArrayBuffer(segment.src);
226
+ } catch (e) {
227
+ console.error("Base64 decode error:", e);
228
+ }
229
+ } else if (segment.src instanceof ArrayBuffer) {
230
+ arrayBuffer = segment.src;
231
+ } else if (segment.src instanceof Blob) {
232
+ arrayBuffer = await segment.src.arrayBuffer();
233
+ } else {
234
+ arrayBuffer = base64ToArrayBuffer(segment.src);
235
+ }
236
+
237
+ if (sourceBuffer.updating)
238
+ await new Promise((resolve) =>
239
+ sourceBuffer.addEventListener("updateend", resolve, { once: true })
240
+ );
241
+
242
+ sourceBuffer.appendBuffer(arrayBuffer);
243
+ console.log(sourceBuffer);
216
244
  }
217
245
 
218
246
  function isBuffered(element, currentTime) {
219
- for (let i = 0; i < element.buffered.length; i++) {
220
- if (
221
- currentTime >= element.buffered.start(i) &&
222
- currentTime <= element.buffered.end(i)
223
- ) {
224
- return true; // The currentTime is within a buffered range
225
- }
226
- }
227
- return false; // The currentTime is not buffered
247
+ for (let i = 0; i < element.buffered.length; i++) {
248
+ if (
249
+ currentTime >= element.buffered.start(i) &&
250
+ currentTime <= element.buffered.end(i)
251
+ ) {
252
+ return true;
253
+ }
254
+ }
255
+ return false;
228
256
  }
229
257
 
230
258
  function base64ToBlob(base64, contentType = "") {
231
- const binaryString = atob(base64.split(",")[1]);
232
- const len = binaryString.length;
233
- const uint8Array = new Uint8Array(len);
234
- for (let i = 0; i < len; i++) {
235
- uint8Array[i] = binaryString.charCodeAt(i);
236
- }
237
- return new Blob([uint8Array], { type: contentType });
259
+ const binaryString = atob(base64.split(",")[1]);
260
+ const len = binaryString.length;
261
+ const uint8Array = new Uint8Array(len);
262
+ for (let i = 0; i < len; i++) {
263
+ uint8Array[i] = binaryString.charCodeAt(i);
264
+ }
265
+ return new Blob([uint8Array], { type: contentType });
238
266
  }
239
267
 
240
268
  function base64ToArrayBuffer(base64) {
241
- const binaryString = window.atob(base64.split(",")[1]); // Decode base64
242
- const len = binaryString.length;
243
- const bytes = new Uint8Array(len);
244
- for (let i = 0; i < len; i++) {
245
- bytes[i] = binaryString.charCodeAt(i);
246
- }
247
- return bytes.buffer; // Return ArrayBuffer
269
+ const binaryString = window.atob(base64.split(",")[1]);
270
+ const len = binaryString.length;
271
+ const bytes = new Uint8Array(len);
272
+ for (let i = 0; i < len; i++) {
273
+ bytes[i] = binaryString.charCodeAt(i);
274
+ }
275
+ return bytes.buffer;
248
276
  }
249
277
 
250
278
  observer.init({
251
- name: "CoCreateSrc",
252
- types: ["addedNodes"],
253
- selector: selector,
254
- callback: function (mutation) {
255
- init(mutation.target);
256
- }
279
+ name: "CoCreateSrc",
280
+ types: ["addedNodes"],
281
+ selector: selector,
282
+ callback: function (mutation) {
283
+ init(mutation.target);
284
+ }
257
285
  });
258
286
 
259
287
  observer.init({
260
- name: "CoCreateSrcOperator",
261
- types: ["addedNodes"],
262
- selector: "img",
263
- // selector: "img, video, audio, script, input, iframe, frame, link, source",
264
- callback: function (mutation) {
265
- let src = mutation.target.getAttribute("src");
266
- if (src) {
267
- mutation.target.setAttribute("src", src);
268
- }
269
- }
288
+ name: "CoCreateSrcOperator",
289
+ types: ["addedNodes"],
290
+ selector: "img",
291
+ callback: function (mutation) {
292
+ let src = mutation.target.getAttribute("src");
293
+ if (src) {
294
+ mutation.target.setAttribute("src", src);
295
+ }
296
+ }
270
297
  });
271
298
 
272
299
  observer.init({
273
- name: "CoCreateSrcAttributes",
274
- types: ["attributes"],
275
- attributeFilter: ["src"],
276
- selector: selector,
277
- callback: function (mutation) {
278
- if (
279
- mutation.oldValue !==
280
- mutation.target.getAttribute(mutation.attributeName)
281
- ) {
282
- init(mutation.target);
283
- }
284
- }
300
+ name: "CoCreateSrcAttributes",
301
+ types: ["attributes"],
302
+ attributeFilter: ["src"],
303
+ selector: selector,
304
+ callback: function (mutation) {
305
+ if (
306
+ mutation.oldValue !==
307
+ mutation.target.getAttribute(mutation.attributeName)
308
+ ) {
309
+ init(mutation.target);
310
+ }
311
+ }
285
312
  });
286
313
 
287
- init();
314
+ init();