1package cef
2
3import (
4 "strings"
5
6 "github.com/bnema/dumber/internal/infrastructure/webutil"
7)
8
9// trustedPageFetchBridgeBaseJS is the invariant fetch-bridge bundle injected on
10// external pages. It is assembled once from shared templates and then only the
11// bridge nonce placeholder is replaced per navigation.
12var trustedPageFetchBridgeBaseJS = buildTrustedPageFetchBridgeBaseJS()
13
14// rendererBridgeExtensionJS is assembled once for renderer-process bridge helpers.
15var rendererBridgeExtensionJS = buildRendererBridgeExtensionJS()
16
17func buildTrustedPageFetchBridgeBaseJS() string {
18 return clipboardSelectionFetchBridgeJSTemplate
19}
20
21const bridgeNoncePlaceholder = "__DUMBER_BRIDGE_NONCE__"
22
23func buildTrustedPageFetchBridgeJS(bridgeNonce string) string {
24 if strings.Contains(bridgeNonce, bridgeNoncePlaceholder) {
25 panic("bridge nonce contains reserved placeholder")
26 }
27 return strings.ReplaceAll(trustedPageFetchBridgeBaseJS, bridgeNoncePlaceholder, bridgeNonce)
28}
29
30func buildRendererBridgeExtensionJS() string {
31 return strings.ReplaceAll(rendererBridgeExtensionTemplateJS, "__DUMBER_POPUP_PROXY_BRIDGE_BODY__", "")
32}
33
34func buildPopupOpenerBridgeJS(bridgeNonce, parentURI string) string {
35 replacer := strings.NewReplacer(
36 "__DUMBER_BRIDGE_NONCE__", webutil.EscapeForJSString(bridgeNonce),
37 "__DUMBER_PARENT_URI__", webutil.EscapeForJSString(parentURI),
38 )
39 return replacer.Replace(popupOpenerBridgeTemplateJS)
40}
41
42// clipboardSelectionFetchBridgeJSTemplate keeps the lightweight fetch bridge
43// enabled while the native renderer bridge remains disabled due to an OSR
44// startup regression. It mirrors explicit clipboard text from DOM selections,
45// safe input/textarea selections, and async Clipboard API writes, and also
46// reasserts browser focus when an editable element gains DOM focus.
47const clipboardSelectionFetchBridgeJSTemplate = `(function(){
48 var bridgeNonce = '__DUMBER_BRIDGE_NONCE__';
49
50 function encodeBody(body) {
51 return typeof btoa === 'function' ? btoa(unescape(encodeURIComponent(body))) : '';
52 }
53
54 function inputType(node) {
55 return node && node.type ? String(node.type).toLowerCase() : 'text';
56 }
57
58 function isTextEditableInputType(type) {
59 switch ((type || 'text').toLowerCase()) {
60 case 'button':
61 case 'submit':
62 case 'reset':
63 case 'checkbox':
64 case 'radio':
65 case 'range':
66 case 'color':
67 case 'file':
68 case 'image':
69 case 'hidden':
70 return false;
71 default:
72 return true;
73 }
74 }
75
76 function isSelectionReadableInputType(type) {
77 return isTextEditableInputType(type) && type !== 'password';
78 }
79
80 function getActiveElementSelection() {
81 var el = document.activeElement;
82 if (!el || el.nodeType !== 1 || typeof el.value !== 'string') return '';
83 var tag = el.tagName;
84 if (el.disabled || el.readOnly) return '';
85 if (tag === 'INPUT' && !isSelectionReadableInputType(inputType(el))) return '';
86 if (tag !== 'INPUT' && tag !== 'TEXTAREA') return '';
87 var start = typeof el.selectionStart === 'number' ? el.selectionStart : 0;
88 var end = typeof el.selectionEnd === 'number' ? el.selectionEnd : 0;
89 if (end > start) return el.value.slice(start, end);
90 return '';
91 }
92
93 function getSelectedText() {
94 var activeSelection = getActiveElementSelection();
95 if (activeSelection) return activeSelection;
96 var sel = window.getSelection();
97 return sel ? sel.toString() : '';
98 }
99
100 function isEditable(node) {
101 if (!node || node.nodeType !== 1) return false;
102 if (node.isContentEditable) return true;
103 var tag = node.tagName;
104 if (tag === 'TEXTAREA') return !node.disabled && !node.readOnly;
105 if (tag !== 'INPUT') return false;
106 if (node.disabled || node.readOnly) return false;
107 return isTextEditableInputType(inputType(node));
108 }
109
110 function sendToClipboard(text) {
111 if (!text) return;
112 var body = JSON.stringify({text: text});
113 var encoded = encodeBody(body);
114 if (!encoded) return;
115 fetch('dumb:///api/clipboard-set', {
116 method: 'POST',
117 headers: {
118 'X-Dumber-Body': encoded,
119 'X-Dumber-Bridge-Nonce': bridgeNonce
120 }
121 }).catch(function(){});
122 }
123
124 function sendFocusSync() {
125 fetch('dumb:///api/focus-sync', {
126 method: 'POST',
127 headers: {
128 'X-Dumber-Bridge-Action': 'focus-sync',
129 'X-Dumber-Bridge-Nonce': bridgeNonce
130 }
131 }).catch(function(){});
132 }
133
134 function mirrorClipboardItems(items) {
135 try {
136 if (!items || typeof items.length !== 'number') return;
137 Array.prototype.forEach.call(items, function(item) {
138 if (!item || !item.types || item.types.indexOf('text/plain') === -1 || typeof item.getType !== 'function') return;
139 Promise.resolve(item.getType('text/plain'))
140 .then(function(blob) {
141 if (!blob || typeof blob.text !== 'function') return '';
142 return blob.text();
143 })
144 .then(function(text) {
145 if (text) sendToClipboard(text);
146 })
147 .catch(function(){});
148 });
149 } catch (_) {}
150 }
151
152 document.addEventListener('copy', function(event) {
153 if (event && event.isTrusted === false) return;
154 var text = getSelectedText();
155 if (text) sendToClipboard(text);
156 });
157 document.addEventListener('cut', function(event) {
158 if (event && event.isTrusted === false) return;
159 var text = getSelectedText();
160 if (text) sendToClipboard(text);
161 });
162 document.addEventListener('focusin', function(event) {
163 if (event && event.isTrusted === false) return;
164 if (isEditable(event && event.target)) sendFocusSync();
165 }, true);
166
167 try {
168 var clipboardObj = navigator && navigator.clipboard ? navigator.clipboard : null;
169 var clipboardProto = (typeof Clipboard !== 'undefined' && Clipboard.prototype) ||
170 (clipboardObj && typeof Object.getPrototypeOf === 'function' ? Object.getPrototypeOf(clipboardObj) : null);
171
172 if (clipboardProto && typeof clipboardProto.writeText === 'function' && !clipboardProto.__dumberWriteTextPatched) {
173 clipboardProto.__dumberWriteTextPatched = true;
174 var originalProtoWriteText = clipboardProto.writeText;
175 var wrappedProtoWriteText = function(text) {
176 var normalized = text == null ? '' : String(text);
177 var result = originalProtoWriteText.call(this, normalized);
178 if (result && typeof result.then === 'function') {
179 return result.then(function(value) {
180 if (normalized) sendToClipboard(normalized);
181 return value;
182 });
183 }
184 if (normalized) sendToClipboard(normalized);
185 return result;
186 };
187 try {
188 Object.defineProperty(clipboardProto, 'writeText', {
189 configurable: true,
190 writable: true,
191 value: wrappedProtoWriteText
192 });
193 } catch (_) {
194 clipboardProto.writeText = wrappedProtoWriteText;
195 }
196 } else if (clipboardObj && typeof clipboardObj.writeText === 'function' && !window.__dumberClipboardWriteTextPatched) {
197 window.__dumberClipboardWriteTextPatched = true;
198 var originalWriteText = clipboardObj.writeText.bind(clipboardObj);
199 var wrappedWriteText = function(text) {
200 var normalized = text == null ? '' : String(text);
201 var result = originalWriteText(normalized);
202 if (result && typeof result.then === 'function') {
203 return result.then(function(value) {
204 if (normalized) sendToClipboard(normalized);
205 return value;
206 });
207 }
208 if (normalized) sendToClipboard(normalized);
209 return result;
210 };
211 try {
212 Object.defineProperty(clipboardObj, 'writeText', {
213 configurable: true,
214 writable: true,
215 value: wrappedWriteText
216 });
217 } catch (_) {
218 clipboardObj.writeText = wrappedWriteText;
219 }
220 }
221
222 if (clipboardProto && typeof clipboardProto.write === 'function' && !clipboardProto.__dumberWritePatched) {
223 clipboardProto.__dumberWritePatched = true;
224 var originalProtoWrite = clipboardProto.write;
225 var wrappedProtoWrite = function(items) {
226 var result = originalProtoWrite.apply(this, arguments);
227 if (result && typeof result.then === 'function') {
228 return result.then(function(value) {
229 mirrorClipboardItems(items);
230 return value;
231 });
232 }
233 mirrorClipboardItems(items);
234 return result;
235 };
236 try {
237 Object.defineProperty(clipboardProto, 'write', {
238 configurable: true,
239 writable: true,
240 value: wrappedProtoWrite
241 });
242 } catch (_) {
243 clipboardProto.write = wrappedProtoWrite;
244 }
245 } else if (clipboardObj && typeof clipboardObj.write === 'function' && !window.__dumberClipboardWritePatched) {
246 window.__dumberClipboardWritePatched = true;
247 var originalWrite = clipboardObj.write.bind(clipboardObj);
248 var wrappedWrite = function(items) {
249 var result = originalWrite(items);
250 if (result && typeof result.then === 'function') {
251 return result.then(function(value) {
252 mirrorClipboardItems(items);
253 return value;
254 });
255 }
256 mirrorClipboardItems(items);
257 return result;
258 };
259 try {
260 Object.defineProperty(clipboardObj, 'write', {
261 configurable: true,
262 writable: true,
263 value: wrappedWrite
264 });
265 } catch (_) {
266 clipboardObj.write = wrappedWrite;
267 }
268 }
269 } catch (_) {}
270
271 if (isEditable(document.activeElement)) sendFocusSync();
272})();`
273
274const rendererBridgeExtensionTemplateJS = `
275(function() {
276 native function Dispatch(action, payload);
277
278 function send(action, payload) {
279 return Dispatch(action, payload == null ? '' : String(payload));
280 }
281
282 function isEditable(node) {
283 if (!node || node.nodeType !== 1) return false;
284 if (node.isContentEditable) return true;
285 var tag = node.tagName;
286 if (tag !== 'INPUT' && tag !== 'TEXTAREA') return false;
287 if (node.disabled || node.readOnly) return false;
288 return true;
289 }
290
291 function getActiveElementSelection() {
292 var el = document.activeElement;
293 if (!el) return '';
294 var tag = el.tagName;
295 if ((tag === 'INPUT' || tag === 'TEXTAREA') && typeof el.value === 'string') {
296 var start = typeof el.selectionStart === 'number' ? el.selectionStart : 0;
297 var end = typeof el.selectionEnd === 'number' ? el.selectionEnd : 0;
298 if (end > start) return el.value.slice(start, end);
299 }
300 return '';
301 }
302
303 function getSelectedText() {
304 var activeSelection = getActiveElementSelection();
305 if (activeSelection) return activeSelection;
306 var sel = window.getSelection();
307 return sel ? sel.toString() : '';
308 }
309
310 function notifyExplicitTextCopy(action, text) {
311 var normalizedText = text == null ? '' : String(text);
312 var normalizedAction = action == null || action === '' ? 'copy' : String(action);
313 return send('explicit_text_copy', JSON.stringify({ text: normalizedText, action: normalizedAction }));
314 }
315
316__DUMBER_POPUP_PROXY_BRIDGE_BODY__
317
318 if (typeof document !== 'undefined' && document.addEventListener) {
319 document.addEventListener('focusin', function(event) {
320 if (isEditable(event.target)) {
321 send('focus_sync', '');
322 }
323 }, true);
324
325 function mirrorClipboardEvent(action, e) {
326 if (!e.isTrusted) return;
327 var capturedText = '';
328 if (e.clipboardData && typeof e.clipboardData.getData === 'function') {
329 try { capturedText = e.clipboardData.getData('text/plain'); } catch(_) {}
330 }
331 setTimeout(function() {
332 var text = capturedText;
333 if (!text && !e.defaultPrevented) text = getSelectedText();
334 if (text) notifyExplicitTextCopy(action, text);
335 }, 0);
336 }
337
338 document.addEventListener('copy', function(e) {
339 mirrorClipboardEvent('copy', e);
340 }, true);
341 document.addEventListener('cut', function(e) {
342 mirrorClipboardEvent('cut', e);
343 }, true);
344 }
345 try {
346 var clipboardObj = navigator && navigator.clipboard ? navigator.clipboard : null;
347 var clipboardProto = (typeof Clipboard !== 'undefined' && Clipboard.prototype) ||
348 (clipboardObj && typeof Object.getPrototypeOf === 'function' ? Object.getPrototypeOf(clipboardObj) : null);
349
350 function mirrorClipboardItems(items) {
351 try {
352 if (!items || typeof items.length !== 'number') return;
353 Array.prototype.forEach.call(items, function(item) {
354 if (!item || !item.types || item.types.indexOf('text/plain') === -1 || typeof item.getType !== 'function') return;
355 Promise.resolve(item.getType('text/plain'))
356 .then(function(blob) {
357 if (!blob || typeof blob.text !== 'function') return '';
358 return blob.text();
359 })
360 .then(function(text) {
361 if (text) notifyExplicitTextCopy('copy', text);
362 })
363 .catch(function() {});
364 });
365 } catch (_) {}
366 }
367
368 if (clipboardProto && typeof clipboardProto.writeText === 'function' && !clipboardProto.__dumberWriteTextPatched) {
369 clipboardProto.__dumberWriteTextPatched = true;
370 var originalProtoWriteText = clipboardProto.writeText;
371 var wrappedProtoWriteText = function(text) {
372 var normalized = text == null ? '' : String(text);
373 var result = originalProtoWriteText.call(this, normalized);
374 if (result && typeof result.then === 'function') {
375 return result.then(function(value) {
376 if (normalized) notifyExplicitTextCopy('copy', normalized);
377 return value;
378 });
379 }
380 if (normalized) notifyExplicitTextCopy('copy', normalized);
381 return result;
382 };
383 try {
384 Object.defineProperty(clipboardProto, 'writeText', {
385 configurable: true,
386 writable: true,
387 value: wrappedProtoWriteText
388 });
389 } catch (_) {
390 clipboardProto.writeText = wrappedProtoWriteText;
391 }
392 } else if (clipboardObj && typeof clipboardObj.writeText === 'function' && !window.__dumberClipboardWriteTextPatched) {
393 window.__dumberClipboardWriteTextPatched = true;
394 var originalWriteText = clipboardObj.writeText.bind(clipboardObj);
395 var wrappedWriteText = function(text) {
396 var normalized = text == null ? '' : String(text);
397 var result = originalWriteText(normalized);
398 if (result && typeof result.then === 'function') {
399 return result.then(function(value) {
400 if (normalized) notifyExplicitTextCopy('copy', normalized);
401 return value;
402 });
403 }
404 if (normalized) notifyExplicitTextCopy('copy', normalized);
405 return result;
406 };
407 try {
408 Object.defineProperty(clipboardObj, 'writeText', {
409 configurable: true,
410 writable: true,
411 value: wrappedWriteText
412 });
413 } catch (_) {
414 clipboardObj.writeText = wrappedWriteText;
415 }
416 }
417
418 if (clipboardProto && typeof clipboardProto.write === 'function' && !clipboardProto.__dumberWritePatched) {
419 clipboardProto.__dumberWritePatched = true;
420 var originalProtoWrite = clipboardProto.write;
421 var wrappedProtoWrite = function(items) {
422 var result = originalProtoWrite.apply(this, arguments);
423 if (result && typeof result.then === 'function') {
424 return result.then(function(value) {
425 mirrorClipboardItems(items);
426 return value;
427 });
428 }
429 mirrorClipboardItems(items);
430 return result;
431 };
432 try {
433 Object.defineProperty(clipboardProto, 'write', {
434 configurable: true,
435 writable: true,
436 value: wrappedProtoWrite
437 });
438 } catch (_) {
439 clipboardProto.write = wrappedProtoWrite;
440 }
441 } else if (clipboardObj && typeof clipboardObj.write === 'function' && !window.__dumberClipboardWritePatched) {
442 window.__dumberClipboardWritePatched = true;
443 var originalWrite = clipboardObj.write.bind(clipboardObj);
444 var wrappedWrite = function(items) {
445 var result = originalWrite(items);
446 if (result && typeof result.then === 'function') {
447 return result.then(function(value) {
448 mirrorClipboardItems(items);
449 return value;
450 });
451 }
452 mirrorClipboardItems(items);
453 return result;
454 };
455 try {
456 Object.defineProperty(clipboardObj, 'write', {
457 configurable: true,
458 writable: true,
459 value: wrappedWrite
460 });
461 } catch (_) {
462 clipboardObj.write = wrappedWrite;
463 }
464 }
465 } catch (_) {}
466
467 send('bridge_ready', (typeof location !== 'undefined' && location && location.href) ? location.href : '');
468})();
469`
470
471const popupOpenerBridgeTemplateJS = `(function() {
472 if (typeof window === 'undefined') return;
473 if (window.__dumberPopupOpenerBridgeInstalled) return;
474 window.__dumberPopupOpenerBridgeInstalled = true;
475 if (window.opener != null) return;
476
477 var bridgeNonce = '__DUMBER_BRIDGE_NONCE__';
478 var openerHref = '__DUMBER_PARENT_URI__';
479
480 function postBridge(path, payload) {
481 try {
482 fetch('dumb:///api/' + path, {
483 method: 'POST',
484 headers: {
485 'Content-Type': 'application/json',
486 'X-Dumber-Body': btoa(unescape(encodeURIComponent(JSON.stringify(payload)))),
487 'X-Dumber-Bridge-Nonce': bridgeNonce
488 }
489 }).catch(function() {});
490 } catch (_) {}
491 }
492
493 function normalizeURLValue(rawURL) {
494 if (rawURL == null) return '';
495 try {
496 return String(rawURL);
497 } catch (_) {
498 return '';
499 }
500 }
501
502 function navigateOpener(nextURL) {
503 var rawURL = normalizeURLValue(nextURL);
504 if (rawURL !== '') openerHref = rawURL;
505 postBridge('popup-opener-navigate', { url: rawURL });
506 return openerHref;
507 }
508
509 function serializeMessage(value) {
510 try {
511 return { kind: 'json', value: JSON.stringify(value) };
512 } catch (_) {
513 return { kind: 'string', value: String(value) };
514 }
515 }
516
517 var locationProxy = {
518 assign: function(nextURL) { return navigateOpener(nextURL); },
519 replace: function(nextURL) { return navigateOpener(nextURL); },
520 toString: function() { return openerHref; }
521 };
522 try {
523 Object.defineProperty(locationProxy, 'href', {
524 configurable: true,
525 enumerable: true,
526 get: function() { return openerHref; },
527 set: function(nextURL) { navigateOpener(nextURL); }
528 });
529 } catch (_) {
530 locationProxy.href = openerHref;
531 }
532
533 var openerProxy = {
534 blur: function() { return undefined; },
535 close: function() { return undefined; },
536 focus: function() { return undefined; },
537 postMessage: function(message, targetOrigin) {
538 var serialized = serializeMessage(message);
539 postBridge('popup-opener-post-message', {
540 data: serialized.value,
541 data_kind: serialized.kind,
542 target_origin: targetOrigin == null ? '*' : String(targetOrigin),
543 source_origin: (typeof location !== 'undefined' && location && location.origin) ? location.origin : '',
544 source_href: (typeof location !== 'undefined' && location && location.href) ? location.href : ''
545 });
546 return undefined;
547 }
548 };
549 openerProxy.self = openerProxy;
550 openerProxy.window = openerProxy;
551 try {
552 Object.defineProperty(openerProxy, 'closed', {
553 configurable: true,
554 enumerable: true,
555 get: function() { return false; }
556 });
557 } catch (_) {
558 openerProxy.closed = false;
559 }
560 try {
561 Object.defineProperty(openerProxy, 'location', {
562 configurable: true,
563 enumerable: true,
564 get: function() { return locationProxy; },
565 set: function(nextURL) { navigateOpener(nextURL); }
566 });
567 } catch (_) {
568 openerProxy.location = locationProxy;
569 }
570
571 try {
572 Object.defineProperty(window, 'opener', {
573 configurable: true,
574 enumerable: true,
575 get: function() { return openerProxy; }
576 });
577 } catch (_) {
578 try { window.opener = openerProxy; } catch (_) {}
579 }
580})();`