bunite-core 0.12.0 → 0.14.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.
Files changed (34) hide show
  1. package/package.json +4 -4
  2. package/src/host/core/App.ts +17 -1
  3. package/src/host/core/BrowserView.ts +197 -28
  4. package/src/host/core/SurfaceBrowserIPC.ts +44 -3
  5. package/src/host/core/SurfaceManager.ts +260 -28
  6. package/src/host/core/SurfaceRegistry.ts +9 -1
  7. package/src/host/core/inputDispatch.ts +147 -0
  8. package/src/host/events/webviewEvents.ts +8 -1
  9. package/src/host/native.ts +124 -1
  10. package/src/native/linux/bunite_linux_ffi.cpp +223 -6
  11. package/src/native/linux/bunite_linux_internal.h +6 -0
  12. package/src/native/linux/bunite_linux_runtime.cpp +1 -1
  13. package/src/native/linux/bunite_linux_utils.cpp +2 -2
  14. package/src/native/linux/bunite_linux_view.cpp +85 -0
  15. package/src/native/mac/bunite_mac_ffi.mm +356 -8
  16. package/src/native/mac/bunite_mac_internal.h +6 -0
  17. package/src/native/mac/bunite_mac_utils.mm +2 -2
  18. package/src/native/mac/bunite_mac_view.mm +144 -2
  19. package/src/native/shared/ffi_exports.h +135 -0
  20. package/src/native/win/native_host_cef.cpp +86 -3
  21. package/src/native/win/native_host_ffi.cpp +378 -1
  22. package/src/native/win/native_host_internal.h +13 -0
  23. package/src/native/win/native_host_utils.cpp +2 -1
  24. package/src/native/win/process_helper_win.cpp +54 -27
  25. package/src/native/win-webview2/bunite_webview2_ffi.cpp +303 -9
  26. package/src/native/win-webview2/webview2_internal.h +11 -0
  27. package/src/native/win-webview2/webview2_runtime.cpp +128 -12
  28. package/src/native/win-webview2/webview2_utils.cpp +30 -12
  29. package/src/preload/runtime.built.js +1 -1
  30. package/src/preload/runtime.ts +97 -0
  31. package/src/rpc/framework.ts +173 -4
  32. package/src/rpc/index.ts +21 -0
  33. package/src/webview/native.ts +126 -25
  34. package/src/webview/polyfill.ts +196 -12
