bunite-core 0.5.0 → 0.8.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 -2
- package/src/bun/core/App.ts +35 -34
- package/src/bun/core/BrowserView.ts +3 -9
- package/src/bun/core/singleInstanceLock.ts +91 -0
- package/src/bun/index.ts +5 -6
- package/src/bun/preload/inline.ts +1 -2
- package/src/bun/proc/native.ts +54 -78
- package/src/native/linux/bunite_linux_appres.cpp +173 -0
- package/src/native/linux/bunite_linux_ffi.cpp +263 -0
- package/src/native/linux/bunite_linux_internal.h +148 -0
- package/src/native/linux/bunite_linux_preload.cpp +1 -0
- package/src/native/linux/bunite_linux_runtime.cpp +120 -0
- package/src/native/linux/bunite_linux_utils.cpp +114 -0
- package/src/native/linux/bunite_linux_view.cpp +244 -0
- package/src/native/linux/bunite_linux_window.cpp +101 -0
- package/src/native/mac/bunite_mac_appres.mm +163 -0
- package/src/native/mac/bunite_mac_ffi.mm +470 -0
- package/src/native/mac/bunite_mac_internal.h +151 -0
- package/src/native/mac/bunite_mac_runtime.mm +15 -0
- package/src/native/mac/bunite_mac_utils.mm +121 -0
- package/src/native/mac/bunite_mac_view.mm +279 -0
- package/src/native/mac/bunite_mac_window.mm +187 -0
- package/src/native/shared/ffi_exports.h +13 -13
- package/src/native/shared/permissions.h +14 -0
- package/src/native/shared/webview_storage.h +6 -3
- package/src/native/win/native_host_cef.cpp +4 -8
- package/src/native/win/native_host_ffi.cpp +76 -123
- package/src/native/win/native_host_internal.h +5 -3
- package/src/native/win/native_host_runtime.cpp +2 -6
- package/src/native/win/native_host_utils.cpp +23 -52
- package/src/native/win/process_helper_win.cpp +1 -3
- package/src/preload/runtime.ts +1 -3
- package/src/preload/tsconfig.tsbuildinfo +1 -1
- package/src/preload/webviewElement.ts +3 -8
- package/src/shared/paths.ts +35 -44
- package/src/shared/platform.ts +1 -2
- package/src/shared/rpc.ts +4 -13
- package/src/shared/rpcDemux.ts +47 -2
- package/src/shared/webRpcHandler.ts +1 -3
- package/src/shared/webviewPolyfill.ts +64 -6
- package/src/bun/core/Utils.ts +0 -301
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// macOS adapter — shared internal declarations.
|
|
2
|
+
//
|
|
3
|
+
// ARC: Obj-C objects in C++ aggregates need explicit __strong/__weak (else UB).
|
|
4
|
+
// Threading: NSWindow / WKWebView APIs are main-thread-only — cross-thread via runOnUiThreadSync.
|
|
5
|
+
|
|
6
|
+
#pragma once
|
|
7
|
+
|
|
8
|
+
#import <Foundation/Foundation.h>
|
|
9
|
+
#import <AppKit/AppKit.h>
|
|
10
|
+
#import <WebKit/WebKit.h>
|
|
11
|
+
|
|
12
|
+
#include <atomic>
|
|
13
|
+
#include <cstdint>
|
|
14
|
+
#include <functional>
|
|
15
|
+
#include <mutex>
|
|
16
|
+
#include <string>
|
|
17
|
+
#include <unordered_map>
|
|
18
|
+
#include <vector>
|
|
19
|
+
|
|
20
|
+
#include "callbacks.h"
|
|
21
|
+
#include "ffi_exports.h"
|
|
22
|
+
#include "log.h"
|
|
23
|
+
#include "permissions.h"
|
|
24
|
+
|
|
25
|
+
@interface BunitePendingPermission : NSObject
|
|
26
|
+
@property (nonatomic, assign) uint32_t viewId;
|
|
27
|
+
@property (nonatomic, copy) void (^handler)(WKPermissionDecision);
|
|
28
|
+
@end
|
|
29
|
+
|
|
30
|
+
@interface BunitePendingRoute : NSObject
|
|
31
|
+
@property (nonatomic, assign) uint32_t viewId;
|
|
32
|
+
@property (nonatomic, strong) id<WKURLSchemeTask> task;
|
|
33
|
+
@end
|
|
34
|
+
|
|
35
|
+
// Stable host for passthrough + mask (WKWebView's internal subviews are fragile).
|
|
36
|
+
// Layer-backed (CAShapeLayer mask), isFlipped (coords match BuniteFlippedView).
|
|
37
|
+
// `maskHoles` gates hitTest alongside the visual mask — clicks pass through (matches win SetWindowRgn).
|
|
38
|
+
@interface BunitePassthroughContainer : NSView
|
|
39
|
+
@property (nonatomic, assign) BOOL passthrough;
|
|
40
|
+
@property (nonatomic, copy) NSArray<NSValue*>* maskHoles; // NSValue.rectValue
|
|
41
|
+
@end
|
|
42
|
+
|
|
43
|
+
namespace bunite_mac {
|
|
44
|
+
|
|
45
|
+
// --- Per-object state ---
|
|
46
|
+
|
|
47
|
+
struct WindowState {
|
|
48
|
+
__strong NSWindow* window = nil;
|
|
49
|
+
__strong NSObject<NSWindowDelegate>* delegate = nil;
|
|
50
|
+
std::atomic<bool> close_pending{false};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
struct ViewState {
|
|
54
|
+
__strong BunitePassthroughContainer* container = nil; // window.contentView 자식
|
|
55
|
+
__strong WKWebView* webview = nil; // container 자식 (autoresize 채움)
|
|
56
|
+
__strong NSObject<WKNavigationDelegate>* nav_delegate = nil;
|
|
57
|
+
__strong NSObject<WKUIDelegate>* ui_delegate = nil;
|
|
58
|
+
uint32_t window_id = 0;
|
|
59
|
+
std::string appres_root;
|
|
60
|
+
std::string preload_script;
|
|
61
|
+
// HTML stashed by load_html; appres handler serves at internal/index.html.
|
|
62
|
+
std::string stored_html;
|
|
63
|
+
std::vector<std::string> navigation_rules;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Runtime singleton.
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
struct RuntimeState {
|
|
71
|
+
std::mutex object_mutex;
|
|
72
|
+
std::unordered_map<uint32_t, WindowState> windows;
|
|
73
|
+
std::unordered_map<uint32_t, ViewState> views;
|
|
74
|
+
|
|
75
|
+
bool initialized = false;
|
|
76
|
+
bool popup_blocking = false;
|
|
77
|
+
std::atomic<bool> shutting_down{false};
|
|
78
|
+
|
|
79
|
+
// Pending permission decisions (request_id → entry). Main-thread only, no mutex.
|
|
80
|
+
__strong NSMutableDictionary<NSNumber*, BunitePendingPermission*>* pending_permissions = nil;
|
|
81
|
+
uint32_t next_permission_request_id = 1;
|
|
82
|
+
|
|
83
|
+
// In-flight dynamic-route tasks (request_id → entry). view_id lets removeView clean up without WebKit stop. Main-thread only.
|
|
84
|
+
__strong NSMutableDictionary<NSNumber*, BunitePendingRoute*>* pending_route_tasks = nil;
|
|
85
|
+
uint32_t next_route_request_id = 1;
|
|
86
|
+
|
|
87
|
+
BuniteWebviewEventHandler webview_event_handler = nullptr;
|
|
88
|
+
BuniteWindowEventHandler window_event_handler = nullptr;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
extern RuntimeState g_runtime;
|
|
92
|
+
|
|
93
|
+
// --- Thread helpers (main-thread fast path; cross-thread = dispatch_sync) ---
|
|
94
|
+
|
|
95
|
+
bool isOnMainThread();
|
|
96
|
+
|
|
97
|
+
template <typename Block>
|
|
98
|
+
auto runOnUiThreadSync(Block block) -> decltype(block()) {
|
|
99
|
+
using R = decltype(block());
|
|
100
|
+
if (isOnMainThread()) return block();
|
|
101
|
+
if constexpr (std::is_void_v<R>) {
|
|
102
|
+
dispatch_sync(dispatch_get_main_queue(), ^{ block(); });
|
|
103
|
+
} else {
|
|
104
|
+
__block R result{};
|
|
105
|
+
dispatch_sync(dispatch_get_main_queue(), ^{ result = block(); });
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Utilities (defined in bunite_mac_utils.mm).
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
NSString* utf8ToNSString(const char* value);
|
|
115
|
+
|
|
116
|
+
std::string escapeJsonString(const std::string& value);
|
|
117
|
+
|
|
118
|
+
// Cocoa screen is bottom-left, Win semantics (and FFI) are top-left.
|
|
119
|
+
NSRect topLeftToBottomLeft(double x, double y, double width, double height);
|
|
120
|
+
void bottomLeftToTopLeft(NSRect frame, double* out_x, double* out_y, double* out_w, double* out_h);
|
|
121
|
+
|
|
122
|
+
// Payload must be valid UTF-8 JSON.
|
|
123
|
+
void emitWindowEvent(uint32_t window_id, const char* event_name, const std::string& payload = {});
|
|
124
|
+
void emitWebviewEvent(uint32_t view_id, const char* event_name, const std::string& payload = {});
|
|
125
|
+
|
|
126
|
+
// Defined in bunite_mac_window.mm.
|
|
127
|
+
WindowState* findWindow(uint32_t window_id); // returns nullptr if missing
|
|
128
|
+
bool createWindow(uint32_t window_id, double x, double y, double width, double height,
|
|
129
|
+
NSString* title, NSString* title_bar_style,
|
|
130
|
+
bool transparent, bool hidden, bool minimized, bool maximized);
|
|
131
|
+
void destroyWindow(uint32_t window_id);
|
|
132
|
+
|
|
133
|
+
// Defined in bunite_mac_view.mm.
|
|
134
|
+
ViewState* findView(uint32_t view_id);
|
|
135
|
+
uint32_t viewIdForWebView(WKWebView* wv); // returns 0 if not tracked
|
|
136
|
+
bool createView(uint32_t view_id, uint32_t window_id,
|
|
137
|
+
NSString* url, NSString* html, NSString* preload, NSString* appres_root,
|
|
138
|
+
NSString* navigation_rules_json, NSString* preload_origins_json,
|
|
139
|
+
double x, double y, double width, double height, bool auto_resize);
|
|
140
|
+
void removeView(uint32_t view_id);
|
|
141
|
+
|
|
142
|
+
// Glob patterns, last-match-wins, default-allow.
|
|
143
|
+
bool globMatchCaseInsensitive(const std::string& pattern, const std::string& value);
|
|
144
|
+
std::vector<std::string> parseNavigationRulesJson(NSString* json);
|
|
145
|
+
bool shouldAlwaysAllowNavigationUrl(const std::string& url);
|
|
146
|
+
bool shouldAllowNavigation(const ViewState* view, const std::string& url);
|
|
147
|
+
|
|
148
|
+
// Defined in bunite_mac_appres.mm.
|
|
149
|
+
id<WKURLSchemeHandler> sharedAppresSchemeHandler();
|
|
150
|
+
|
|
151
|
+
} // namespace bunite_mac
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Process-wide singletons + thread helpers + NSApp lifecycle for the macOS adapter.
|
|
2
|
+
|
|
3
|
+
#import "bunite_mac_internal.h"
|
|
4
|
+
|
|
5
|
+
#include <CoreFoundation/CoreFoundation.h>
|
|
6
|
+
|
|
7
|
+
namespace bunite_mac {
|
|
8
|
+
|
|
9
|
+
RuntimeState g_runtime;
|
|
10
|
+
|
|
11
|
+
bool isOnMainThread() {
|
|
12
|
+
return [NSThread isMainThread] == YES;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
} // namespace bunite_mac
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// String conversion + event emit helpers + navigation rule matching.
|
|
2
|
+
|
|
3
|
+
#import "bunite_mac_internal.h"
|
|
4
|
+
|
|
5
|
+
#include <cctype>
|
|
6
|
+
#include <cstdio>
|
|
7
|
+
#include <cstring> // strdup
|
|
8
|
+
|
|
9
|
+
namespace bunite_mac {
|
|
10
|
+
|
|
11
|
+
NSString* utf8ToNSString(const char* value) {
|
|
12
|
+
if (!value) return @"";
|
|
13
|
+
return [NSString stringWithUTF8String:value] ?: @"";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
std::string escapeJsonString(const std::string& value) {
|
|
17
|
+
std::string out;
|
|
18
|
+
out.reserve(value.size() + 8);
|
|
19
|
+
for (unsigned char c : value) {
|
|
20
|
+
switch (c) {
|
|
21
|
+
case '"': out += "\\\""; break;
|
|
22
|
+
case '\\': out += "\\\\"; break;
|
|
23
|
+
case '\b': out += "\\b"; break;
|
|
24
|
+
case '\f': out += "\\f"; break;
|
|
25
|
+
case '\n': out += "\\n"; break;
|
|
26
|
+
case '\r': out += "\\r"; break;
|
|
27
|
+
case '\t': out += "\\t"; break;
|
|
28
|
+
default:
|
|
29
|
+
if (c < 0x20) {
|
|
30
|
+
char buf[8];
|
|
31
|
+
std::snprintf(buf, sizeof(buf), "\\u%04x", c);
|
|
32
|
+
out += buf;
|
|
33
|
+
} else {
|
|
34
|
+
out += static_cast<char>(c);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
NSRect topLeftToBottomLeft(double x, double y, double width, double height) {
|
|
42
|
+
CGFloat screenH = [NSScreen mainScreen].frame.size.height;
|
|
43
|
+
return NSMakeRect(x, screenH - y - height, width, height);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
void bottomLeftToTopLeft(NSRect frame, double* out_x, double* out_y, double* out_w, double* out_h) {
|
|
47
|
+
CGFloat screenH = [NSScreen mainScreen].frame.size.height;
|
|
48
|
+
*out_x = frame.origin.x;
|
|
49
|
+
*out_y = screenH - frame.origin.y - frame.size.height;
|
|
50
|
+
*out_w = frame.size.width;
|
|
51
|
+
*out_h = frame.size.height;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
void emitWindowEvent(uint32_t window_id, const char* event_name, const std::string& payload) {
|
|
55
|
+
// Bun calls bunite_free_cstring — strdup so we don't return .rodata or temporary pointers.
|
|
56
|
+
if (BuniteWindowEventHandler h = g_runtime.window_event_handler) {
|
|
57
|
+
h(window_id, strdup(event_name ? event_name : ""), strdup(payload.c_str()));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
void emitWebviewEvent(uint32_t view_id, const char* event_name, const std::string& payload) {
|
|
62
|
+
if (BuniteWebviewEventHandler h = g_runtime.webview_event_handler) {
|
|
63
|
+
h(view_id, strdup(event_name ? event_name : ""), strdup(payload.c_str()));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
bool globMatchCaseInsensitive(const std::string& pattern, const std::string& value) {
|
|
68
|
+
size_t pi = 0, vi = 0;
|
|
69
|
+
size_t star_p = std::string::npos, star_v = 0;
|
|
70
|
+
while (vi < value.size()) {
|
|
71
|
+
if (pi < pattern.size() &&
|
|
72
|
+
std::tolower(static_cast<unsigned char>(pattern[pi])) ==
|
|
73
|
+
std::tolower(static_cast<unsigned char>(value[vi]))) {
|
|
74
|
+
++pi; ++vi;
|
|
75
|
+
} else if (pi < pattern.size() && pattern[pi] == '*') {
|
|
76
|
+
star_p = pi++; star_v = vi;
|
|
77
|
+
} else if (star_p != std::string::npos) {
|
|
78
|
+
pi = star_p + 1; vi = ++star_v;
|
|
79
|
+
} else {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
while (pi < pattern.size() && pattern[pi] == '*') ++pi;
|
|
84
|
+
return pi == pattern.size();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
std::vector<std::string> parseNavigationRulesJson(NSString* json) {
|
|
88
|
+
std::vector<std::string> rules;
|
|
89
|
+
if (json.length == 0) return rules;
|
|
90
|
+
NSData* data = [json dataUsingEncoding:NSUTF8StringEncoding];
|
|
91
|
+
if (!data) return rules;
|
|
92
|
+
id parsed = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
93
|
+
if (![parsed isKindOfClass:[NSArray class]]) return rules;
|
|
94
|
+
for (id entry in (NSArray*)parsed) {
|
|
95
|
+
if (![entry isKindOfClass:[NSString class]]) continue;
|
|
96
|
+
const char* s = [(NSString*)entry UTF8String];
|
|
97
|
+
if (s && *s) rules.emplace_back(s);
|
|
98
|
+
}
|
|
99
|
+
return rules;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
bool shouldAlwaysAllowNavigationUrl(const std::string& url) {
|
|
103
|
+
return url == "about:blank" ||
|
|
104
|
+
url.rfind("appres://app.internal/internal/", 0) == 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
bool shouldAllowNavigation(const ViewState* view, const std::string& url) {
|
|
108
|
+
if (!view || shouldAlwaysAllowNavigationUrl(url) || view->navigation_rules.empty()) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
bool allowed = true; // default-allow, last-match-wins
|
|
112
|
+
for (const std::string& raw : view->navigation_rules) {
|
|
113
|
+
const bool block = !raw.empty() && raw.front() == '^';
|
|
114
|
+
const std::string pattern = block ? raw.substr(1) : raw;
|
|
115
|
+
if (pattern.empty()) continue;
|
|
116
|
+
if (globMatchCaseInsensitive(pattern, url)) allowed = !block;
|
|
117
|
+
}
|
|
118
|
+
return allowed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
} // namespace bunite_mac
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// WKWebView lifecycle + navigation/UI delegates.
|
|
2
|
+
|
|
3
|
+
#import "bunite_mac_internal.h"
|
|
4
|
+
|
|
5
|
+
#include "webview_storage.h"
|
|
6
|
+
|
|
7
|
+
@implementation BunitePendingPermission
|
|
8
|
+
@end
|
|
9
|
+
|
|
10
|
+
@implementation BunitePendingRoute
|
|
11
|
+
@end
|
|
12
|
+
|
|
13
|
+
@implementation BunitePassthroughContainer
|
|
14
|
+
- (BOOL)isFlipped { return YES; }
|
|
15
|
+
- (NSView*)hitTest:(NSPoint)point {
|
|
16
|
+
if (self.passthrough) return nil;
|
|
17
|
+
if (self.maskHoles.count > 0) {
|
|
18
|
+
NSPoint local = [self convertPoint:point fromView:self.superview];
|
|
19
|
+
for (NSValue* v in self.maskHoles) {
|
|
20
|
+
if (NSPointInRect(local, v.rectValue)) return nil;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return [super hitTest:point];
|
|
24
|
+
}
|
|
25
|
+
@end
|
|
26
|
+
|
|
27
|
+
using bunite_mac::g_runtime;
|
|
28
|
+
using bunite_mac::utf8ToNSString;
|
|
29
|
+
|
|
30
|
+
@interface BuniteNavigationDelegate : NSObject <WKNavigationDelegate>
|
|
31
|
+
@end
|
|
32
|
+
|
|
33
|
+
@implementation BuniteNavigationDelegate
|
|
34
|
+
|
|
35
|
+
- (void)webView:(WKWebView*)wv
|
|
36
|
+
decidePolicyForNavigationAction:(WKNavigationAction*)action
|
|
37
|
+
decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler
|
|
38
|
+
{
|
|
39
|
+
// nil targetFrame = popup (window.open, target=_blank, ctrl-click); apply main-frame rules.
|
|
40
|
+
if (action.targetFrame && !action.targetFrame.mainFrame) {
|
|
41
|
+
decisionHandler(WKNavigationActionPolicyAllow);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
uint32_t view_id = bunite_mac::viewIdForWebView(wv);
|
|
45
|
+
const std::string url_str = (action.request.URL.absoluteString ?: @"").UTF8String;
|
|
46
|
+
// Evaluate before will-navigate — a synchronous handler could destroy the view.
|
|
47
|
+
const bool allow = bunite_mac::shouldAllowNavigation(bunite_mac::findView(view_id), url_str);
|
|
48
|
+
bunite_mac::emitWebviewEvent(view_id, "will-navigate", url_str);
|
|
49
|
+
decisionHandler(allow ? WKNavigationActionPolicyAllow : WKNavigationActionPolicyCancel);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
- (void)webView:(WKWebView*)wv didFinishNavigation:(WKNavigation*)nav {
|
|
53
|
+
(void)nav;
|
|
54
|
+
uint32_t view_id = bunite_mac::viewIdForWebView(wv);
|
|
55
|
+
NSString* url = wv.URL.absoluteString ?: @"";
|
|
56
|
+
bunite_mac::emitWebviewEvent(view_id, "did-navigate", url.UTF8String);
|
|
57
|
+
bunite_mac::emitWebviewEvent(view_id, "dom-ready", url.UTF8String);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@end
|
|
61
|
+
|
|
62
|
+
@interface BuniteUIDelegate : NSObject <WKUIDelegate>
|
|
63
|
+
@end
|
|
64
|
+
|
|
65
|
+
@implementation BuniteUIDelegate
|
|
66
|
+
|
|
67
|
+
- (WKWebView*)webView:(WKWebView*)wv
|
|
68
|
+
createWebViewWithConfiguration:(WKWebViewConfiguration*)config
|
|
69
|
+
forNavigationAction:(WKNavigationAction*)action
|
|
70
|
+
windowFeatures:(WKWindowFeatures*)features
|
|
71
|
+
{
|
|
72
|
+
(void)config; (void)features;
|
|
73
|
+
uint32_t view_id = bunite_mac::viewIdForWebView(wv);
|
|
74
|
+
NSString* url = action.request.URL.absoluteString ?: @"";
|
|
75
|
+
// Match win OnBeforePopup/OnOpenURLFromTab: emit, cancel; JS decides via load_url.
|
|
76
|
+
std::string payload = "{\"url\":\"" +
|
|
77
|
+
bunite_mac::escapeJsonString(url.UTF8String ?: "") + "\"}";
|
|
78
|
+
bunite_mac::emitWebviewEvent(view_id, "new-window-open", payload);
|
|
79
|
+
return nil;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
- (void)webView:(WKWebView*)wv
|
|
83
|
+
requestMediaCapturePermissionForOrigin:(WKSecurityOrigin*)origin
|
|
84
|
+
initiatedByFrame:(WKFrameInfo*)frame
|
|
85
|
+
type:(WKMediaCaptureType)type
|
|
86
|
+
decisionHandler:(void(^)(WKPermissionDecision))decisionHandler
|
|
87
|
+
API_AVAILABLE(macos(12.0))
|
|
88
|
+
{
|
|
89
|
+
(void)frame;
|
|
90
|
+
uint32_t view_id = bunite_mac::viewIdForWebView(wv);
|
|
91
|
+
uint32_t kind = 0;
|
|
92
|
+
switch (type) {
|
|
93
|
+
case WKMediaCaptureTypeCamera:
|
|
94
|
+
kind = BUNITE_PERMISSION_CAMERA; break;
|
|
95
|
+
case WKMediaCaptureTypeMicrophone:
|
|
96
|
+
kind = BUNITE_PERMISSION_MICROPHONE; break;
|
|
97
|
+
case WKMediaCaptureTypeCameraAndMicrophone:
|
|
98
|
+
kind = BUNITE_PERMISSION_CAMERA | BUNITE_PERMISSION_MICROPHONE; break;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!bunite_mac::g_runtime.pending_permissions) {
|
|
102
|
+
bunite_mac::g_runtime.pending_permissions = [NSMutableDictionary dictionary];
|
|
103
|
+
}
|
|
104
|
+
uint32_t request_id = bunite_mac::g_runtime.next_permission_request_id++;
|
|
105
|
+
BunitePendingPermission* entry = [[BunitePendingPermission alloc] init];
|
|
106
|
+
entry.viewId = view_id;
|
|
107
|
+
entry.handler = decisionHandler;
|
|
108
|
+
bunite_mac::g_runtime.pending_permissions[@(request_id)] = entry;
|
|
109
|
+
|
|
110
|
+
NSString* origin_str = origin.port != 0
|
|
111
|
+
? [NSString stringWithFormat:@"%@://%@:%ld", origin.protocol, origin.host, (long)origin.port]
|
|
112
|
+
: [NSString stringWithFormat:@"%@://%@", origin.protocol, origin.host];
|
|
113
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
114
|
+
",\"kind\":" + std::to_string(kind) +
|
|
115
|
+
",\"url\":\"" + bunite_mac::escapeJsonString(origin_str.UTF8String ?: "") + "\"}";
|
|
116
|
+
bunite_mac::emitWebviewEvent(view_id, "permission-requested", payload);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@end
|
|
120
|
+
|
|
121
|
+
namespace {
|
|
122
|
+
|
|
123
|
+
// __weak NSMapTable so we don't retain WKWebViews via the lookup table.
|
|
124
|
+
NSMapTable<WKWebView*, NSNumber*>* webviewIdTable() {
|
|
125
|
+
static NSMapTable* t = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsWeakMemory
|
|
126
|
+
valueOptions:NSPointerFunctionsStrongMemory];
|
|
127
|
+
return t;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
} // namespace
|
|
131
|
+
|
|
132
|
+
namespace bunite_mac {
|
|
133
|
+
|
|
134
|
+
ViewState* findView(uint32_t view_id) {
|
|
135
|
+
std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
|
|
136
|
+
auto it = g_runtime.views.find(view_id);
|
|
137
|
+
return it == g_runtime.views.end() ? nullptr : &it->second;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
uint32_t viewIdForWebView(WKWebView* wv) {
|
|
141
|
+
NSNumber* id = [webviewIdTable() objectForKey:wv];
|
|
142
|
+
return id ? id.unsignedIntValue : 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
bool createView(uint32_t view_id, uint32_t window_id,
|
|
146
|
+
NSString* url, NSString* html, NSString* preload, NSString* appres_root,
|
|
147
|
+
NSString* navigation_rules_json, NSString* preload_origins_json,
|
|
148
|
+
double x, double y, double width, double height, bool auto_resize) {
|
|
149
|
+
WindowState* window_state = findWindow(window_id);
|
|
150
|
+
if (!window_state) {
|
|
151
|
+
BUNITE_WARN("bunite_view_create: window %u not found.", window_id);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if (findView(view_id)) {
|
|
155
|
+
BUNITE_WARN("bunite_view_create: view %u already exists.", view_id);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init];
|
|
160
|
+
[config setURLSchemeHandler:sharedAppresSchemeHandler() forURLScheme:@"appres"];
|
|
161
|
+
// popup_blocking=true → block popups without user gesture (default).
|
|
162
|
+
config.preferences.javaScriptCanOpenWindowsAutomatically = !g_runtime.popup_blocking;
|
|
163
|
+
|
|
164
|
+
if (preload.length > 0) {
|
|
165
|
+
// WKUserScript has no per-origin filter — gate in-script so remote pages don't inherit RPC bridge + secret.
|
|
166
|
+
NSString* origins = preload_origins_json.length > 0 ? preload_origins_json : @"[]";
|
|
167
|
+
NSString* gated = [NSString stringWithFormat:
|
|
168
|
+
@"(function(){"
|
|
169
|
+
@" var _o=%@;_o.push('appres://app.internal');"
|
|
170
|
+
@" if(_o.indexOf(location.origin)<0)return;"
|
|
171
|
+
@" %@"
|
|
172
|
+
@"})();", origins, preload];
|
|
173
|
+
WKUserScript* script = [[WKUserScript alloc] initWithSource:gated
|
|
174
|
+
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
|
|
175
|
+
forMainFrameOnly:YES];
|
|
176
|
+
[config.userContentController addUserScript:script];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// auto_resize=true: logical points (main view). auto_resize=false: physical pixels (surface, JS × DPR).
|
|
180
|
+
NSRect frame;
|
|
181
|
+
if (auto_resize) {
|
|
182
|
+
frame = NSMakeRect(x, y, width, height);
|
|
183
|
+
} else {
|
|
184
|
+
CGFloat dpr = window_state->window.backingScaleFactor ?: 1.0;
|
|
185
|
+
frame = NSMakeRect(x / dpr, y / dpr, width / dpr, height / dpr);
|
|
186
|
+
}
|
|
187
|
+
BunitePassthroughContainer* container = [[BunitePassthroughContainer alloc] initWithFrame:frame];
|
|
188
|
+
container.wantsLayer = YES; // enable CAShapeLayer mask (set_mask_region)
|
|
189
|
+
|
|
190
|
+
WKWebView* wv = [[WKWebView alloc] initWithFrame:container.bounds configuration:config];
|
|
191
|
+
wv.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
192
|
+
// DevTools off by default — bunite_view_open_devtools toggles via setInspectable.
|
|
193
|
+
if (@available(macOS 13.3, *)) wv.inspectable = NO;
|
|
194
|
+
|
|
195
|
+
static __strong BuniteNavigationDelegate* navDelegate = [[BuniteNavigationDelegate alloc] init];
|
|
196
|
+
static __strong BuniteUIDelegate* uiDelegate = [[BuniteUIDelegate alloc] init];
|
|
197
|
+
wv.navigationDelegate = navDelegate;
|
|
198
|
+
wv.UIDelegate = uiDelegate;
|
|
199
|
+
|
|
200
|
+
[container addSubview:wv];
|
|
201
|
+
[window_state->window.contentView addSubview:container];
|
|
202
|
+
if (auto_resize) container.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
203
|
+
|
|
204
|
+
{
|
|
205
|
+
std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
|
|
206
|
+
auto& st = g_runtime.views[view_id];
|
|
207
|
+
st.container = container;
|
|
208
|
+
st.webview = wv;
|
|
209
|
+
st.nav_delegate = navDelegate;
|
|
210
|
+
st.ui_delegate = uiDelegate;
|
|
211
|
+
st.window_id = window_id;
|
|
212
|
+
st.appres_root = appres_root.length > 0 ? appres_root.UTF8String : "";
|
|
213
|
+
st.preload_script = preload.length > 0 ? preload.UTF8String : "";
|
|
214
|
+
st.navigation_rules = parseNavigationRulesJson(navigation_rules_json);
|
|
215
|
+
}
|
|
216
|
+
[webviewIdTable() setObject:@(view_id) forKey:wv];
|
|
217
|
+
|
|
218
|
+
if (url.length > 0) {
|
|
219
|
+
NSURL* u = [NSURL URLWithString:url];
|
|
220
|
+
if (u) [wv loadRequest:[NSURLRequest requestWithURL:u]];
|
|
221
|
+
} else if (html.length > 0) {
|
|
222
|
+
[wv loadHTMLString:html baseURL:nil];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
emitWebviewEvent(view_id, "view-ready", "");
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
void removeView(uint32_t view_id) {
|
|
230
|
+
__strong WKWebView* wv = nil;
|
|
231
|
+
__strong NSView* container = nil;
|
|
232
|
+
{
|
|
233
|
+
std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
|
|
234
|
+
auto it = g_runtime.views.find(view_id);
|
|
235
|
+
if (it == g_runtime.views.end()) return;
|
|
236
|
+
wv = it->second.webview;
|
|
237
|
+
container = it->second.container;
|
|
238
|
+
g_runtime.views.erase(it);
|
|
239
|
+
}
|
|
240
|
+
bunite::WebviewContentStorage::instance().remove(view_id);
|
|
241
|
+
|
|
242
|
+
// WebKit may not call stopURLSchemeTask during teardown — fail in-flight tasks here.
|
|
243
|
+
if (g_runtime.pending_route_tasks.count > 0) {
|
|
244
|
+
NSMutableArray<NSNumber*>* keys = [NSMutableArray array];
|
|
245
|
+
NSMutableArray<id<WKURLSchemeTask>>* victims = [NSMutableArray array];
|
|
246
|
+
for (NSNumber* key in g_runtime.pending_route_tasks) {
|
|
247
|
+
BunitePendingRoute* p = g_runtime.pending_route_tasks[key];
|
|
248
|
+
if (p.viewId == view_id) { [keys addObject:key]; [victims addObject:p.task]; }
|
|
249
|
+
}
|
|
250
|
+
[g_runtime.pending_route_tasks removeObjectsForKeys:keys];
|
|
251
|
+
NSError* err = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:nil];
|
|
252
|
+
@try {
|
|
253
|
+
for (id<WKURLSchemeTask> t in victims) [t didFailWithError:err];
|
|
254
|
+
} @catch (NSException* e) {
|
|
255
|
+
// Race: WebKit may stop the task between snapshot and failure — swallow.
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Deny pending permissions — releases the handler block, unblocks JS waiters.
|
|
260
|
+
NSArray<BunitePendingPermission*>* to_deny = nil;
|
|
261
|
+
if (g_runtime.pending_permissions.count > 0) {
|
|
262
|
+
NSMutableArray<NSNumber*>* keys = [NSMutableArray array];
|
|
263
|
+
NSMutableArray<BunitePendingPermission*>* victims = [NSMutableArray array];
|
|
264
|
+
for (NSNumber* key in g_runtime.pending_permissions) {
|
|
265
|
+
BunitePendingPermission* p = g_runtime.pending_permissions[key];
|
|
266
|
+
if (p.viewId == view_id) { [keys addObject:key]; [victims addObject:p]; }
|
|
267
|
+
}
|
|
268
|
+
[g_runtime.pending_permissions removeObjectsForKeys:keys];
|
|
269
|
+
to_deny = victims;
|
|
270
|
+
}
|
|
271
|
+
if (@available(macOS 12.0, *)) {
|
|
272
|
+
for (BunitePendingPermission* p in to_deny) p.handler(WKPermissionDecisionDeny);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (wv) [webviewIdTable() removeObjectForKey:wv];
|
|
276
|
+
if (container) [container removeFromSuperview];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
} // namespace bunite_mac
|