bunite-core 0.12.1 → 0.16.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/package.json +4 -4
- package/src/host/core/App.ts +19 -2
- package/src/host/core/BrowserView.ts +515 -38
- package/src/host/core/SurfaceBrowserIPC.ts +53 -3
- package/src/host/core/SurfaceManager.ts +603 -30
- package/src/host/core/SurfaceRegistry.ts +9 -1
- package/src/host/core/inputDispatch.ts +147 -0
- package/src/host/events/webviewEvents.ts +25 -1
- package/src/host/log.ts +6 -1
- package/src/host/native.ts +263 -1
- package/src/host/preloadBundle.ts +7 -2
- package/src/native/linux/bunite_linux_ffi.cpp +427 -6
- package/src/native/linux/bunite_linux_internal.h +18 -0
- package/src/native/linux/bunite_linux_runtime.cpp +6 -1
- package/src/native/linux/bunite_linux_utils.cpp +2 -2
- package/src/native/linux/bunite_linux_view.cpp +296 -5
- package/src/native/mac/bunite_mac_ffi.mm +630 -8
- package/src/native/mac/bunite_mac_internal.h +19 -0
- package/src/native/mac/bunite_mac_utils.mm +2 -2
- package/src/native/mac/bunite_mac_view.mm +371 -9
- package/src/native/shared/ffi_exports.h +200 -2
- package/src/native/win/native_host_cef.cpp +186 -11
- package/src/native/win/native_host_ffi.cpp +1194 -1
- package/src/native/win/native_host_internal.h +35 -0
- package/src/native/win/native_host_utils.cpp +2 -1
- package/src/native/win/process_helper_win.cpp +54 -27
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +1023 -12
- package/src/native/win-webview2/webview2_internal.h +25 -0
- package/src/native/win-webview2/webview2_runtime.cpp +403 -34
- package/src/native/win-webview2/webview2_utils.cpp +30 -12
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +97 -0
- package/src/rpc/framework.ts +340 -8
- package/src/rpc/index.ts +32 -0
- package/src/webview/native.ts +253 -51
- package/src/webview/polyfill.ts +283 -22
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#include "bunite_linux_internal.h"
|
|
2
2
|
#include "webview_storage.h"
|
|
3
3
|
|
|
4
|
+
#include <cairo.h>
|
|
5
|
+
|
|
4
6
|
#include <cstdlib>
|
|
5
7
|
#include <cstring>
|
|
6
8
|
#include <mutex>
|
|
7
9
|
#include <string>
|
|
10
|
+
#include <vector>
|
|
8
11
|
|
|
9
12
|
using bunite_linux::g_runtime;
|
|
10
13
|
using bunite_linux::runOnUiThreadSync;
|
|
@@ -133,12 +136,103 @@ extern "C" BUNITE_EXPORT void bunite_view_execute_javascript(uint32_t view_id, c
|
|
|
133
136
|
});
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
namespace {
|
|
140
|
+
|
|
141
|
+
struct EvaluateCtx {
|
|
142
|
+
uint32_t view_id;
|
|
143
|
+
uint32_t request_id;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
void on_evaluate_done(GObject* source, GAsyncResult* res, gpointer user_data) {
|
|
147
|
+
auto* ctx = static_cast<EvaluateCtx*>(user_data);
|
|
148
|
+
WebKitWebView* wv = WEBKIT_WEB_VIEW(source);
|
|
149
|
+
GError* err = nullptr;
|
|
150
|
+
JSCValue* value = webkit_web_view_evaluate_javascript_finish(wv, res, &err);
|
|
151
|
+
|
|
152
|
+
std::string payload = "{\"requestId\":" + std::to_string(ctx->request_id);
|
|
153
|
+
if (err || !value) {
|
|
154
|
+
std::string msg = err ? err->message : "evaluate failed";
|
|
155
|
+
if (err) g_error_free(err);
|
|
156
|
+
payload += ",\"ok\":false,\"code\":\"runtime_error\","
|
|
157
|
+
"\"message\":\"" + bunite_linux::escapeJsonString(msg) + "\"}";
|
|
158
|
+
} else if (!jsc_value_is_string(value)) {
|
|
159
|
+
payload += ",\"ok\":false,\"code\":\"runtime_error\","
|
|
160
|
+
"\"message\":\"wrapper returned non-string\"}";
|
|
161
|
+
} else {
|
|
162
|
+
char* raw = jsc_value_to_string(value);
|
|
163
|
+
std::string inner = raw ? raw : "";
|
|
164
|
+
if (raw) g_free(raw);
|
|
165
|
+
if (inner.find("\"__bunite_ok\":true") != std::string::npos) {
|
|
166
|
+
static const std::string prefix = "{\"__bunite_ok\":true,\"value\":";
|
|
167
|
+
std::string value_json = "null";
|
|
168
|
+
if (inner.compare(0, prefix.size(), prefix) == 0 &&
|
|
169
|
+
inner.size() > prefix.size() + 1) {
|
|
170
|
+
value_json = inner.substr(prefix.size(), inner.size() - prefix.size() - 1);
|
|
171
|
+
}
|
|
172
|
+
payload += ",\"ok\":true,\"value\":\"" + bunite_linux::escapeJsonString(value_json) + "\"}";
|
|
173
|
+
} else {
|
|
174
|
+
static const std::string codePrefix = "{\"__bunite_ok\":false,\"code\":\"";
|
|
175
|
+
std::string code = "runtime_error";
|
|
176
|
+
std::string msg = "script threw";
|
|
177
|
+
if (inner.compare(0, codePrefix.size(), codePrefix) == 0) {
|
|
178
|
+
size_t start = codePrefix.size();
|
|
179
|
+
size_t end = start;
|
|
180
|
+
while (end < inner.size() && inner[end] != '"') ++end;
|
|
181
|
+
if (end > start) code = inner.substr(start, end - start);
|
|
182
|
+
static const std::string msgKey = "\",\"message\":\"";
|
|
183
|
+
if (end + msgKey.size() <= inner.size() &&
|
|
184
|
+
inner.compare(end, msgKey.size(), msgKey) == 0) {
|
|
185
|
+
size_t mstart = end + msgKey.size();
|
|
186
|
+
size_t mend = mstart;
|
|
187
|
+
while (mend < inner.size()) {
|
|
188
|
+
if (inner[mend] == '"' && (mend == mstart || inner[mend - 1] != '\\')) break;
|
|
189
|
+
++mend;
|
|
190
|
+
}
|
|
191
|
+
if (mend > mstart) msg = inner.substr(mstart, mend - mstart);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
payload += ",\"ok\":false,\"code\":\"" + bunite_linux::escapeJsonString(code) + "\","
|
|
195
|
+
"\"message\":\"" + bunite_linux::escapeJsonString(msg) + "\"}";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (value) g_object_unref(value);
|
|
199
|
+
bunite_linux::emitWebviewEvent(ctx->view_id, "evaluate-result", payload);
|
|
200
|
+
delete ctx;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
} // namespace
|
|
204
|
+
|
|
205
|
+
extern "C" BUNITE_EXPORT void bunite_view_evaluate(uint32_t view_id, uint32_t request_id, const char* script) {
|
|
206
|
+
// Wrapper matches WebView2/CEF: try/catch returns JSON envelope string.
|
|
207
|
+
// jsc_value_to_string delivers the wrapper's return value directly.
|
|
208
|
+
if (!script) {
|
|
209
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
210
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
211
|
+
"\"message\":\"null script\"}";
|
|
212
|
+
bunite_linux::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
std::string wrapped =
|
|
216
|
+
"(function(){try{return JSON.stringify({__bunite_ok:true,value:("
|
|
217
|
+
+ std::string(script) +
|
|
218
|
+
")})}catch(e){var c=(e&&e.name===\"SecurityError\")?\"cross_origin\":\"runtime_error\";"
|
|
219
|
+
"return JSON.stringify({__bunite_ok:false,code:c,"
|
|
220
|
+
"message:(e&&e.message)?e.message:String(e),"
|
|
221
|
+
"name:(e&&e.name)||\"\"})}})()";
|
|
222
|
+
runOnUiThreadSync([=]() {
|
|
223
|
+
auto* v = bunite_linux::findView(view_id);
|
|
224
|
+
if (!v || !v->webview) {
|
|
225
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
226
|
+
",\"ok\":false,\"code\":\"not_supported\","
|
|
227
|
+
"\"message\":\"view not ready\"}";
|
|
228
|
+
bunite_linux::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
auto* ctx = new EvaluateCtx{view_id, request_id};
|
|
232
|
+
webkit_web_view_evaluate_javascript(
|
|
233
|
+
v->webview, wrapped.c_str(), -1, nullptr, nullptr, nullptr,
|
|
234
|
+
on_evaluate_done, ctx);
|
|
235
|
+
});
|
|
142
236
|
}
|
|
143
237
|
|
|
144
238
|
extern "C" BUNITE_EXPORT void bunite_view_load_url(uint32_t view_id, const char* url) {
|
|
@@ -254,6 +348,333 @@ extern "C" BUNITE_EXPORT void bunite_view_remove(uint32_t view_id) {
|
|
|
254
348
|
runOnUiThreadSync([=]() { bunite_linux::removeView(view_id); });
|
|
255
349
|
}
|
|
256
350
|
|
|
351
|
+
// Input dispatch — no-op on GTK4 + Wayland (no portable synthetic-input primitive).
|
|
352
|
+
// Capability `click/type/press/scroll: false` is honest; calls are silent no-ops.
|
|
353
|
+
extern "C" BUNITE_EXPORT void bunite_view_click(uint32_t, double, double, int32_t, int32_t, uint32_t) {}
|
|
354
|
+
extern "C" BUNITE_EXPORT void bunite_view_type(uint32_t, const char*) {}
|
|
355
|
+
extern "C" BUNITE_EXPORT void bunite_view_press(uint32_t, int32_t, int32_t, const char*, const char*, const char*, uint32_t, int32_t, bool, int32_t) {}
|
|
356
|
+
extern "C" BUNITE_EXPORT void bunite_view_scroll(uint32_t, double, double, double, double, uint32_t) {}
|
|
357
|
+
extern "C" BUNITE_EXPORT void bunite_view_mouse(uint32_t, int32_t, double, double, int32_t, uint32_t) {}
|
|
358
|
+
|
|
359
|
+
extern "C" BUNITE_EXPORT void bunite_view_resolve_and_click(
|
|
360
|
+
uint32_t view_id, uint32_t request_id, const char* /*selector*/, const char* /*frame_id*/,
|
|
361
|
+
int32_t /*button*/, int32_t /*click_count*/, uint32_t /*modifiers*/) {
|
|
362
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
363
|
+
",\"ok\":false,\"code\":\"not_supported\","
|
|
364
|
+
"\"message\":\"WebKitGTK has no synthetic input API\"}";
|
|
365
|
+
bunite_linux::emitWebviewEvent(view_id, "resolve-and-click-result", payload);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
extern "C" BUNITE_EXPORT void bunite_view_respond_dialog(uint32_t view_id, uint32_t request_id,
|
|
369
|
+
bool accept, const char* text) {
|
|
370
|
+
bunite_linux::respondToDialogRequest(view_id, request_id, accept, text ? text : "");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Screenshot — webkit_web_view_get_snapshot → cairo_surface_t → PNG bytes via
|
|
374
|
+
// cairo_surface_write_to_png_stream → g_base64_encode. JPEG path uses GdkPixbuf
|
|
375
|
+
// for `pixbuf_save_to_buffer(... "jpeg" ...)`.
|
|
376
|
+
namespace {
|
|
377
|
+
|
|
378
|
+
struct LinuxShotCtx {
|
|
379
|
+
uint32_t view_id;
|
|
380
|
+
uint32_t request_id;
|
|
381
|
+
std::string format; // "png" | "jpeg"
|
|
382
|
+
int32_t quality;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
void emitLinuxShotError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& msg) {
|
|
386
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
387
|
+
",\"ok\":false,\"code\":\"" + code + "\","
|
|
388
|
+
"\"message\":\"" + bunite_linux::escapeJsonString(msg) + "\"}";
|
|
389
|
+
bunite_linux::emitWebviewEvent(view_id, "screenshot-result", payload);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// WebKitGTK 2.52+ snapshot returns GdkTexture (was cairo_surface_t pre-2.52).
|
|
393
|
+
// PNG: gdk_texture_save_to_png_bytes — built-in. JPEG: bridge through GdkPixbuf
|
|
394
|
+
// via gdk_pixbuf_get_from_texture (non-deprecated GTK4 path).
|
|
395
|
+
void on_snapshot_done(GObject* source, GAsyncResult* res, gpointer user_data) {
|
|
396
|
+
auto* ctx = static_cast<LinuxShotCtx*>(user_data);
|
|
397
|
+
WebKitWebView* wv = WEBKIT_WEB_VIEW(source);
|
|
398
|
+
GError* err = nullptr;
|
|
399
|
+
GdkTexture* texture = webkit_web_view_get_snapshot_finish(wv, res, &err);
|
|
400
|
+
if (!texture) {
|
|
401
|
+
emitLinuxShotError(ctx->view_id, ctx->request_id, "runtime_error",
|
|
402
|
+
err ? err->message : "snapshot returned nil");
|
|
403
|
+
if (err) g_error_free(err);
|
|
404
|
+
delete ctx;
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
std::vector<unsigned char> bytes;
|
|
409
|
+
std::string mime;
|
|
410
|
+
const bool jpeg = (ctx->format == "jpeg" || ctx->format == "jpg");
|
|
411
|
+
if (jpeg) {
|
|
412
|
+
GdkPixbuf* pix = gdk_pixbuf_get_from_texture(texture);
|
|
413
|
+
if (pix) {
|
|
414
|
+
gchar* raw = nullptr; gsize raw_len = 0;
|
|
415
|
+
char qbuf[8]; snprintf(qbuf, sizeof(qbuf), "%d",
|
|
416
|
+
ctx->quality < 0 ? 90 : (ctx->quality > 100 ? 100 : ctx->quality));
|
|
417
|
+
GError* perr = nullptr;
|
|
418
|
+
if (gdk_pixbuf_save_to_buffer(pix, &raw, &raw_len, "jpeg", &perr, "quality", qbuf, nullptr)) {
|
|
419
|
+
bytes.assign(raw, raw + raw_len);
|
|
420
|
+
g_free(raw);
|
|
421
|
+
} else if (perr) {
|
|
422
|
+
g_error_free(perr);
|
|
423
|
+
}
|
|
424
|
+
g_object_unref(pix);
|
|
425
|
+
}
|
|
426
|
+
mime = "image/jpeg";
|
|
427
|
+
ctx->format = "jpeg";
|
|
428
|
+
} else {
|
|
429
|
+
GBytes* png = gdk_texture_save_to_png_bytes(texture);
|
|
430
|
+
if (png) {
|
|
431
|
+
gsize n = 0;
|
|
432
|
+
const auto* p = static_cast<const unsigned char*>(g_bytes_get_data(png, &n));
|
|
433
|
+
bytes.assign(p, p + n);
|
|
434
|
+
g_bytes_unref(png);
|
|
435
|
+
}
|
|
436
|
+
mime = "image/png";
|
|
437
|
+
ctx->format = "png";
|
|
438
|
+
}
|
|
439
|
+
g_object_unref(texture);
|
|
440
|
+
|
|
441
|
+
if (bytes.empty()) {
|
|
442
|
+
emitLinuxShotError(ctx->view_id, ctx->request_id, "runtime_error", "encode failed");
|
|
443
|
+
delete ctx;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
gchar* b64 = g_base64_encode(bytes.data(), bytes.size());
|
|
447
|
+
std::string payload = "{\"requestId\":" + std::to_string(ctx->request_id) +
|
|
448
|
+
",\"ok\":true,\"format\":\"" + ctx->format +
|
|
449
|
+
"\",\"mime\":\"" + mime +
|
|
450
|
+
"\",\"dataBase64\":\"" + (b64 ? b64 : "") + "\"}";
|
|
451
|
+
if (b64) g_free(b64);
|
|
452
|
+
bunite_linux::emitWebviewEvent(ctx->view_id, "screenshot-result", payload);
|
|
453
|
+
delete ctx;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
} // namespace
|
|
457
|
+
|
|
458
|
+
extern "C" BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
|
|
459
|
+
// WebKitGTK — input dispatch impossible on GTK4+Wayland; screenshot via cairo.
|
|
460
|
+
auto* v = bunite_linux::findView(view_id);
|
|
461
|
+
if (!v) return 0;
|
|
462
|
+
return BUNITE_CAP_EVALUATE | BUNITE_CAP_SURFACE_EVENTS |
|
|
463
|
+
BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
|
|
464
|
+
BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG |
|
|
465
|
+
BUNITE_CAP_BOUNDING_RECT | BUNITE_CAP_DOWNLOADS | BUNITE_CAP_POPUPS |
|
|
466
|
+
BUNITE_CAP_AX | BUNITE_CAP_FRAMES;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
namespace {
|
|
470
|
+
|
|
471
|
+
struct AxCtx { uint32_t view_id; uint32_t request_id; };
|
|
472
|
+
|
|
473
|
+
// JS-bridge ax tree: walks DOM + reads ARIA attrs. Emits CDP-shaped flat list
|
|
474
|
+
// so TS-side convertAxTree works unchanged. `ignored` is always false — TS
|
|
475
|
+
// `interestingOnly` filter is a no-op on this backend (limitation).
|
|
476
|
+
const char* kAxScript = R"((function(){
|
|
477
|
+
var nodes=[];
|
|
478
|
+
function add(el,parentId){
|
|
479
|
+
if(!el||el.nodeType!==1)return null;
|
|
480
|
+
var id=String(nodes.length+1);
|
|
481
|
+
var node={nodeId:id,parentId:parentId,
|
|
482
|
+
role:{value:el.getAttribute('role')||el.tagName.toLowerCase()},
|
|
483
|
+
name:{value:el.getAttribute('aria-label')||
|
|
484
|
+
(el.tagName==='INPUT'||el.tagName==='TEXTAREA'?'':
|
|
485
|
+
(el.firstChild&&el.firstChild.nodeType===3?el.firstChild.textContent.trim().slice(0,100):''))},
|
|
486
|
+
properties:[],childIds:[],ignored:false};
|
|
487
|
+
var d=el.getAttribute('aria-description');if(d)node.description={value:d};
|
|
488
|
+
if(el.tagName==='INPUT'||el.tagName==='TEXTAREA'){if(el.value)node.value={value:el.value};}
|
|
489
|
+
if(el.getAttribute('aria-disabled')==='true'||el.disabled)node.properties.push({name:'disabled',value:{value:true}});
|
|
490
|
+
var ck=el.getAttribute('aria-checked');
|
|
491
|
+
if(ck==='true')node.properties.push({name:'checked',value:{value:true}});
|
|
492
|
+
else if(ck==='false')node.properties.push({name:'checked',value:{value:false}});
|
|
493
|
+
else if(ck==='mixed')node.properties.push({name:'checked',value:{value:'mixed'}});
|
|
494
|
+
var pr=el.getAttribute('aria-pressed');
|
|
495
|
+
if(pr==='true')node.properties.push({name:'pressed',value:{value:true}});
|
|
496
|
+
else if(pr==='false')node.properties.push({name:'pressed',value:{value:false}});
|
|
497
|
+
else if(pr==='mixed')node.properties.push({name:'pressed',value:{value:'mixed'}});
|
|
498
|
+
if(el.getAttribute('aria-expanded')==='true')node.properties.push({name:'expanded',value:{value:true}});
|
|
499
|
+
if(el.getAttribute('aria-selected')==='true')node.properties.push({name:'selected',value:{value:true}});
|
|
500
|
+
if(el.getAttribute('aria-required')==='true')node.properties.push({name:'required',value:{value:true}});
|
|
501
|
+
if(el.getAttribute('aria-invalid')==='true')node.properties.push({name:'invalid',value:{value:true}});
|
|
502
|
+
var lv=el.getAttribute('aria-level');if(lv)node.properties.push({name:'level',value:{value:parseInt(lv,10)}});
|
|
503
|
+
if(document.activeElement===el)node.properties.push({name:'focused',value:{value:true}});
|
|
504
|
+
nodes.push(node);
|
|
505
|
+
for(var i=0;i<el.children.length;i++){var cid=add(el.children[i],id);if(cid)node.childIds.push(cid);}
|
|
506
|
+
return id;
|
|
507
|
+
}
|
|
508
|
+
add(document.documentElement,null);
|
|
509
|
+
return JSON.stringify({nodes:nodes});
|
|
510
|
+
})())";
|
|
511
|
+
|
|
512
|
+
void on_ax_done(GObject* source, GAsyncResult* res, gpointer user_data) {
|
|
513
|
+
auto* ctx = static_cast<AxCtx*>(user_data);
|
|
514
|
+
WebKitWebView* wv = WEBKIT_WEB_VIEW(source);
|
|
515
|
+
GError* err = nullptr;
|
|
516
|
+
JSCValue* value = webkit_web_view_evaluate_javascript_finish(wv, res, &err);
|
|
517
|
+
std::string payload = "{\"requestId\":" + std::to_string(ctx->request_id);
|
|
518
|
+
if (err || !value) {
|
|
519
|
+
std::string msg = err ? err->message : "evaluate failed";
|
|
520
|
+
if (err) g_error_free(err);
|
|
521
|
+
payload += ",\"ok\":false,\"code\":\"runtime_error\","
|
|
522
|
+
"\"message\":\"" + bunite_linux::escapeJsonString(msg) + "\"}";
|
|
523
|
+
} else if (!jsc_value_is_string(value)) {
|
|
524
|
+
payload += ",\"ok\":false,\"code\":\"runtime_error\","
|
|
525
|
+
"\"message\":\"non-string ax result\"}";
|
|
526
|
+
} else {
|
|
527
|
+
char* raw = jsc_value_to_string(value);
|
|
528
|
+
std::string tree_json = raw ? raw : "{}";
|
|
529
|
+
if (raw) g_free(raw);
|
|
530
|
+
payload += ",\"ok\":true,\"tree\":" + tree_json + "}";
|
|
531
|
+
}
|
|
532
|
+
if (value) g_object_unref(value);
|
|
533
|
+
bunite_linux::emitWebviewEvent(ctx->view_id, "accessibility-result", payload);
|
|
534
|
+
delete ctx;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
} // namespace
|
|
538
|
+
|
|
539
|
+
extern "C" BUNITE_EXPORT void bunite_view_accessibility_snapshot(uint32_t view_id, uint32_t request_id,
|
|
540
|
+
int32_t /*interesting_only*/) {
|
|
541
|
+
auto* st = bunite_linux::findView(view_id);
|
|
542
|
+
if (!st || !st->webview) {
|
|
543
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
544
|
+
",\"ok\":false,\"code\":\"not_supported\","
|
|
545
|
+
"\"message\":\"view not ready\"}";
|
|
546
|
+
bunite_linux::emitWebviewEvent(view_id, "accessibility-result", payload);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
auto* ctx = new AxCtx{view_id, request_id};
|
|
550
|
+
webkit_web_view_evaluate_javascript(st->webview, kAxScript, -1, nullptr, nullptr, nullptr, on_ax_done, ctx);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
namespace {
|
|
554
|
+
|
|
555
|
+
struct FramesCtx { uint32_t view_id; uint32_t request_id; };
|
|
556
|
+
|
|
557
|
+
// JS-bridge frame tree: walks window.frames. Synthetic IDs are sequential —
|
|
558
|
+
// not stable across calls. Cross-origin frames are included with empty url/origin
|
|
559
|
+
// (SecurityError catch). Output matches CDP `Page.getFrameTree` shape so the
|
|
560
|
+
// TS-side flattenFrameTree works unchanged.
|
|
561
|
+
const char* kFramesScript = R"((function(){
|
|
562
|
+
var id=0;
|
|
563
|
+
function walk(win){
|
|
564
|
+
var fid=String(++id);
|
|
565
|
+
var frame={id:fid,securityOrigin:'',url:''};
|
|
566
|
+
try{
|
|
567
|
+
frame.url=win.location.href;
|
|
568
|
+
frame.securityOrigin=win.location.origin;
|
|
569
|
+
if(win.frameElement&&win.frameElement.name)frame.name=win.frameElement.name;
|
|
570
|
+
}catch(e){}
|
|
571
|
+
var children=[];
|
|
572
|
+
try{for(var i=0;i<win.frames.length;i++)children.push(walk(win.frames[i]));}catch(e){}
|
|
573
|
+
return {frame:frame,childFrames:children};
|
|
574
|
+
}
|
|
575
|
+
return JSON.stringify({frameTree:walk(window)});
|
|
576
|
+
})())";
|
|
577
|
+
|
|
578
|
+
void on_frames_done(GObject* source, GAsyncResult* res, gpointer user_data) {
|
|
579
|
+
auto* ctx = static_cast<FramesCtx*>(user_data);
|
|
580
|
+
WebKitWebView* wv = WEBKIT_WEB_VIEW(source);
|
|
581
|
+
GError* err = nullptr;
|
|
582
|
+
JSCValue* value = webkit_web_view_evaluate_javascript_finish(wv, res, &err);
|
|
583
|
+
std::string payload = "{\"requestId\":" + std::to_string(ctx->request_id);
|
|
584
|
+
if (err || !value) {
|
|
585
|
+
std::string msg = err ? err->message : "evaluate failed";
|
|
586
|
+
if (err) g_error_free(err);
|
|
587
|
+
payload += ",\"ok\":false,\"code\":\"runtime_error\","
|
|
588
|
+
"\"message\":\"" + bunite_linux::escapeJsonString(msg) + "\"}";
|
|
589
|
+
} else if (!jsc_value_is_string(value)) {
|
|
590
|
+
payload += ",\"ok\":false,\"code\":\"runtime_error\","
|
|
591
|
+
"\"message\":\"non-string frames result\"}";
|
|
592
|
+
} else {
|
|
593
|
+
char* raw = jsc_value_to_string(value);
|
|
594
|
+
std::string tree_json = raw ? raw : "{}";
|
|
595
|
+
if (raw) g_free(raw);
|
|
596
|
+
payload += ",\"ok\":true,\"raw\":" + tree_json + "}";
|
|
597
|
+
}
|
|
598
|
+
if (value) g_object_unref(value);
|
|
599
|
+
bunite_linux::emitWebviewEvent(ctx->view_id, "list-frames-result", payload);
|
|
600
|
+
delete ctx;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
} // namespace
|
|
604
|
+
|
|
605
|
+
extern "C" BUNITE_EXPORT void bunite_view_list_frames(uint32_t view_id, uint32_t request_id) {
|
|
606
|
+
auto* st = bunite_linux::findView(view_id);
|
|
607
|
+
if (!st || !st->webview) {
|
|
608
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
609
|
+
",\"ok\":false,\"code\":\"not_supported\","
|
|
610
|
+
"\"message\":\"view not ready\"}";
|
|
611
|
+
bunite_linux::emitWebviewEvent(view_id, "list-frames-result", payload);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
auto* ctx = new FramesCtx{view_id, request_id};
|
|
615
|
+
webkit_web_view_evaluate_javascript(st->webview, kFramesScript, -1, nullptr, nullptr, nullptr, on_frames_done, ctx);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
extern "C" BUNITE_EXPORT void bunite_view_evaluate_in_frame(uint32_t view_id, uint32_t request_id,
|
|
619
|
+
const char* script_c, const char* frame_id_c) {
|
|
620
|
+
std::string script = script_c ? script_c : "";
|
|
621
|
+
std::string frame_id = frame_id_c ? frame_id_c : "";
|
|
622
|
+
if (frame_id.empty()) {
|
|
623
|
+
bunite_view_evaluate(view_id, request_id, script_c);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
// JS-bridge: walk window.frames matching listFrames numbering, `eval` user
|
|
627
|
+
// script in target frame. frameIds are sequential per walk — caller must use
|
|
628
|
+
// them immediately after listFrames. The outer envelope from
|
|
629
|
+
// bunite_view_evaluate handles ok/cross_origin/runtime_error mapping; we
|
|
630
|
+
// surface SecurityError so cross-origin → cross_origin and re-throw missing
|
|
631
|
+
// frames as plain Error → runtime_error.
|
|
632
|
+
std::string js_target = bunite_linux::escapeJsonString(frame_id);
|
|
633
|
+
std::string js_script = bunite_linux::escapeJsonString(script);
|
|
634
|
+
std::string inner =
|
|
635
|
+
"(function(){var target=\"" + js_target + "\";var id=0;var found=null;"
|
|
636
|
+
"function walk(win){var fid=String(++id);if(fid===target){found=win;return;}"
|
|
637
|
+
"try{for(var i=0;i<win.frames.length;i++){walk(win.frames[i]);if(found)return;}}catch(e){}}"
|
|
638
|
+
"walk(window);"
|
|
639
|
+
"if(!found)throw new Error('frame not found');"
|
|
640
|
+
"return found.eval(\"(\"+\"" + js_script + "\"+\")\");"
|
|
641
|
+
"})()";
|
|
642
|
+
bunite_view_evaluate(view_id, request_id, inner.c_str());
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
extern "C" BUNITE_EXPORT void bunite_view_set_download_policy(uint32_t view_id, int32_t policy, const char* download_dir) {
|
|
646
|
+
auto* st = bunite_linux::findView(view_id);
|
|
647
|
+
if (!st) return;
|
|
648
|
+
if (policy < 0 || policy > 2) policy = 2;
|
|
649
|
+
st->download_policy.store(policy);
|
|
650
|
+
st->download_dir = download_dir ? download_dir : "";
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
extern "C" BUNITE_EXPORT void bunite_view_popup_accept(uint32_t new_view_id, uint32_t host_window_id,
|
|
654
|
+
double x, double y, double w, double h) {
|
|
655
|
+
runOnUiThreadSync([=]() { bunite_linux::acceptParkedPopup(new_view_id, host_window_id, x, y, w, h); });
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
extern "C" BUNITE_EXPORT void bunite_view_popup_dismiss(uint32_t new_view_id) {
|
|
659
|
+
runOnUiThreadSync([=]() { bunite_linux::dismissParkedPopup(new_view_id); });
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
extern "C" BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t request_id,
|
|
663
|
+
const char* format, int32_t quality) {
|
|
664
|
+
std::string fmt = format ? format : "png";
|
|
665
|
+
runOnUiThreadSync([=]() {
|
|
666
|
+
auto* v = bunite_linux::findView(view_id);
|
|
667
|
+
if (!v || !v->webview) {
|
|
668
|
+
emitLinuxShotError(view_id, request_id, "not_supported", "view not ready");
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
auto* ctx = new LinuxShotCtx{view_id, request_id, fmt, quality};
|
|
672
|
+
webkit_web_view_get_snapshot(v->webview, WEBKIT_SNAPSHOT_REGION_VISIBLE,
|
|
673
|
+
WEBKIT_SNAPSHOT_OPTIONS_NONE, nullptr,
|
|
674
|
+
on_snapshot_done, ctx);
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
257
678
|
extern "C" BUNITE_EXPORT void bunite_view_open_devtools(uint32_t view_id) {
|
|
258
679
|
(void)view_id; BUNITE_LINUX_TODO("bunite_view_open_devtools");
|
|
259
680
|
}
|
|
@@ -38,6 +38,15 @@ struct ViewState {
|
|
|
38
38
|
std::string preload_script;
|
|
39
39
|
std::string stored_html;
|
|
40
40
|
std::vector<std::string> navigation_rules;
|
|
41
|
+
|
|
42
|
+
// Page-initiated dialogs awaiting host response. WebKitScriptDialog is
|
|
43
|
+
// held until we mark confirmed/text + emit signal completion.
|
|
44
|
+
std::unordered_map<uint32_t, WebKitScriptDialog*> pending_dialogs;
|
|
45
|
+
uint32_t next_dialog_request_id = 1;
|
|
46
|
+
|
|
47
|
+
// Download policy: 0=auto, 1=ask (treated as block), 2=block (default).
|
|
48
|
+
std::atomic<int32_t> download_policy{2};
|
|
49
|
+
std::string download_dir;
|
|
41
50
|
};
|
|
42
51
|
|
|
43
52
|
struct RuntimeState {
|
|
@@ -54,6 +63,12 @@ struct RuntimeState {
|
|
|
54
63
|
pthread_t ui_thread = 0;
|
|
55
64
|
bool ui_thread_set = false;
|
|
56
65
|
|
|
66
|
+
// Hidden top-level window — popup-minted WebViews park here until adoption.
|
|
67
|
+
GtkWindow* popup_parent = nullptr;
|
|
68
|
+
// Parked popup-minted views awaiting acceptPopup/dismissPopup. Keyed by
|
|
69
|
+
// popup view_id (>= 0x80000000).
|
|
70
|
+
std::unordered_map<uint32_t, WebKitWebView*> parked_popups;
|
|
71
|
+
|
|
57
72
|
std::unordered_map<uint32_t, WebKitPermissionRequest*> pending_permissions;
|
|
58
73
|
uint32_t next_permission_request_id = 1;
|
|
59
74
|
|
|
@@ -134,6 +149,7 @@ void destroyWindow(uint32_t window_id);
|
|
|
134
149
|
|
|
135
150
|
ViewState* findView(uint32_t view_id);
|
|
136
151
|
uint32_t viewIdForWebView(WebKitWebView* wv);
|
|
152
|
+
void respondToDialogRequest(uint32_t view_id, uint32_t request_id, bool accept, const std::string& text);
|
|
137
153
|
bool createView(uint32_t view_id, uint32_t window_id,
|
|
138
154
|
const char* url, const char* html, const char* preload, const char* appres_root,
|
|
139
155
|
const char* navigation_rules_json, const char* preload_origins_json,
|
|
@@ -141,6 +157,8 @@ bool createView(uint32_t view_id, uint32_t window_id,
|
|
|
141
157
|
void removeView(uint32_t view_id);
|
|
142
158
|
void detachViewSideState(uint32_t view_id);
|
|
143
159
|
void applyViewBounds(uint32_t view_id, double x, double y, double width, double height);
|
|
160
|
+
bool acceptParkedPopup(uint32_t new_view_id, uint32_t host_window_id, double x, double y, double w, double h);
|
|
161
|
+
void dismissParkedPopup(uint32_t new_view_id);
|
|
144
162
|
void queueViewRedraw(WebKitWebView* wv);
|
|
145
163
|
|
|
146
164
|
void registerAppresScheme(WebKitWebContext* ctx);
|
|
@@ -16,7 +16,7 @@ bool isOnMainThread() {
|
|
|
16
16
|
|
|
17
17
|
namespace {
|
|
18
18
|
|
|
19
|
-
constexpr int32_t kBuniteAbiVersion =
|
|
19
|
+
constexpr int32_t kBuniteAbiVersion = 11;
|
|
20
20
|
|
|
21
21
|
} // namespace
|
|
22
22
|
|
|
@@ -59,6 +59,11 @@ extern "C" BUNITE_EXPORT bool bunite_init(
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
rt.app = app;
|
|
62
|
+
// Hidden offscreen parent — popup-minted views park here until host adoption.
|
|
63
|
+
rt.popup_parent = GTK_WINDOW(gtk_window_new());
|
|
64
|
+
gtk_window_set_decorated(rt.popup_parent, FALSE);
|
|
65
|
+
gtk_window_set_default_size(rt.popup_parent, 1, 1);
|
|
66
|
+
gtk_window_set_child(rt.popup_parent, gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
|
|
62
67
|
rt.initialized = true;
|
|
63
68
|
return true;
|
|
64
69
|
}
|
|
@@ -93,8 +93,8 @@ std::vector<std::string> parseNavigationRulesJson(const std::string& json) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
bool shouldAlwaysAllowNavigationUrl(const std::string& url) {
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
// Exact-match — prefix would let `../../evil` style paths bypass scrutiny.
|
|
97
|
+
return url == "about:blank" || url == "appres://app.internal/internal/index.html";
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
bool shouldAllowNavigation(const ViewState* view, const std::string& url) {
|