@@ -102,6 +102,31 @@ type NativeSymbols = {
102
102
  bunite_view_reload: (viewId: number) => void;
103
103
  bunite_view_execute_javascript: (viewId: number, script: CStringPointer) => void;
104
104
  bunite_view_evaluate: (viewId: number, requestId: number, script: CStringPointer) => void;
105
+ bunite_view_click: (
106
+ viewId: number, x: number, y: number,
107
+ button: number, clickCount: number, modifiers: number
108
+ ) => void;
109
+ bunite_view_type: (viewId: number, text: CStringPointer) => void;
110
+ bunite_view_press: (
111
+ viewId: number, windowsVkCode: number, macKeyCode: number,
112
+ key: CStringPointer, code: CStringPointer, character: CStringPointer,
113
+ modifiers: number, action: number, extended: boolean, location: number
114
+ ) => void;
115
+ bunite_view_scroll: (
116
+ viewId: number, dx: number, dy: number,
117
+ x: number, y: number, modifiers: number
118
+ ) => void;
119
+ bunite_view_mouse: (
120
+ viewId: number, action: number, x: number, y: number,
121
+ button: number, modifiers: number
122
+ ) => void;
123
+ bunite_view_respond_dialog: (
124
+ viewId: number, requestId: number, accept: boolean, text: CStringPointer
125
+ ) => void;
126
+ bunite_view_screenshot: (
127
+ viewId: number, requestId: number, format: CStringPointer, quality: number
128
+ ) => void;
129
+ bunite_view_capabilities: (viewId: number) => number;
105
130
  bunite_view_load_url: (viewId: number, url: CStringPointer) => void;
106
131
  bunite_view_load_html: (viewId: number, html: CStringPointer) => void;
107
132
  bunite_view_remove: (viewId: number) => void;
@@ -289,6 +314,38 @@ const nativeSymbolDefinitions = {
289
314
  args: [FFIType.u32, FFIType.u32, FFIType.cstring],
290
315
  returns: FFIType.void
291
316
  },
317
+ bunite_view_click: {
318
+ args: [FFIType.u32, FFIType.f64, FFIType.f64, FFIType.i32, FFIType.i32, FFIType.u32],
319
+ returns: FFIType.void
320
+ },
321
+ bunite_view_type: {
322
+ args: [FFIType.u32, FFIType.cstring],
323
+ returns: FFIType.void
324
+ },
325
+ bunite_view_press: {
326
+ args: [FFIType.u32, FFIType.i32, FFIType.i32, FFIType.cstring, FFIType.cstring, FFIType.cstring, FFIType.u32, FFIType.i32, FFIType.bool, FFIType.i32],
327
+ returns: FFIType.void
328
+ },
329
+ bunite_view_scroll: {
330
+ args: [FFIType.u32, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.u32],
331
+ returns: FFIType.void
332
+ },
333
+ bunite_view_mouse: {
334
+ args: [FFIType.u32, FFIType.i32, FFIType.f64, FFIType.f64, FFIType.i32, FFIType.u32],
335
+ returns: FFIType.void
336
+ },
337
+ bunite_view_respond_dialog: {
338
+ args: [FFIType.u32, FFIType.u32, FFIType.bool, FFIType.cstring],
339
+ returns: FFIType.void
340
+ },
341
+ bunite_view_screenshot: {
342
+ args: [FFIType.u32, FFIType.u32, FFIType.cstring, FFIType.i32],
343
+ returns: FFIType.void
344
+ },
345
+ bunite_view_capabilities: {
346
+ args: [FFIType.u32],
347
+ returns: FFIType.u32
348
+ },
292
349
  bunite_view_load_url: {
293
350
  args: [FFIType.u32, FFIType.cstring],
294
351
  returns: FFIType.void
@@ -350,6 +407,20 @@ export function setEvaluateResultHandler(handler: (viewId: number, result: Nativ
350
407
  evaluateResultHandler = handler;
351
408
  }
352
409
 
410
+ export type NativeScreenshotResult = {
411
+ requestId: number;
412
+ ok: boolean;
413
+ format?: "png" | "jpeg";
414
+ mime?: string;
415
+ dataBase64?: string;
416
+ code?: string;
417
+ message?: string;
418
+ };
419
+ let screenshotResultHandler: ((viewId: number, result: NativeScreenshotResult) => void) | null = null;
420
+ export function setScreenshotResultHandler(handler: (viewId: number, result: NativeScreenshotResult) => void) {
421
+ screenshotResultHandler = handler;
422
+ }
423
+
353
424
  // Per-view deferred resolvers for "view-ready" (OnAfterCreated).
354
425
  const viewReadyResolvers = new Map<number, () => void>();
355
426
 
@@ -499,6 +570,11 @@ function registerNativeCallbacks(library: LoadedNativeLibrary) {
499
570
  evaluateResultHandler?.(viewId, parsed);
500
571
  break;
501
572
  }
573
+ case "screenshot-result": {
574
+ const parsed = maybeParsePayload(payload) as NativeScreenshotResult;
575
+ screenshotResultHandler?.(viewId, parsed);
576
+ break;
577
+ }
502
578
  case "title-changed": {
503
579
  const parsed = maybeParsePayload(payload) as { title: string };
504
580
  buniteEventEmitter.emitEvent(
@@ -507,6 +583,53 @@ function registerNativeCallbacks(library: LoadedNativeLibrary) {
507
583
  );
508
584
  break;
509
585
  }
586
+ case "load-start":
587
+ buniteEventEmitter.emitEvent(
588
+ buniteEventEmitter.events.webview.loadStart({ detail: payload }),
589
+ viewId
590
+ );
591
+ break;
592
+ case "load-finish":
593
+ buniteEventEmitter.emitEvent(
594
+ buniteEventEmitter.events.webview.loadFinish({ detail: payload }),
595
+ viewId
596
+ );
597
+ break;
598
+ case "load-fail": {
599
+ const parsed = maybeParsePayload(payload) as { url?: string; reason?: string };
600
+ buniteEventEmitter.emitEvent(
601
+ buniteEventEmitter.events.webview.loadFail({
602
+ url: parsed.url ?? "", reason: parsed.reason,
603
+ }),
604
+ viewId
605
+ );
606
+ break;
607
+ }
608
+ case "dialog": {
609
+ const parsed = maybeParsePayload(payload) as {
610
+ requestId: number;
611
+ kind: "alert" | "confirm" | "prompt" | "beforeunload";
612
+ message: string;
613
+ defaultPrompt?: string;
614
+ };
615
+ buniteEventEmitter.emitEvent(
616
+ buniteEventEmitter.events.webview.dialog(parsed),
617
+ viewId
618
+ );
619
+ break;
620
+ }
621
+ case "console-message": {
622
+ const parsed = maybeParsePayload(payload) as {
623
+ level: "log" | "warn" | "error" | "info" | "debug";
624
+ args: string[];
625
+ ts: number;
626
+ };
627
+ buniteEventEmitter.emitEvent(
628
+ buniteEventEmitter.events.webview.consoleMessage(parsed),
629
+ viewId
630
+ );
631
+ break;
632
+ }
510
633
  }
511
634
  },
512
635
  {
@@ -656,7 +779,7 @@ export async function initNativeRuntime(
656
779
  throw new Error(`bunite: failed to load native library at ${artifacts.nativeLibPath}.`);
657
780
  }
658
781
 
659
- const EXPECTED_ABI = 5;
782
+ const EXPECTED_ABI = 9;
660
783
  const nativeAbi = nativeLibrary.symbols.bunite_abi_version();
661
784
  if (nativeAbi !== EXPECTED_ABI) {
662
785
  throw new Error(
@@ -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
- extern "C" BUNITE_EXPORT void bunite_view_evaluate(uint32_t view_id, uint32_t request_id, const char* /*script*/) {
137
- // Stage A: Linux evaluate not yet implemented.
138
- std::string payload = "{\"requestId\":" + std::to_string(request_id) +
139
- ",\"ok\":false,\"code\":\"not_supported\","
140
- "\"message\":\"Linux evaluate not implemented (Stage A: Windows only)\"}";
141
- bunite_linux::emitWebviewEvent(view_id, "evaluate-result", payload);
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,129 @@ 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_respond_dialog(uint32_t view_id, uint32_t request_id,
360
+ bool accept, const char* text) {
361
+ bunite_linux::respondToDialogRequest(view_id, request_id, accept, text ? text : "");
362
+ }
363
+
364
+ // Screenshot — webkit_web_view_get_snapshot → cairo_surface_t → PNG bytes via
365
+ // cairo_surface_write_to_png_stream → g_base64_encode. JPEG path uses GdkPixbuf
366
+ // for `pixbuf_save_to_buffer(... "jpeg" ...)`.
367
+ namespace {
368
+
369
+ struct LinuxShotCtx {
370
+ uint32_t view_id;
371
+ uint32_t request_id;
372
+ std::string format; // "png" | "jpeg"
373
+ int32_t quality;
374
+ };
375
+
376
+ void emitLinuxShotError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& msg) {
377
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
378
+ ",\"ok\":false,\"code\":\"" + code + "\","
379
+ "\"message\":\"" + bunite_linux::escapeJsonString(msg) + "\"}";
380
+ bunite_linux::emitWebviewEvent(view_id, "screenshot-result", payload);
381
+ }
382
+
383
+ // WebKitGTK 2.52+ snapshot returns GdkTexture (was cairo_surface_t pre-2.52).
384
+ // PNG: gdk_texture_save_to_png_bytes — built-in. JPEG: bridge through GdkPixbuf
385
+ // via gdk_pixbuf_get_from_texture (non-deprecated GTK4 path).
386
+ void on_snapshot_done(GObject* source, GAsyncResult* res, gpointer user_data) {
387
+ auto* ctx = static_cast<LinuxShotCtx*>(user_data);
388
+ WebKitWebView* wv = WEBKIT_WEB_VIEW(source);
389
+ GError* err = nullptr;
390
+ GdkTexture* texture = webkit_web_view_get_snapshot_finish(wv, res, &err);
391
+ if (!texture) {
392
+ emitLinuxShotError(ctx->view_id, ctx->request_id, "runtime_error",
393
+ err ? err->message : "snapshot returned nil");
394
+ if (err) g_error_free(err);
395
+ delete ctx;
396
+ return;
397
+ }
398
+
399
+ std::vector<unsigned char> bytes;
400
+ std::string mime;
401
+ const bool jpeg = (ctx->format == "jpeg" || ctx->format == "jpg");
402
+ if (jpeg) {
403
+ GdkPixbuf* pix = gdk_pixbuf_get_from_texture(texture);
404
+ if (pix) {
405
+ gchar* raw = nullptr; gsize raw_len = 0;
406
+ char qbuf[8]; snprintf(qbuf, sizeof(qbuf), "%d",
407
+ ctx->quality < 0 ? 90 : (ctx->quality > 100 ? 100 : ctx->quality));
408
+ GError* perr = nullptr;
409
+ if (gdk_pixbuf_save_to_buffer(pix, &raw, &raw_len, "jpeg", &perr, "quality", qbuf, nullptr)) {
410
+ bytes.assign(raw, raw + raw_len);
411
+ g_free(raw);
412
+ } else if (perr) {
413
+ g_error_free(perr);
414
+ }
415
+ g_object_unref(pix);
416
+ }
417
+ mime = "image/jpeg";
418
+ ctx->format = "jpeg";
419
+ } else {
420
+ GBytes* png = gdk_texture_save_to_png_bytes(texture);
421
+ if (png) {
422
+ gsize n = 0;
423
+ const auto* p = static_cast<const unsigned char*>(g_bytes_get_data(png, &n));
424
+ bytes.assign(p, p + n);
425
+ g_bytes_unref(png);
426
+ }
427
+ mime = "image/png";
428
+ ctx->format = "png";
429
+ }
430
+ g_object_unref(texture);
431
+
432
+ if (bytes.empty()) {
433
+ emitLinuxShotError(ctx->view_id, ctx->request_id, "runtime_error", "encode failed");
434
+ delete ctx;
435
+ return;
436
+ }
437
+ gchar* b64 = g_base64_encode(bytes.data(), bytes.size());
438
+ std::string payload = "{\"requestId\":" + std::to_string(ctx->request_id) +
439
+ ",\"ok\":true,\"format\":\"" + ctx->format +
440
+ "\",\"mime\":\"" + mime +
441
+ "\",\"dataBase64\":\"" + (b64 ? b64 : "") + "\"}";
442
+ if (b64) g_free(b64);
443
+ bunite_linux::emitWebviewEvent(ctx->view_id, "screenshot-result", payload);
444
+ delete ctx;
445
+ }
446
+
447
+ } // namespace
448
+
449
+ extern "C" BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
450
+ // WebKitGTK — input dispatch impossible on GTK4+Wayland; screenshot via cairo.
451
+ auto* v = bunite_linux::findView(view_id);
452
+ if (!v) return 0;
453
+ return BUNITE_CAP_EVALUATE | BUNITE_CAP_SURFACE_EVENTS |
454
+ BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
455
+ BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG;
456
+ }
457
+
458
+ extern "C" BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t request_id,
459
+ const char* format, int32_t quality) {
460
+ std::string fmt = format ? format : "png";
461
+ runOnUiThreadSync([=]() {
462
+ auto* v = bunite_linux::findView(view_id);
463
+ if (!v || !v->webview) {
464
+ emitLinuxShotError(view_id, request_id, "not_supported", "view not ready");
465
+ return;
466
+ }
467
+ auto* ctx = new LinuxShotCtx{view_id, request_id, fmt, quality};
468
+ webkit_web_view_get_snapshot(v->webview, WEBKIT_SNAPSHOT_REGION_VISIBLE,
469
+ WEBKIT_SNAPSHOT_OPTIONS_NONE, nullptr,
470
+ on_snapshot_done, ctx);
471
+ });
472
+ }
473
+
257
474
  extern "C" BUNITE_EXPORT void bunite_view_open_devtools(uint32_t view_id) {
258
475
  (void)view_id; BUNITE_LINUX_TODO("bunite_view_open_devtools");
259
476
  }
@@ -38,6 +38,11 @@ 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;
41
46
  };
42
47
 
43
48
  struct RuntimeState {
@@ -134,6 +139,7 @@ void destroyWindow(uint32_t window_id);
134
139
 
135
140
  ViewState* findView(uint32_t view_id);
136
141
  uint32_t viewIdForWebView(WebKitWebView* wv);
142
+ void respondToDialogRequest(uint32_t view_id, uint32_t request_id, bool accept, const std::string& text);
137
143
  bool createView(uint32_t view_id, uint32_t window_id,
138
144
  const char* url, const char* html, const char* preload, const char* appres_root,
139
145
  const char* navigation_rules_json, const char* preload_origins_json,
@@ -16,7 +16,7 @@ bool isOnMainThread() {
16
16
 
17
17
  namespace {
18
18
 
19
- constexpr int32_t kBuniteAbiVersion = 5;
19
+ constexpr int32_t kBuniteAbiVersion = 9;
20
20
 
21
21
  } // namespace
22
22
 
@@ -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
- return url == "about:blank" ||
97
- url.rfind("appres://app.internal/internal/", 0) == 0;
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) {
@@ -18,11 +18,15 @@ void emit_url(uint32_t view_id, const char* name, WebKitWebView* wv) {
18
18
  void on_load_changed(WebKitWebView* wv, WebKitLoadEvent event, gpointer user_data) {
19
19
  const uint32_t view_id = GPOINTER_TO_UINT(user_data);
20
20
  switch (event) {
21
+ case WEBKIT_LOAD_STARTED:
22
+ emit_url(view_id, "load-start", wv);
23
+ break;
21
24
  case WEBKIT_LOAD_COMMITTED:
22
25
  emit_url(view_id, "did-navigate", wv);
23
26
  queueViewRedraw(wv);
24
27
  break;
25
28
  case WEBKIT_LOAD_FINISHED:
29
+ emit_url(view_id, "load-finish", wv);
26
30
  emit_url(view_id, "dom-ready", wv);
27
31
  queueViewRedraw(wv);
28
32
  break;
@@ -30,6 +34,55 @@ void on_load_changed(WebKitWebView* wv, WebKitLoadEvent event, gpointer user_dat
30
34
  }
31
35
  }
32
36
 
37
+ gboolean on_load_failed(WebKitWebView* /*wv*/, WebKitLoadEvent /*ev*/, const char* failing_uri,
38
+ GError* error, gpointer user_data) {
39
+ const uint32_t view_id = GPOINTER_TO_UINT(user_data);
40
+ std::string payload = "{\"url\":\"" + escapeJsonString(failing_uri ? failing_uri : "") +
41
+ "\",\"reason\":\"" + escapeJsonString(error && error->message ? error->message : "") + "\"}";
42
+ emitWebviewEvent(view_id, "load-fail", payload);
43
+ return FALSE; // let WebKit show its default error page
44
+ }
45
+
46
+ gboolean on_load_failed_tls(WebKitWebView* /*wv*/, const char* failing_uri,
47
+ GTlsCertificate* /*cert*/, GTlsCertificateFlags /*errors*/,
48
+ gpointer user_data) {
49
+ const uint32_t view_id = GPOINTER_TO_UINT(user_data);
50
+ std::string payload = "{\"url\":\"" + escapeJsonString(failing_uri ? failing_uri : "") +
51
+ "\",\"reason\":\"tls-certificate-error\"}";
52
+ emitWebviewEvent(view_id, "load-fail", payload);
53
+ return FALSE; // do not override default certificate failure behavior
54
+ }
55
+
56
+ gboolean on_script_dialog(WebKitWebView* /*wv*/, WebKitScriptDialog* dialog, gpointer user_data) {
57
+ const uint32_t view_id = GPOINTER_TO_UINT(user_data);
58
+ auto* v = findView(view_id);
59
+ if (!v) return FALSE;
60
+ WebKitScriptDialogType type = webkit_script_dialog_get_dialog_type(dialog);
61
+ const char* kind = nullptr;
62
+ switch (type) {
63
+ case WEBKIT_SCRIPT_DIALOG_ALERT: kind = "alert"; break;
64
+ case WEBKIT_SCRIPT_DIALOG_CONFIRM: kind = "confirm"; break;
65
+ case WEBKIT_SCRIPT_DIALOG_PROMPT: kind = "prompt"; break;
66
+ case WEBKIT_SCRIPT_DIALOG_BEFORE_UNLOAD_CONFIRM: kind = "beforeunload"; break;
67
+ default: return FALSE;
68
+ }
69
+ // Defer the dialog so the page execution stays paused until host responds.
70
+ webkit_script_dialog_ref(dialog);
71
+ const uint32_t rid = v->next_dialog_request_id++;
72
+ v->pending_dialogs[rid] = dialog;
73
+ const char* message = webkit_script_dialog_get_message(dialog);
74
+ std::string payload = "{\"requestId\":" + std::to_string(rid) +
75
+ ",\"kind\":\"" + kind +
76
+ "\",\"message\":\"" + escapeJsonString(message ? message : "") + "\"";
77
+ if (type == WEBKIT_SCRIPT_DIALOG_PROMPT) {
78
+ const char* def = webkit_script_dialog_prompt_get_default_text(dialog);
79
+ payload += ",\"defaultPrompt\":\"" + escapeJsonString(def ? def : "") + "\"";
80
+ }
81
+ payload += "}";
82
+ emitWebviewEvent(view_id, "dialog", payload);
83
+ return TRUE; // we handled it
84
+ }
85
+
33
86
  GtkWidget* on_create(WebKitWebView* wv, WebKitNavigationAction* action, gpointer user_data) {
34
87
  const uint32_t view_id = GPOINTER_TO_UINT(user_data);
35
88
  WebKitURIRequest* req = webkit_navigation_action_get_request(action);
@@ -40,6 +93,14 @@ GtkWidget* on_create(WebKitWebView* wv, WebKitNavigationAction* action, gpointer
40
93
  return nullptr; // cancel; JS opens via load_url if desired
41
94
  }
42
95
 
96
+ void on_title_changed(GObject* source, GParamSpec* /*pspec*/, gpointer user_data) {
97
+ const uint32_t view_id = GPOINTER_TO_UINT(user_data);
98
+ WebKitWebView* wv = WEBKIT_WEB_VIEW(source);
99
+ const char* title = webkit_web_view_get_title(wv);
100
+ std::string payload = "{\"title\":\"" + escapeJsonString(title ? title : "") + "\"}";
101
+ emitWebviewEvent(view_id, "title-changed", payload);
102
+ }
103
+
43
104
  // WebKitGTK fires nav-action for sub-frames with no main-frame discriminator — iframes hit nav rules.
44
105
  gboolean on_decide_policy(WebKitWebView* wv, WebKitPolicyDecision* decision,
45
106
  WebKitPolicyDecisionType type, gpointer user_data) {
@@ -72,6 +133,26 @@ uint32_t viewIdForWebView(WebKitWebView* wv) {
72
133
  return GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(wv), kViewIdKey));
73
134
  }
74
135
 
136
+ void respondToDialogRequest(uint32_t view_id, uint32_t request_id, bool accept,
137
+ const std::string& text) {
138
+ auto* v = findView(view_id);
139
+ if (!v) return;
140
+ auto it = v->pending_dialogs.find(request_id);
141
+ if (it == v->pending_dialogs.end()) return;
142
+ WebKitScriptDialog* dialog = it->second;
143
+ v->pending_dialogs.erase(it);
144
+ if (!dialog) return;
145
+ WebKitScriptDialogType type = webkit_script_dialog_get_dialog_type(dialog);
146
+ if (type == WEBKIT_SCRIPT_DIALOG_CONFIRM || type == WEBKIT_SCRIPT_DIALOG_BEFORE_UNLOAD_CONFIRM) {
147
+ webkit_script_dialog_confirm_set_confirmed(dialog, accept ? TRUE : FALSE);
148
+ } else if (type == WEBKIT_SCRIPT_DIALOG_PROMPT) {
149
+ if (accept) webkit_script_dialog_prompt_set_text(dialog, text.c_str());
150
+ else webkit_script_dialog_prompt_set_text(dialog, nullptr);
151
+ }
152
+ webkit_script_dialog_close(dialog);
153
+ webkit_script_dialog_unref(dialog);
154
+ }
155
+
75
156
  bool createView(uint32_t view_id, uint32_t window_id,
76
157
  const char* url, const char* html, const char* preload, const char* appres_root,
77
158
  const char* navigation_rules_json, const char* preload_origins_json,
@@ -122,8 +203,12 @@ bool createView(uint32_t view_id, uint32_t window_id,
122
203
  registerAppresScheme(webkit_web_view_get_context(wv));
123
204
 
124
205
  g_signal_connect(wv, "load-changed", G_CALLBACK(on_load_changed), GUINT_TO_POINTER(view_id));
206
+ g_signal_connect(wv, "load-failed", G_CALLBACK(on_load_failed), GUINT_TO_POINTER(view_id));
207
+ g_signal_connect(wv, "load-failed-with-tls-errors", G_CALLBACK(on_load_failed_tls), GUINT_TO_POINTER(view_id));
125
208
  g_signal_connect(wv, "decide-policy", G_CALLBACK(on_decide_policy), GUINT_TO_POINTER(view_id));
126
209
  g_signal_connect(wv, "create", G_CALLBACK(on_create), GUINT_TO_POINTER(view_id));
210
+ g_signal_connect(wv, "notify::title", G_CALLBACK(on_title_changed), GUINT_TO_POINTER(view_id));
211
+ g_signal_connect(wv, "script-dialog", G_CALLBACK(on_script_dialog), GUINT_TO_POINTER(view_id));
127
212
 
128
213
  GtkWidget* container = GTK_WIDGET(wv);
129
214
  if (auto_resize) {