stupidpythonbrowser 1.0.2__py3-none-any.whl → 1.0.4__py3-none-any.whl
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.
- pybrowser/__init__.py +1 -1
- pybrowser/browser.py +310 -94
- pybrowser/doh_server.py +44 -19
- pybrowser/incognito.py +1 -1
- pybrowser/settings.py +49 -21
- pybrowser/task_manager.py +9 -5
- pybrowser/terminal.py +8 -6
- {stupidpythonbrowser-1.0.2.dist-info → stupidpythonbrowser-1.0.4.dist-info}/METADATA +1 -1
- stupidpythonbrowser-1.0.4.dist-info/RECORD +14 -0
- stupidpythonbrowser-1.0.2.dist-info/RECORD +0 -14
- {stupidpythonbrowser-1.0.2.dist-info → stupidpythonbrowser-1.0.4.dist-info}/WHEEL +0 -0
- {stupidpythonbrowser-1.0.2.dist-info → stupidpythonbrowser-1.0.4.dist-info}/entry_points.txt +0 -0
- {stupidpythonbrowser-1.0.2.dist-info → stupidpythonbrowser-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {stupidpythonbrowser-1.0.2.dist-info → stupidpythonbrowser-1.0.4.dist-info}/top_level.txt +0 -0
pybrowser/__init__.py
CHANGED
pybrowser/browser.py
CHANGED
|
@@ -42,7 +42,8 @@ from pybrowser.doh_server import (
|
|
|
42
42
|
DNS_CACHE, DNS_LOCK, STEVENBLACK_DOMAINS, BASE_ADBLOCK_DOMAINS, ADBLOCK_LOCK,
|
|
43
43
|
WK_RULE_LIST, is_domain_blocked, search_blocked_domains, load_dns_cache,
|
|
44
44
|
start_proxy_server, create_nw_proxy_config, load_adblock_filters_from_disk,
|
|
45
|
-
compile_webkit_adblock_rules, update_adblock_filters_from_web
|
|
45
|
+
compile_webkit_adblock_rules, update_adblock_filters_from_web,
|
|
46
|
+
clear_dns_cache_and_disk
|
|
46
47
|
)
|
|
47
48
|
from pybrowser.incognito import create_incognito_data_store, apply_incognito_preferences
|
|
48
49
|
from pybrowser.terminal import TERMINAL_MANAGER, dispatch_to_main
|
|
@@ -113,9 +114,10 @@ def download_image_data(img_url, dest_path, webview=None, completion_callback=No
|
|
|
113
114
|
if img_url.startswith("blob:"):
|
|
114
115
|
if webview:
|
|
115
116
|
dest_json = json.dumps(dest_path)
|
|
117
|
+
img_url_json = json.dumps(img_url)
|
|
116
118
|
js = f"""(async () => {{
|
|
117
119
|
try {{
|
|
118
|
-
const res = await fetch(
|
|
120
|
+
const res = await fetch({img_url_json});
|
|
119
121
|
const blob = await res.blob();
|
|
120
122
|
const reader = new FileReader();
|
|
121
123
|
reader.onloadend = () => {{
|
|
@@ -131,10 +133,18 @@ def download_image_data(img_url, dest_path, webview=None, completion_callback=No
|
|
|
131
133
|
if completion_callback: completion_callback(False)
|
|
132
134
|
return False
|
|
133
135
|
|
|
136
|
+
# Bug 8 Fix: Safely resolve referer on the main thread before starting background thread
|
|
137
|
+
referer = img_url
|
|
138
|
+
if webview and Cocoa.NSThread.isMainThread():
|
|
139
|
+
try:
|
|
140
|
+
if webview.URL():
|
|
141
|
+
referer = webview.URL().absoluteString()
|
|
142
|
+
except Exception:
|
|
143
|
+
referer = img_url
|
|
144
|
+
|
|
134
145
|
def worker():
|
|
135
146
|
success = False
|
|
136
147
|
try:
|
|
137
|
-
referer = webview.URL().absoluteString() if (webview and webview.URL()) else img_url
|
|
138
148
|
req = urllib.request.Request(img_url, headers={
|
|
139
149
|
"User-Agent": SETTINGS.get_user_agent(),
|
|
140
150
|
"Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
|
|
@@ -147,9 +157,10 @@ def download_image_data(img_url, dest_path, webview=None, completion_callback=No
|
|
|
147
157
|
except Exception:
|
|
148
158
|
if webview:
|
|
149
159
|
dest_json = json.dumps(dest_path)
|
|
160
|
+
img_url_json = json.dumps(img_url)
|
|
150
161
|
js = f"""(async () => {{
|
|
151
162
|
try {{
|
|
152
|
-
const res = await fetch(
|
|
163
|
+
const res = await fetch({img_url_json}, {{ credentials: 'include' }});
|
|
153
164
|
const blob = await res.blob();
|
|
154
165
|
const reader = new FileReader();
|
|
155
166
|
reader.onloadend = () => {{
|
|
@@ -336,7 +347,9 @@ def reload_all_extensions_views():
|
|
|
336
347
|
def init_history_db():
|
|
337
348
|
try:
|
|
338
349
|
with sqlite3.connect(HISTORY_FILE) as conn:
|
|
339
|
-
|
|
350
|
+
# Bug 5 Fix: Separate into individual statements so synchronous pragma executes
|
|
351
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
|
352
|
+
conn.execute("PRAGMA synchronous=NORMAL;")
|
|
340
353
|
conn.execute("""
|
|
341
354
|
CREATE TABLE IF NOT EXISTS history (
|
|
342
355
|
id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, title TEXT,
|
|
@@ -455,15 +468,24 @@ COMMAND_CLICK_JS = """
|
|
|
455
468
|
|
|
456
469
|
AUDIO_MONITOR_JS = """
|
|
457
470
|
document.addEventListener('play', (e) => {
|
|
458
|
-
if (e.target && !e.target.muted
|
|
471
|
+
if (e.target && !e.target.muted && window.webkit?.messageHandlers?.audio) {
|
|
472
|
+
window.webkit.messageHandlers.audio.postMessage({cmd:'audio_play'});
|
|
473
|
+
}
|
|
474
|
+
}, true);
|
|
475
|
+
document.addEventListener('pause', () => {
|
|
476
|
+
if (window.webkit?.messageHandlers?.audio) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'});
|
|
477
|
+
}, true);
|
|
478
|
+
document.addEventListener('ended', () => {
|
|
479
|
+
if (window.webkit?.messageHandlers?.audio) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'});
|
|
459
480
|
}, true);
|
|
460
|
-
document.addEventListener('pause', () => window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'}), true);
|
|
461
|
-
document.addEventListener('ended', () => window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'}), true);
|
|
462
481
|
document.addEventListener('volumechange', (e) => {
|
|
463
|
-
if (e.target && e.target.muted
|
|
482
|
+
if (e.target && e.target.muted && window.webkit?.messageHandlers?.audio) {
|
|
483
|
+
window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'});
|
|
484
|
+
}
|
|
464
485
|
}, true);
|
|
465
486
|
"""
|
|
466
487
|
|
|
488
|
+
# Bug 9 Fix: Always report target image (or null) to clear stale context menu states
|
|
467
489
|
IMAGE_CONTEXT_MENU_JS = """
|
|
468
490
|
(function() {
|
|
469
491
|
document.addEventListener('contextmenu', function(e) {
|
|
@@ -478,30 +500,33 @@ IMAGE_CONTEXT_MENU_JS = """
|
|
|
478
500
|
let bg = window.getComputedStyle(el).backgroundImage;
|
|
479
501
|
if (bg && bg.startsWith('url(')) imgSrc = bg.slice(4, -1).replace(/["']/g, "");
|
|
480
502
|
}
|
|
481
|
-
if (
|
|
503
|
+
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {
|
|
482
504
|
window.webkit.messageHandlers.bridge.postMessage({cmd: 'context_target_image', src: imgSrc});
|
|
483
505
|
}
|
|
484
506
|
}, true);
|
|
485
507
|
})();
|
|
486
508
|
"""
|
|
487
509
|
|
|
510
|
+
# Bug 18 Fix: Wrap keychain autosave in IIFE to protect scope
|
|
488
511
|
KEYCHAIN_AUTOSAVE_JS = """
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
window.webkit
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
window.webkit
|
|
503
|
-
|
|
504
|
-
}
|
|
512
|
+
(function() {
|
|
513
|
+
document.addEventListener('submit', (e) => {
|
|
514
|
+
if (!e.target || !e.target.querySelector) return;
|
|
515
|
+
let pw = e.target.querySelector('input[type="password"]');
|
|
516
|
+
let user = e.target.querySelector('input[type="text"], input[type="email"]');
|
|
517
|
+
if (pw && pw.value && window.webkit?.messageHandlers?.bridge) {
|
|
518
|
+
window.webkit.messageHandlers.bridge.postMessage({
|
|
519
|
+
cmd: 'prompt_save_password', host: location.hostname,
|
|
520
|
+
username: user ? user.value : 'unknown', password: pw.value
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}, true);
|
|
524
|
+
window.addEventListener('load', () => {
|
|
525
|
+
if (location.hostname && window.webkit?.messageHandlers?.bridge) {
|
|
526
|
+
window.webkit.messageHandlers.bridge.postMessage({cmd: 'check_autofill', host: location.hostname});
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
})();
|
|
505
530
|
"""
|
|
506
531
|
|
|
507
532
|
COSMETIC_ADBLOCK_CSS = """
|
|
@@ -630,14 +655,16 @@ class TabGroupHeaderView(Cocoa.NSView):
|
|
|
630
655
|
hex_to_nscolor(GROUP_COLORS.get(self.group.color_name, "#5F6368")).setFill()
|
|
631
656
|
path.fill()
|
|
632
657
|
attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.boldSystemFontOfSize_(11), Cocoa.NSForegroundColorAttributeName: Cocoa.NSColor.whiteColor()}
|
|
633
|
-
|
|
658
|
+
# Bug 17 Fix: Guard title against None
|
|
659
|
+
title_str = self.group.title or "Group"
|
|
660
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_(title_str, attrs).drawAtPoint_(Cocoa.NSMakePoint(6, 6))
|
|
634
661
|
|
|
635
662
|
def mouseDown_(self, event):
|
|
636
663
|
self.group.collapsed = not self.group.collapsed
|
|
637
664
|
self.controller.relayout_tabs()
|
|
638
665
|
|
|
639
666
|
# ============================================================================
|
|
640
|
-
# TITLE BAR VIEW
|
|
667
|
+
# TITLE BAR VIEW
|
|
641
668
|
# ============================================================================
|
|
642
669
|
class TitleBarView(Cocoa.NSView):
|
|
643
670
|
"""
|
|
@@ -711,6 +738,9 @@ class ChromeTabView(Cocoa.NSView):
|
|
|
711
738
|
self.tracking_area = None
|
|
712
739
|
self.close_btn_rect = Cocoa.NSZeroRect
|
|
713
740
|
self.audio_btn_rect = Cocoa.NSZeroRect
|
|
741
|
+
# Bug 2 Fix: Track drag state for interactive tab reordering
|
|
742
|
+
self.drag_start = None
|
|
743
|
+
self.is_dragging = False
|
|
714
744
|
return self
|
|
715
745
|
|
|
716
746
|
def updateTrackingAreas(self):
|
|
@@ -790,10 +820,10 @@ class ChromeTabView(Cocoa.NSView):
|
|
|
790
820
|
}
|
|
791
821
|
Cocoa.NSAttributedString.alloc().initWithString_attributes_("✕", close_attrs).drawAtPoint_(Cocoa.NSMakePoint(self.close_btn_rect.origin.x + 2, self.close_btn_rect.origin.y - 1))
|
|
792
822
|
|
|
793
|
-
# Tab title
|
|
823
|
+
# Tab title (Bug 17 Fix: Ensure title is never None)
|
|
794
824
|
title_x = icon_x + 20.0
|
|
795
825
|
max_title_w = max(10.0, bounds.size.width - title_x - right_offset)
|
|
796
|
-
title = tab.get("title"
|
|
826
|
+
title = tab.get("title") or "New Tab"
|
|
797
827
|
text_color = Cocoa.NSColor.labelColor() if is_active else Cocoa.NSColor.secondaryLabelColor()
|
|
798
828
|
title_attrs = {
|
|
799
829
|
Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(12),
|
|
@@ -811,6 +841,42 @@ class ChromeTabView(Cocoa.NSView):
|
|
|
811
841
|
self.controller.toggle_mute(self.index)
|
|
812
842
|
return
|
|
813
843
|
self.controller.switch_to_tab(self.index)
|
|
844
|
+
# Bug 2 Fix: Initialize drag start location
|
|
845
|
+
self.drag_start = event.locationInWindow()
|
|
846
|
+
self.is_dragging = False
|
|
847
|
+
|
|
848
|
+
def mouseDragged_(self, event):
|
|
849
|
+
# Bug 2 Fix: Drag reordering logic
|
|
850
|
+
if not self.drag_start or not self.controller:
|
|
851
|
+
return
|
|
852
|
+
cur_pt = event.locationInWindow()
|
|
853
|
+
dx = cur_pt.x - self.drag_start.x
|
|
854
|
+
dy = cur_pt.y - self.drag_start.y
|
|
855
|
+
if not self.is_dragging and (abs(dx) > 5 or abs(dy) > 5):
|
|
856
|
+
self.is_dragging = True
|
|
857
|
+
|
|
858
|
+
if self.is_dragging:
|
|
859
|
+
strip = self.controller.tab_strip
|
|
860
|
+
if not strip:
|
|
861
|
+
return
|
|
862
|
+
strip_pt = strip.convertPoint_fromView_(cur_pt, None)
|
|
863
|
+
for sub in strip.subviews():
|
|
864
|
+
if isinstance(sub, ChromeTabView) and sub is not self:
|
|
865
|
+
if Cocoa.NSPointInRect(strip_pt, sub.frame()):
|
|
866
|
+
target_idx = sub.index
|
|
867
|
+
if target_idx != self.index:
|
|
868
|
+
from_idx = self.index
|
|
869
|
+
self.controller.move_tab(from_idx, target_idx)
|
|
870
|
+
self.drag_start = cur_pt
|
|
871
|
+
break
|
|
872
|
+
|
|
873
|
+
def mouseUp_(self, event):
|
|
874
|
+
# Bug 2 Fix: Finalize drag and persist ordering
|
|
875
|
+
if self.is_dragging:
|
|
876
|
+
self.is_dragging = False
|
|
877
|
+
if self.controller:
|
|
878
|
+
self.controller.save_session()
|
|
879
|
+
self.drag_start = None
|
|
814
880
|
|
|
815
881
|
def menuForEvent_(self, event):
|
|
816
882
|
menu = Cocoa.NSMenu.alloc().init()
|
|
@@ -880,7 +946,8 @@ class BookmarksBarView(Cocoa.NSView):
|
|
|
880
946
|
for b in bms:
|
|
881
947
|
if not isinstance(b, dict) or not b.get("url"):
|
|
882
948
|
continue
|
|
883
|
-
|
|
949
|
+
# Bug 16 Fix: Safely handle None bookmark titles
|
|
950
|
+
title = (b.get("title") or b.get("url") or "")[:24]
|
|
884
951
|
btn_w = max(50, min(160, len(title) * 7.5 + 26))
|
|
885
952
|
btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(x, 2, btn_w, h - 4))
|
|
886
953
|
btn.setTitle_(title)
|
|
@@ -1104,26 +1171,34 @@ window.writeToTerm = b => {{
|
|
|
1104
1171
|
for b in bms:
|
|
1105
1172
|
if isinstance(b, dict) and b.get("url"):
|
|
1106
1173
|
u = b["url"]
|
|
1107
|
-
|
|
1174
|
+
# Bug 16 Fix: Guard title against None
|
|
1175
|
+
t = b.get("title") or u or ""
|
|
1108
1176
|
safe_u = json.dumps(u)
|
|
1177
|
+
esc_u = html.escape(u)
|
|
1178
|
+
esc_t = html.escape(t)
|
|
1109
1179
|
host = urllib.parse.urlparse(u).netloc
|
|
1110
|
-
fav_tag = f"<img src='https://www.google.com/s2/favicons?domain={urllib.parse.quote(host)}&sz=32' width='16' height='16' style='vertical-align:middle;margin-right:8px;border-radius:3px;' onerror
|
|
1111
|
-
del_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_bookmark', url: {safe_u}}}); setTimeout(() => location.reload(), 100);"
|
|
1112
|
-
items.append(f"<li style='display:flex;align-items:center;padding:6px 0;'><span style='flex:1;display:flex;align-items:center;'>{fav_tag}<a href='{
|
|
1180
|
+
fav_tag = f"<img src='https://www.google.com/s2/favicons?domain={urllib.parse.quote(host)}&sz=32' width='16' height='16' style='vertical-align:middle;margin-right:8px;border-radius:3px;' onerror=\"this.style.display='none'\">" if host else ""
|
|
1181
|
+
del_call = f"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_bookmark', url: {safe_u}}}); setTimeout(() => location.reload(), 100);"
|
|
1182
|
+
items.append(f"<li style='display:flex;align-items:center;padding:6px 0;'><span style='flex:1;display:flex;align-items:center;'>{fav_tag}<a href='{esc_u}' style='color:#1a73e8;text-decoration:none;'>{esc_t}</a></span> <button onclick='{del_call}' style='margin-left:12px;padding:3px 8px;'>Delete</button></li>")
|
|
1113
1183
|
rows = "".join(items)
|
|
1114
|
-
return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Bookmarks</h2><div style='margin-bottom:15px;'><input id='t' placeholder='Title' style='padding:4px 8px;margin-right:8px;'><input id='u' placeholder='https://...' style='padding:4px 8px;margin-right:8px;width:260px;'><button onclick='window.webkit.messageHandlers.bridge.postMessage({{cmd:\"add_bookmark\",title:document.getElementById(\"t\").value,url:document.getElementById(\"u\").value}});setTimeout(()=>location.reload(),100)' style='padding:4px 12px;'>Add</button></div><ul style='list-style:none;padding:0;'>{rows or '<li>No bookmarks</li>'}</ul></body></html>"
|
|
1184
|
+
return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Bookmarks</h2><div style='margin-bottom:15px;'><input id='t' placeholder='Title' style='padding:4px 8px;margin-right:8px;'><input id='u' placeholder='https://...' style='padding:4px 8px;margin-right:8px;width:260px;'><button onclick='if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd:\"add_bookmark\",title:document.getElementById(\"t\").value,url:document.getElementById(\"u\").value}});setTimeout(()=>location.reload(),100)' style='padding:4px 12px;'>Add</button></div><ul style='list-style:none;padding:0;'>{rows or '<li>No bookmarks</li>'}</ul></body></html>"
|
|
1115
1185
|
|
|
1116
1186
|
def get_history_html(self):
|
|
1117
1187
|
records = query_history()
|
|
1118
1188
|
items = []
|
|
1119
1189
|
for r in records:
|
|
1120
|
-
del_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_history_item', id: {r[0]}}}); this.closest('tr').remove();"
|
|
1121
|
-
|
|
1190
|
+
del_call = f"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_history_item', id: {r[0]}}}); this.closest('tr').remove();"
|
|
1191
|
+
# Bug 16 Fix: Guard title against None and avoid backslashes inside f-string expressions
|
|
1192
|
+
title_text = r[2] or r[1] or ""
|
|
1193
|
+
url_str = r[1] or ""
|
|
1194
|
+
esc_url = html.escape(url_str)
|
|
1195
|
+
esc_title = html.escape(title_text)
|
|
1196
|
+
items.append(f"<tr><td>{r[3]}</td><td><a href='{esc_url}'>{esc_title}</a></td><td><button onclick='{del_call}'>✕</button></td></tr>")
|
|
1122
1197
|
rows = "".join(items)
|
|
1123
|
-
return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>History</h2><button onclick=\"window.webkit.messageHandlers.bridge.postMessage({{cmd:'clear_browsing_data',targets:['history']}});setTimeout(()=>location.reload(),100)\">Clear All</button><table style='width:100%;margin-top:15px;'>{rows or '<tr><td>No history</td></tr>'}</table></body></html>"
|
|
1198
|
+
return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>History</h2><button onclick=\"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd:'clear_browsing_data',targets:['history']}});setTimeout(()=>location.reload(),100)\">Clear All</button><table style='width:100%;margin-top:15px;'>{rows or '<tr><td>No history</td></tr>'}</table></body></html>"
|
|
1124
1199
|
|
|
1125
1200
|
def get_downloads_html(self):
|
|
1126
|
-
return "<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Downloads</h2><div id='dl-list'>No active downloads.</div><script>const dls={};window.updateDownload=(id,name,path,status,progress)=>{dls[id]={name,path,status,progress};render()};function render(){const el=document.getElementById('dl-list');el.innerHTML=Object.values(dls).map(d=>`<div style='border:1px solid #ccc;padding:10px;margin-bottom:8px;'><b>${d.name}</b> (${d.status}) <button onclick=\"window.webkit.messageHandlers.bridge.postMessage({cmd:'dl_show',path:
|
|
1201
|
+
return "<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Downloads</h2><div id='dl-list'>No active downloads.</div><script>const dls={};window.updateDownload=(id,name,path,status,progress)=>{dls[id]={name,path,status,progress};render()};function render(){const el=document.getElementById('dl-list');el.innerHTML=Object.values(dls).map(d=>`<div style='border:1px solid #ccc;padding:10px;margin-bottom:8px;'><b>${d.name}</b> (${d.status}) <button onclick=\"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({cmd:'dl_show',path:d.path})\">Show in Finder</button></div>`).join('')};if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({cmd:'dl_request_sync'});</script></body></html>"
|
|
1127
1202
|
|
|
1128
1203
|
def get_extensions_html(self):
|
|
1129
1204
|
scripts = get_installed_userscripts()
|
|
@@ -1132,8 +1207,8 @@ window.writeToTerm = b => {{
|
|
|
1132
1207
|
f = s["file"]
|
|
1133
1208
|
safe_f = json.dumps(f)
|
|
1134
1209
|
checked = "checked" if s["enabled"] else ""
|
|
1135
|
-
toggle_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'toggle_script', file: {safe_f}, enabled: this.checked}})"
|
|
1136
|
-
del_call = f"if(confirm('Delete?')){{window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_script', file: {safe_f}}}); setTimeout(() => location.reload(), 100);}}"
|
|
1210
|
+
toggle_call = f"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd: 'toggle_script', file: {safe_f}, enabled: this.checked}})"
|
|
1211
|
+
del_call = f"if(confirm('Delete?')){{if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_script', file: {safe_f}}}); setTimeout(() => location.reload(), 100);}}"
|
|
1137
1212
|
items.append(
|
|
1138
1213
|
f"<div style='border:1px solid #ccc;padding:10px;margin-bottom:8px;'>"
|
|
1139
1214
|
f"<b>{html.escape(s['name'])}</b> ({html.escape(f)}) "
|
|
@@ -1142,7 +1217,7 @@ window.writeToTerm = b => {{
|
|
|
1142
1217
|
f"</div>"
|
|
1143
1218
|
)
|
|
1144
1219
|
rows = "".join(items)
|
|
1145
|
-
return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Extensions & Userscripts</h2><button onclick='let id=prompt(\"CRX ID or URL:\");if(id)window.webkit.messageHandlers.bridge.postMessage({{cmd:\"install_crx\",id:id}})'>Install CRX</button><div style='margin-top:15px;'>{rows or 'No userscripts installed.'}</div></body></html>"
|
|
1220
|
+
return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Extensions & Userscripts</h2><button onclick='let id=prompt(\"CRX ID or URL:\");if(id && window.webkit?.messageHandlers?.bridge)window.webkit.messageHandlers.bridge.postMessage({{cmd:\"install_crx\",id:id}})'>Install CRX</button><div style='margin-top:15px;'>{rows or 'No userscripts installed.'}</div></body></html>"
|
|
1146
1221
|
|
|
1147
1222
|
# ============================================================================
|
|
1148
1223
|
# WEBKIT SUBCLASS & IMAGE CONTEXT MENU
|
|
@@ -1234,22 +1309,22 @@ class ChromeOmniboxField(Cocoa.NSTextField):
|
|
|
1234
1309
|
def becomeFirstResponder(self):
|
|
1235
1310
|
if self.superview():
|
|
1236
1311
|
self.superview().setNeedsDisplay_(True)
|
|
1237
|
-
|
|
1312
|
+
# Bug 1 Fix: Ensure string value is never None and eliminate infinite loop selectText
|
|
1313
|
+
self.setStringValue_(self.raw_url or "")
|
|
1238
1314
|
self.setTextColor_(Cocoa.NSColor.labelColor())
|
|
1239
|
-
self.performSelector_withObject_afterDelay_("selectText:", self, 0.0)
|
|
1240
1315
|
return objc.super(ChromeOmniboxField, self).becomeFirstResponder()
|
|
1241
1316
|
|
|
1242
1317
|
def resignFirstResponder(self):
|
|
1243
1318
|
if self.superview():
|
|
1244
1319
|
self.superview().setNeedsDisplay_(True)
|
|
1245
|
-
|
|
1320
|
+
# Bug 1 Fix: Do not wipe input text on resign so enter and navigation work properly
|
|
1246
1321
|
return objc.super(ChromeOmniboxField, self).resignFirstResponder()
|
|
1247
1322
|
|
|
1248
1323
|
def set_url(self, url):
|
|
1249
|
-
self.raw_url = url
|
|
1324
|
+
self.raw_url = url or ""
|
|
1250
1325
|
fr = self.window().firstResponder() if self.window() else None
|
|
1251
1326
|
if fr != self and fr != self.currentEditor():
|
|
1252
|
-
self.setStringValue_(
|
|
1327
|
+
self.setStringValue_(self.raw_url)
|
|
1253
1328
|
|
|
1254
1329
|
class OmniboxPopupPanel(Cocoa.NSPanel):
|
|
1255
1330
|
def canBecomeKeyWindow(self): return False
|
|
@@ -1270,6 +1345,7 @@ class OmniboxSuggestionView(Cocoa.NSView):
|
|
|
1270
1345
|
opts = Cocoa.NSTrackingMouseMoved | Cocoa.NSTrackingActiveAlways | Cocoa.NSTrackingInVisibleRect
|
|
1271
1346
|
self.tracking_area = Cocoa.NSTrackingArea.alloc().initWithRect_options_owner_userInfo_(Cocoa.NSZeroRect, opts, self, None)
|
|
1272
1347
|
self.addTrackingArea_(self.tracking_area)
|
|
1348
|
+
# Bug 1 Fix: Use correct class in objc.super to avoid fatal TypeError
|
|
1273
1349
|
objc.super(OmniboxSuggestionView, self).updateTrackingAreas()
|
|
1274
1350
|
|
|
1275
1351
|
def mouseMoved_(self, event):
|
|
@@ -1298,7 +1374,9 @@ class OmniboxSuggestionView(Cocoa.NSView):
|
|
|
1298
1374
|
|
|
1299
1375
|
icon = "🔍" if sugg["type"] == "search" else ("🕒" if sugg["type"] == "history" else ("🔖" if sugg["type"] == "bookmark" else ("🧮" if sugg["type"] == "math" else "⚡")))
|
|
1300
1376
|
attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(13), Cocoa.NSForegroundColorAttributeName: text_color}
|
|
1301
|
-
|
|
1377
|
+
# Bug 1 Fix: Ensure suggestion text is never None
|
|
1378
|
+
sugg_text = sugg.get("text") or ""
|
|
1379
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_(f"{icon} {sugg_text}", attrs).drawAtPoint_(Cocoa.NSMakePoint(12, rect.origin.y + 6))
|
|
1302
1380
|
|
|
1303
1381
|
class OmniboxPanelCoordinator:
|
|
1304
1382
|
def __init__(self, window_controller):
|
|
@@ -1333,14 +1411,15 @@ class OmniboxPanelCoordinator:
|
|
|
1333
1411
|
bms = json.load(f)
|
|
1334
1412
|
for b in bms:
|
|
1335
1413
|
if isinstance(b, dict):
|
|
1336
|
-
t, u = b.get("title"
|
|
1414
|
+
t, u = b.get("title") or "", b.get("url") or ""
|
|
1337
1415
|
if query_str.lower() in t.lower() or query_str.lower() in u.lower():
|
|
1338
1416
|
results.append({"type": "bookmark", "text": t or u, "url": u})
|
|
1339
1417
|
except Exception:
|
|
1340
1418
|
pass
|
|
1341
1419
|
|
|
1420
|
+
# Bug 1 Fix: Safely fallback title for history autocomplete
|
|
1342
1421
|
for h in query_history(query_str)[:4]:
|
|
1343
|
-
results.append({"type": "history", "text": h[2], "url": h[1]})
|
|
1422
|
+
results.append({"type": "history", "text": h[2] or h[1] or "", "url": h[1]})
|
|
1344
1423
|
|
|
1345
1424
|
def fetch_remote():
|
|
1346
1425
|
try:
|
|
@@ -1411,7 +1490,7 @@ class OmniboxPanelCoordinator:
|
|
|
1411
1490
|
pb.setString_forType_(val, Cocoa.NSPasteboardTypeString)
|
|
1412
1491
|
self.hide()
|
|
1413
1492
|
return
|
|
1414
|
-
target = sugg.get("url") or sugg.get("text")
|
|
1493
|
+
target = sugg.get("url") or sugg.get("text") or ""
|
|
1415
1494
|
self.wc.omnibox.setStringValue_(target)
|
|
1416
1495
|
editor = self.wc.omnibox.currentEditor()
|
|
1417
1496
|
if editor:
|
|
@@ -1426,16 +1505,33 @@ class OmniboxPanelCoordinator:
|
|
|
1426
1505
|
self.suggestion_view.selected_index = min(len(self.suggestion_view.suggestions)-1, self.suggestion_view.selected_index + 1)
|
|
1427
1506
|
if self.suggestion_view.selected_index >= 0:
|
|
1428
1507
|
s = self.suggestion_view.suggestions[self.suggestion_view.selected_index]
|
|
1429
|
-
|
|
1508
|
+
val = s.get("url") or s.get("text") or ""
|
|
1509
|
+
self.wc.omnibox.setStringValue_(val)
|
|
1510
|
+
# Bug 10 Fix: Update active field editor
|
|
1511
|
+
editor = self.wc.omnibox.currentEditor()
|
|
1512
|
+
if editor:
|
|
1513
|
+
editor.setString_(val)
|
|
1514
|
+
editor.selectAll_(None)
|
|
1430
1515
|
self.suggestion_view.setNeedsDisplay_(True)
|
|
1431
1516
|
return True
|
|
1432
1517
|
elif sel == "moveUp:":
|
|
1433
1518
|
self.suggestion_view.selected_index = max(-1, self.suggestion_view.selected_index - 1)
|
|
1434
1519
|
if self.suggestion_view.selected_index >= 0:
|
|
1435
1520
|
s = self.suggestion_view.suggestions[self.suggestion_view.selected_index]
|
|
1436
|
-
|
|
1521
|
+
val = s.get("url") or s.get("text") or ""
|
|
1522
|
+
self.wc.omnibox.setStringValue_(val)
|
|
1523
|
+
# Bug 10 Fix: Update active field editor
|
|
1524
|
+
editor = self.wc.omnibox.currentEditor()
|
|
1525
|
+
if editor:
|
|
1526
|
+
editor.setString_(val)
|
|
1527
|
+
editor.selectAll_(None)
|
|
1437
1528
|
else:
|
|
1438
|
-
self.
|
|
1529
|
+
val = self.typed_query or ""
|
|
1530
|
+
self.wc.omnibox.setStringValue_(val)
|
|
1531
|
+
editor = self.wc.omnibox.currentEditor()
|
|
1532
|
+
if editor:
|
|
1533
|
+
editor.setString_(val)
|
|
1534
|
+
editor.selectAll_(None)
|
|
1439
1535
|
self.suggestion_view.setNeedsDisplay_(True)
|
|
1440
1536
|
return True
|
|
1441
1537
|
elif sel == "insertNewline:":
|
|
@@ -1514,6 +1610,7 @@ class FindBarView(Cocoa.NSView):
|
|
|
1514
1610
|
|
|
1515
1611
|
def onClose_(self, sender):
|
|
1516
1612
|
self.setHidden_(True)
|
|
1613
|
+
# Bug 22 Fix: Refocus webview when closing find bar
|
|
1517
1614
|
wv = self.controller.get_active_webview()
|
|
1518
1615
|
if wv:
|
|
1519
1616
|
self.controller.window().makeFirstResponder_(wv)
|
|
@@ -1521,6 +1618,11 @@ class FindBarView(Cocoa.NSView):
|
|
|
1521
1618
|
# ============================================================================
|
|
1522
1619
|
# QUICK SWITCHER & MEDIA CONTROLS (PIP)
|
|
1523
1620
|
# ============================================================================
|
|
1621
|
+
# Bug 15 Fix: Dedicated panel subclass allowing keyboard focus on borderless window
|
|
1622
|
+
class QuickSwitcherPanel(Cocoa.NSPanel):
|
|
1623
|
+
def canBecomeKeyWindow(self): return True
|
|
1624
|
+
def canBecomeMainWindow(self): return True
|
|
1625
|
+
|
|
1524
1626
|
class QuickSwitcherListView(Cocoa.NSView):
|
|
1525
1627
|
def initWithFrame_controller_(self, frame, controller):
|
|
1526
1628
|
self = objc.super(QuickSwitcherListView, self).initWithFrame_(frame)
|
|
@@ -1530,6 +1632,19 @@ class QuickSwitcherListView(Cocoa.NSView):
|
|
|
1530
1632
|
|
|
1531
1633
|
def isFlipped(self): return True
|
|
1532
1634
|
|
|
1635
|
+
def mouseDown_(self, event):
|
|
1636
|
+
loc = self.convertPoint_fromView_(event.locationInWindow(), None)
|
|
1637
|
+
idx = int(loc.y // 36)
|
|
1638
|
+
if 0 <= idx < len(self.controller.filtered_tabs):
|
|
1639
|
+
self.controller.selected_index = idx
|
|
1640
|
+
self.setNeedsDisplay_(True)
|
|
1641
|
+
t = self.controller.filtered_tabs[idx]
|
|
1642
|
+
cur_idx = t["wc"].get_tab_index_by_id(t["id"])
|
|
1643
|
+
if cur_idx is not None:
|
|
1644
|
+
t["wc"].window().makeKeyAndOrderFront_(None)
|
|
1645
|
+
t["wc"].switch_to_tab(cur_idx)
|
|
1646
|
+
self.controller.window().orderOut_(None)
|
|
1647
|
+
|
|
1533
1648
|
def drawRect_(self, dirtyRect):
|
|
1534
1649
|
for i, tab in enumerate(self.controller.filtered_tabs):
|
|
1535
1650
|
rect = Cocoa.NSMakeRect(0, i * 36, self.bounds().size.width, 36)
|
|
@@ -1540,15 +1655,19 @@ class QuickSwitcherListView(Cocoa.NSView):
|
|
|
1540
1655
|
else:
|
|
1541
1656
|
tc, sub_color = Cocoa.NSColor.labelColor(), Cocoa.NSColor.secondaryLabelColor()
|
|
1542
1657
|
|
|
1658
|
+
# Bug 16 Fix: Guard title and url against None
|
|
1659
|
+
title_text = (tab.get("title") or "Tab")[:60]
|
|
1660
|
+
url_text = (tab.get("url") or "")[:80]
|
|
1543
1661
|
attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.boldSystemFontOfSize_(13), Cocoa.NSForegroundColorAttributeName: tc}
|
|
1544
|
-
Cocoa.NSAttributedString.alloc().initWithString_attributes_(
|
|
1662
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_(title_text, attrs).drawAtPoint_(Cocoa.NSMakePoint(20, rect.origin.y + 3))
|
|
1545
1663
|
sub_attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(11), Cocoa.NSForegroundColorAttributeName: sub_color}
|
|
1546
|
-
Cocoa.NSAttributedString.alloc().initWithString_attributes_(
|
|
1664
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_(url_text, sub_attrs).drawAtPoint_(Cocoa.NSMakePoint(20, rect.origin.y + 19))
|
|
1547
1665
|
|
|
1548
1666
|
class QuickSwitcherController(Cocoa.NSWindowController):
|
|
1549
1667
|
def init(self):
|
|
1550
1668
|
w, h = 600, 400
|
|
1551
|
-
window
|
|
1669
|
+
# Bug 15 Fix: Use QuickSwitcherPanel so window can receive keyboard events
|
|
1670
|
+
window = QuickSwitcherPanel.alloc().initWithContentRect_styleMask_backing_defer_(
|
|
1552
1671
|
Cocoa.NSMakeRect(0, 0, w, h), Cocoa.NSWindowStyleMaskBorderless | Cocoa.NSWindowStyleMaskFullSizeContentView, Cocoa.NSBackingStoreBuffered, False
|
|
1553
1672
|
)
|
|
1554
1673
|
window.setOpaque_(False)
|
|
@@ -1591,8 +1710,8 @@ class QuickSwitcherController(Cocoa.NSWindowController):
|
|
|
1591
1710
|
for t in wc.tabs:
|
|
1592
1711
|
self.tabs.append({
|
|
1593
1712
|
"id": t.get("id"),
|
|
1594
|
-
"title": t.get("title"
|
|
1595
|
-
"url": t.get("url"
|
|
1713
|
+
"title": t.get("title") or "Tab",
|
|
1714
|
+
"url": t.get("url") or "",
|
|
1596
1715
|
"wc": wc
|
|
1597
1716
|
})
|
|
1598
1717
|
self.filtered_tabs = self.tabs.copy()
|
|
@@ -1612,7 +1731,8 @@ class QuickSwitcherController(Cocoa.NSWindowController):
|
|
|
1612
1731
|
def control_textView_doCommandBySelector_(self, control, textView, commandSelector):
|
|
1613
1732
|
sel = str(commandSelector)
|
|
1614
1733
|
if sel == "moveDown:":
|
|
1615
|
-
|
|
1734
|
+
if self.filtered_tabs:
|
|
1735
|
+
self.selected_index = min(len(self.filtered_tabs) - 1, self.selected_index + 1)
|
|
1616
1736
|
self.list_view.setNeedsDisplay_(True)
|
|
1617
1737
|
return True
|
|
1618
1738
|
elif sel == "moveUp:":
|
|
@@ -1644,6 +1764,7 @@ class PiPPanelController(Cocoa.NSWindowController):
|
|
|
1644
1764
|
window.setFloatingPanel_(True)
|
|
1645
1765
|
window.setLevel_(Cocoa.NSFloatingWindowLevel)
|
|
1646
1766
|
window.setReleasedWhenClosed_(False)
|
|
1767
|
+
window.center()
|
|
1647
1768
|
self = objc.super(PiPPanelController, self).initWithWindow_(window)
|
|
1648
1769
|
if self:
|
|
1649
1770
|
self.browser = browser_controller
|
|
@@ -1693,6 +1814,7 @@ class PiPPanelController(Cocoa.NSWindowController):
|
|
|
1693
1814
|
# BROWSER WINDOW CONTROLLER
|
|
1694
1815
|
# ============================================================================
|
|
1695
1816
|
TITLE_BAR_H = 30
|
|
1817
|
+
SCRIPT_HANDLER_NAMES = ("bridge", "audio", "terminalInput", "terminalResize")
|
|
1696
1818
|
|
|
1697
1819
|
class BrowserWindowController(Cocoa.NSWindowController):
|
|
1698
1820
|
def init(self):
|
|
@@ -1939,6 +2061,16 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
1939
2061
|
wv.setFrame_(self.web_container.bounds())
|
|
1940
2062
|
self.relayout_tabs()
|
|
1941
2063
|
|
|
2064
|
+
def move_tab(self, from_idx, to_idx):
|
|
2065
|
+
"""Bug 2 Fix: Interactive tab reordering."""
|
|
2066
|
+
if from_idx == to_idx or not (0 <= from_idx < len(self.tabs) and 0 <= to_idx < len(self.tabs)):
|
|
2067
|
+
return
|
|
2068
|
+
tab = self.tabs.pop(from_idx)
|
|
2069
|
+
self.tabs.insert(to_idx, tab)
|
|
2070
|
+
self.active_tab_index = to_idx
|
|
2071
|
+
self.relayout_tabs()
|
|
2072
|
+
self.save_session()
|
|
2073
|
+
|
|
1942
2074
|
def relayout_tabs(self):
|
|
1943
2075
|
for sub in list(self.tab_strip.subviews()):
|
|
1944
2076
|
sub.removeFromSuperview()
|
|
@@ -1948,7 +2080,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
1948
2080
|
tab_h = 28 if not is_vert else 32
|
|
1949
2081
|
y = self.tab_strip.bounds().size.height - 38 - tab_h if is_vert else 3
|
|
1950
2082
|
|
|
1951
|
-
|
|
2083
|
+
# Bug 3 Fix: Safely guard against orphan group IDs
|
|
2084
|
+
vis = [t for t in self.tabs if not t.get("group_id") or not (self.get_group(t["group_id"]) and self.get_group(t["group_id"]).collapsed)]
|
|
1952
2085
|
pinned_cnt = sum(1 for t in vis if t.get("pinned", False))
|
|
1953
2086
|
tab_w = max(60, min(240, (w - x - 44 - (pinned_cnt * 36)) / max(1, len(vis) - pinned_cnt))) if not is_vert else w
|
|
1954
2087
|
|
|
@@ -2025,10 +2158,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2025
2158
|
|
|
2026
2159
|
config.setURLSchemeHandler_forURLScheme_(InternalSchemeHandler.alloc().init(), "browser")
|
|
2027
2160
|
uc = WebKit.WKUserContentController.alloc().init()
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
uc.addScriptMessageHandler_name_(self, "terminalInput")
|
|
2031
|
-
uc.addScriptMessageHandler_name_(self, "terminalResize")
|
|
2161
|
+
for h_name in SCRIPT_HANDLER_NAMES:
|
|
2162
|
+
uc.addScriptMessageHandler_name_(self, h_name)
|
|
2032
2163
|
|
|
2033
2164
|
# Injected scripts: Command+click capture, audio monitoring, context menus, and fullscreen handlers
|
|
2034
2165
|
uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(COMMAND_CLICK_JS, WebKit.WKUserScriptInjectionTimeAtDocumentStart, False))
|
|
@@ -2057,7 +2188,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2057
2188
|
reg = m.replace("*", ".*").replace("/", escaped_slash)
|
|
2058
2189
|
conds.append(f"new RegExp('^{reg}$').test(location.href)")
|
|
2059
2190
|
cond_str = " || ".join(conds) if conds else "true"
|
|
2060
|
-
|
|
2191
|
+
# Bug 26 Fix: Wrap in an IIFE so top-level return statements in scripts execute safely
|
|
2192
|
+
wrapped = f"if ({cond_str}) {{\n(function() {{\n" + script["code"] + "\n}})();\n}}"
|
|
2061
2193
|
uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(wrapped, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
|
|
2062
2194
|
|
|
2063
2195
|
if WK_RULE_LIST and SETTINGS.settings.get("adblock_enabled", True):
|
|
@@ -2150,6 +2282,13 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2150
2282
|
wv.removeObserver_forKeyPath_(self, k)
|
|
2151
2283
|
except Exception:
|
|
2152
2284
|
pass
|
|
2285
|
+
# Bug 20 Fix: Remove message handlers to eliminate retain cycles
|
|
2286
|
+
uc = wv.configuration().userContentController()
|
|
2287
|
+
for name in SCRIPT_HANDLER_NAMES:
|
|
2288
|
+
try:
|
|
2289
|
+
uc.removeScriptMessageHandlerForName_(name)
|
|
2290
|
+
except Exception:
|
|
2291
|
+
pass
|
|
2153
2292
|
if wv.superview():
|
|
2154
2293
|
wv.removeFromSuperview()
|
|
2155
2294
|
if not self.tabs:
|
|
@@ -2375,6 +2514,9 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2375
2514
|
def handle_escape_key(self):
|
|
2376
2515
|
if self.find_bar and not self.find_bar.isHidden():
|
|
2377
2516
|
self.find_bar.setHidden_(True)
|
|
2517
|
+
wv = self.get_active_webview()
|
|
2518
|
+
if wv:
|
|
2519
|
+
self.window().makeFirstResponder_(wv)
|
|
2378
2520
|
return
|
|
2379
2521
|
if self.is_active_tab_terminal():
|
|
2380
2522
|
return
|
|
@@ -2442,7 +2584,11 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2442
2584
|
inc_item = menu.addItemWithTitle_action_keyEquivalent_("New Incognito Window", "onNewIncognitoWindow:", "N")
|
|
2443
2585
|
inc_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagShift)
|
|
2444
2586
|
inc_item.setTarget_(Cocoa.NSApp.delegate())
|
|
2445
|
-
|
|
2587
|
+
|
|
2588
|
+
term_item = menu.addItemWithTitle_action_keyEquivalent_("New Terminal Tab", "openTerminal:", "t")
|
|
2589
|
+
term_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagOption)
|
|
2590
|
+
term_item.setTarget_(self)
|
|
2591
|
+
|
|
2446
2592
|
menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
2447
2593
|
menu.addItemWithTitle_action_keyEquivalent_("Bookmark Current Page", "onBookmarkCurrentPage:", "d").setTarget_(self)
|
|
2448
2594
|
bm_text = "Hide Bookmarks Bar" if SETTINGS.settings.get("bookmarks_bar_visible", True) else "Show Bookmarks Bar"
|
|
@@ -2536,9 +2682,12 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2536
2682
|
if not wv: return
|
|
2537
2683
|
url = wv.URL().absoluteString() if wv.URL() else "browser://newtab"
|
|
2538
2684
|
parsed = Foundation.NSURL.URLWithString_(url)
|
|
2685
|
+
# Bug 24 Fix: Safely guard against None parsed URL
|
|
2686
|
+
host_str = (parsed.host() or "Internal Page") if parsed else "Internal Page"
|
|
2687
|
+
is_https = (parsed.scheme() == "https") if parsed else False
|
|
2539
2688
|
menu = Cocoa.NSMenu.alloc().init()
|
|
2540
|
-
menu.addItemWithTitle_action_keyEquivalent_(f"Host: {
|
|
2541
|
-
menu.addItemWithTitle_action_keyEquivalent_("Connection: Secure (HTTPS)" if
|
|
2689
|
+
menu.addItemWithTitle_action_keyEquivalent_(f"Host: {host_str}", "", "").setEnabled_(False)
|
|
2690
|
+
menu.addItemWithTitle_action_keyEquivalent_("Connection: Secure (HTTPS)" if is_https else "Connection: Insecure / Local", "", "").setEnabled_(False)
|
|
2542
2691
|
menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
2543
2692
|
menu.addItemWithTitle_action_keyEquivalent_("Copy Current URL", "onCopyUrl:", "").setTarget_(self)
|
|
2544
2693
|
menu.popUpMenuPositioningItem_atLocation_inView_(None, Cocoa.NSMakePoint(0, 0), sender)
|
|
@@ -2583,7 +2732,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2583
2732
|
wv = self.get_active_webview()
|
|
2584
2733
|
if wv:
|
|
2585
2734
|
wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_(val)))
|
|
2586
|
-
|
|
2735
|
+
# Bug 11 Fix: Transfer keyboard focus directly to active webview
|
|
2736
|
+
self.window().makeFirstResponder_(wv)
|
|
2587
2737
|
|
|
2588
2738
|
def controlTextDidChange_(self, notif):
|
|
2589
2739
|
if notif.object() == self.omnibox:
|
|
@@ -2685,10 +2835,14 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2685
2835
|
return
|
|
2686
2836
|
elif name == "terminalResize":
|
|
2687
2837
|
b = message.body()
|
|
2688
|
-
TERMINAL_MANAGER.get_or_create(
|
|
2838
|
+
TERMINAL_MANAGER.get_or_create(
|
|
2839
|
+
message.webView(),
|
|
2840
|
+
b.get("cols") if hasattr(b, "get") else None,
|
|
2841
|
+
b.get("rows") if hasattr(b, "get") else None
|
|
2842
|
+
)
|
|
2689
2843
|
return
|
|
2690
2844
|
elif name == "audio":
|
|
2691
|
-
b = message.body() if
|
|
2845
|
+
b = message.body() if hasattr(message.body(), "get") else {}
|
|
2692
2846
|
has_a = (b.get("cmd") == "audio_play")
|
|
2693
2847
|
for t in self.tabs:
|
|
2694
2848
|
if t.get("webview") == message.webView():
|
|
@@ -2698,7 +2852,13 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2698
2852
|
return
|
|
2699
2853
|
|
|
2700
2854
|
body = message.body()
|
|
2701
|
-
cmd =
|
|
2855
|
+
cmd = None
|
|
2856
|
+
if hasattr(body, "get"):
|
|
2857
|
+
cmd = body.get("cmd")
|
|
2858
|
+
elif isinstance(body, dict):
|
|
2859
|
+
cmd = body.get("cmd")
|
|
2860
|
+
if cmd is not None:
|
|
2861
|
+
cmd = str(cmd)
|
|
2702
2862
|
|
|
2703
2863
|
if cmd == "open_url":
|
|
2704
2864
|
target_url = body.get("url")
|
|
@@ -2707,6 +2867,7 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2707
2867
|
elif cmd == "toggle_fullscreen_terminal":
|
|
2708
2868
|
self.toggle_tab_bar()
|
|
2709
2869
|
elif cmd == "context_target_image":
|
|
2870
|
+
# Bug 9 Fix: Store or clear target image URL
|
|
2710
2871
|
self.last_context_image_url = body.get("src")
|
|
2711
2872
|
elif cmd == "blob_image_data":
|
|
2712
2873
|
data_url, dest_path = body.get("data"), body.get("path")
|
|
@@ -2740,17 +2901,29 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2740
2901
|
elif cmd == "prompt_save_password":
|
|
2741
2902
|
self.show_password_banner(body.get("host", ""), body.get("username", ""), body.get("password", ""))
|
|
2742
2903
|
elif cmd == "check_autofill":
|
|
2904
|
+
# Bug 18 & 19 Fix: Apply credentials specifically to requesting webview using safe JSON encoding
|
|
2743
2905
|
host = body.get("host")
|
|
2744
2906
|
saved_user = SETTINGS.settings.get("saved_logins", {}).get(host)
|
|
2745
2907
|
if saved_user:
|
|
2746
2908
|
pwd = get_from_keychain(host, saved_user)
|
|
2747
2909
|
if pwd:
|
|
2748
|
-
|
|
2749
|
-
if
|
|
2750
|
-
|
|
2910
|
+
calling_wv = message.webView()
|
|
2911
|
+
if calling_wv:
|
|
2912
|
+
pwd_json = json.dumps(pwd)
|
|
2913
|
+
user_json = json.dumps(saved_user)
|
|
2914
|
+
js = f"""(function() {{
|
|
2915
|
+
let pw = document.querySelector('input[type=password]');
|
|
2916
|
+
let user = document.querySelector('input[type=text], input[type=email]');
|
|
2917
|
+
if (pw) pw.value = {pwd_json};
|
|
2918
|
+
if (user) user.value = {user_json};
|
|
2919
|
+
}})();"""
|
|
2920
|
+
calling_wv.evaluateJavaScript_completionHandler_(js, None)
|
|
2751
2921
|
elif cmd == "save_setting":
|
|
2752
|
-
|
|
2753
|
-
|
|
2922
|
+
raw_key, raw_val = body.get("key"), body.get("val")
|
|
2923
|
+
if raw_key is None:
|
|
2924
|
+
return
|
|
2925
|
+
key = str(raw_key)
|
|
2926
|
+
val = sanitize_for_json(raw_val)
|
|
2754
2927
|
if key == "download_path":
|
|
2755
2928
|
val = os.path.expanduser(str(val).strip())
|
|
2756
2929
|
SETTINGS.settings[key] = val
|
|
@@ -2768,12 +2941,15 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2768
2941
|
for sub in c.tab_strip.subviews():
|
|
2769
2942
|
sub.setNeedsDisplay_(True)
|
|
2770
2943
|
elif key in ("default_zoom",):
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
t
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2944
|
+
try:
|
|
2945
|
+
new_zoom = float(val)
|
|
2946
|
+
for t in self.tabs:
|
|
2947
|
+
t["zoom"] = new_zoom
|
|
2948
|
+
twv = t.get("webview")
|
|
2949
|
+
if twv:
|
|
2950
|
+
self.apply_zoom_to_webview(twv, t)
|
|
2951
|
+
except (ValueError, TypeError):
|
|
2952
|
+
pass
|
|
2777
2953
|
elif key in ("user_agent_mode", "custom_user_agent"):
|
|
2778
2954
|
ua = SETTINGS.get_user_agent()
|
|
2779
2955
|
for t in self.tabs:
|
|
@@ -2809,7 +2985,7 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2809
2985
|
|
|
2810
2986
|
res = panel.runModal()
|
|
2811
2987
|
if res == Cocoa.NSModalResponseOK and panel.URLs():
|
|
2812
|
-
chosen_path = panel.URLs()[0].path()
|
|
2988
|
+
chosen_path = str(panel.URLs()[0].path())
|
|
2813
2989
|
if os.access(chosen_path, os.W_OK):
|
|
2814
2990
|
SETTINGS.settings["download_path"] = chosen_path
|
|
2815
2991
|
SETTINGS.save()
|
|
@@ -2839,15 +3015,16 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2839
3015
|
dispatch_to_main(on_done)
|
|
2840
3016
|
threading.Thread(target=fetch_task, daemon=True).start()
|
|
2841
3017
|
elif cmd == "clear_browsing_data":
|
|
2842
|
-
|
|
3018
|
+
raw_targets = body.get("targets", [])
|
|
3019
|
+
targets = list(raw_targets) if hasattr(raw_targets, "__iter__") else []
|
|
2843
3020
|
if "history" in targets:
|
|
2844
3021
|
clear_history()
|
|
2845
3022
|
if "favicons" in targets:
|
|
2846
3023
|
FaviconCache.clear()
|
|
2847
3024
|
self.bookmarks_bar.reload_bookmarks()
|
|
2848
3025
|
if "dns_cache" in targets:
|
|
2849
|
-
|
|
2850
|
-
|
|
3026
|
+
# Bug 12 Fix: Completely wipe in-memory and disk DNS cache
|
|
3027
|
+
clear_dns_cache_and_disk()
|
|
2851
3028
|
if "cookies_cache" in targets:
|
|
2852
3029
|
ds = WebKit.WKWebsiteDataStore.defaultDataStore()
|
|
2853
3030
|
ds.removeDataOfTypes_modifiedSince_completionHandler_(WebKit.WKWebsiteDataStore.allWebsiteDataTypes(), Foundation.NSDate.dateWithTimeIntervalSince1970_(0), lambda: None)
|
|
@@ -3025,6 +3202,9 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
3025
3202
|
if not t.get("is_sleeping"):
|
|
3026
3203
|
t["title"] = str(obj.title())
|
|
3027
3204
|
break
|
|
3205
|
+
# Bug 6 Fix: Update SQLite history with real page title when available
|
|
3206
|
+
if not self.is_incognito and obj.URL() and SETTINGS.settings.get("history_enabled", True):
|
|
3207
|
+
record_history(obj.URL().absoluteString(), str(obj.title()))
|
|
3028
3208
|
self.relayout_tabs()
|
|
3029
3209
|
elif keyPath == "estimatedProgress":
|
|
3030
3210
|
self.update_progress_for_webview(obj)
|
|
@@ -3067,6 +3247,14 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
3067
3247
|
self.omnibox_panel.hide()
|
|
3068
3248
|
|
|
3069
3249
|
def windowWillClose_(self, notif):
|
|
3250
|
+
# Bug 4 Fix: Invalidate repeating NSTimer to prevent memory retain cycle
|
|
3251
|
+
if hasattr(self, 'sleep_timer') and self.sleep_timer:
|
|
3252
|
+
self.sleep_timer.invalidate()
|
|
3253
|
+
self.sleep_timer = None
|
|
3254
|
+
|
|
3255
|
+
if hasattr(self, 'omnibox_panel') and self.omnibox_panel.is_visible():
|
|
3256
|
+
self.omnibox_panel.hide()
|
|
3257
|
+
|
|
3070
3258
|
self.save_session()
|
|
3071
3259
|
for t in self.tabs:
|
|
3072
3260
|
wv = t.get("webview")
|
|
@@ -3077,6 +3265,13 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
3077
3265
|
wv.removeObserver_forKeyPath_(self, k)
|
|
3078
3266
|
except Exception:
|
|
3079
3267
|
pass
|
|
3268
|
+
# Bug 20 Fix: Clean up userContentController message handlers
|
|
3269
|
+
uc = wv.configuration().userContentController()
|
|
3270
|
+
for name in SCRIPT_HANDLER_NAMES:
|
|
3271
|
+
try:
|
|
3272
|
+
uc.removeScriptMessageHandlerForName_(name)
|
|
3273
|
+
except Exception:
|
|
3274
|
+
pass
|
|
3080
3275
|
app_delegate = Cocoa.NSApp.delegate()
|
|
3081
3276
|
if app_delegate and hasattr(app_delegate, "controllers") and self in app_delegate.controllers:
|
|
3082
3277
|
app_delegate.controllers.remove(self)
|
|
@@ -3149,12 +3344,14 @@ class AppDelegate(Cocoa.NSObject):
|
|
|
3149
3344
|
# Allow Escape to pass directly into terminal tabs (vim, nano, fzf, less)
|
|
3150
3345
|
if wc.is_active_tab_terminal():
|
|
3151
3346
|
return event
|
|
3347
|
+
# Bug 22 Fix: Refocus webview when find_bar is closed via Escape
|
|
3152
3348
|
if wc.find_bar and not wc.find_bar.isHidden():
|
|
3153
3349
|
wc.find_bar.setHidden_(True)
|
|
3350
|
+
wv = wc.get_active_webview()
|
|
3351
|
+
if wv:
|
|
3352
|
+
wc.window().makeFirstResponder_(wv)
|
|
3154
3353
|
return None
|
|
3155
|
-
|
|
3156
|
-
wc.toggle_tab_bar()
|
|
3157
|
-
return None
|
|
3354
|
+
# Bug 21 Fix: Delegate Escape to handle_escape_key() so HTML5 fullscreen exit is not bypassed
|
|
3158
3355
|
wc.handle_escape_key()
|
|
3159
3356
|
return event
|
|
3160
3357
|
|
|
@@ -3188,11 +3385,24 @@ class AppDelegate(Cocoa.NSObject):
|
|
|
3188
3385
|
wc = BrowserWindowController.alloc().initWithIncognito_(True)
|
|
3189
3386
|
self.controllers.append(wc)
|
|
3190
3387
|
|
|
3388
|
+
# Bug 7 Fix: Allow opening new tabs when all windows are closed
|
|
3389
|
+
def onNewTab_(self, sender=None):
|
|
3390
|
+
win = Cocoa.NSApp.keyWindow()
|
|
3391
|
+
wc = win.delegate() if win and isinstance(win.delegate(), BrowserWindowController) else None
|
|
3392
|
+
if wc:
|
|
3393
|
+
wc.onNewTab_(sender)
|
|
3394
|
+
else:
|
|
3395
|
+
self.onNewWindow_(sender)
|
|
3396
|
+
|
|
3191
3397
|
def onNewTerminal_(self, sender):
|
|
3192
3398
|
win = Cocoa.NSApp.keyWindow()
|
|
3193
3399
|
wc = win.delegate() if win and isinstance(win.delegate(), BrowserWindowController) else (self.controllers[0] if self.controllers else None)
|
|
3194
3400
|
if wc:
|
|
3195
3401
|
wc.add_tab("browser://terminal")
|
|
3402
|
+
else:
|
|
3403
|
+
self.onNewWindow_(None)
|
|
3404
|
+
if self.controllers:
|
|
3405
|
+
self.controllers[-1].add_tab("browser://terminal")
|
|
3196
3406
|
|
|
3197
3407
|
def onCloseTab_(self, sender=None):
|
|
3198
3408
|
win = Cocoa.NSApp.keyWindow()
|
|
@@ -3223,7 +3433,12 @@ class AppDelegate(Cocoa.NSObject):
|
|
|
3223
3433
|
file_item = Cocoa.NSMenuItem.alloc().init()
|
|
3224
3434
|
file_menu = Cocoa.NSMenu.alloc().initWithTitle_("File")
|
|
3225
3435
|
file_menu.addItemWithTitle_action_keyEquivalent_("New Tab", "onNewTab:", "t")
|
|
3226
|
-
|
|
3436
|
+
|
|
3437
|
+
# Bug 27 Fix: Assign distinct shortcut Cmd+Option+T to Terminal tab so Cmd+Shift+T belongs to Reopen Closed Tab
|
|
3438
|
+
term_item = file_menu.addItemWithTitle_action_keyEquivalent_("New Terminal Tab", "onNewTerminal:", "t")
|
|
3439
|
+
term_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagOption)
|
|
3440
|
+
term_item.setTarget_(self)
|
|
3441
|
+
|
|
3227
3442
|
file_menu.addItemWithTitle_action_keyEquivalent_("New Window", "onNewWindow:", "n").setTarget_(self)
|
|
3228
3443
|
inc_item = file_menu.addItemWithTitle_action_keyEquivalent_("New Incognito Window", "onNewIncognitoWindow:", "N")
|
|
3229
3444
|
inc_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagShift)
|
|
@@ -3282,7 +3497,8 @@ class AppDelegate(Cocoa.NSObject):
|
|
|
3282
3497
|
for i in range(1, 10):
|
|
3283
3498
|
win_menu.addItemWithTitle_action_keyEquivalent_(f"Tab {i}", f"onSwitchTab{i}:", str(i))
|
|
3284
3499
|
win_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
3285
|
-
win_menu.addItemWithTitle_action_keyEquivalent_("Quick Switcher", "quickSwitcher:", "k")
|
|
3500
|
+
qs_item = win_menu.addItemWithTitle_action_keyEquivalent_("Quick Switcher", "quickSwitcher:", "k")
|
|
3501
|
+
qs_item.setTarget_(self)
|
|
3286
3502
|
win_menu.addItemWithTitle_action_keyEquivalent_("Task Manager", "showTaskManager:", "").setTarget_(self)
|
|
3287
3503
|
win_item.setSubmenu_(win_menu)
|
|
3288
3504
|
main_menu.addItem_(win_item)
|
pybrowser/doh_server.py
CHANGED
|
@@ -36,6 +36,9 @@ DNS_CACHE = {}
|
|
|
36
36
|
DNS_LOCK = threading.Lock()
|
|
37
37
|
DNS_CACHE_DIRTY = False
|
|
38
38
|
|
|
39
|
+
_PROXY_STARTED = False
|
|
40
|
+
_PROXY_START_LOCK = threading.Lock()
|
|
41
|
+
|
|
39
42
|
def rebuild_active_domains():
|
|
40
43
|
global ACTIVE_BLOCK_DOMAINS
|
|
41
44
|
with ADBLOCK_LOCK:
|
|
@@ -92,9 +95,12 @@ def load_dns_cache():
|
|
|
92
95
|
if os.path.exists(DNS_CACHE_FILE):
|
|
93
96
|
try:
|
|
94
97
|
with open(DNS_CACHE_FILE, 'r', encoding='utf-8') as f:
|
|
95
|
-
|
|
98
|
+
loaded = json.load(f)
|
|
99
|
+
with DNS_LOCK:
|
|
100
|
+
DNS_CACHE = loaded if isinstance(loaded, dict) else {}
|
|
96
101
|
except Exception:
|
|
97
|
-
|
|
102
|
+
with DNS_LOCK:
|
|
103
|
+
DNS_CACHE = {}
|
|
98
104
|
|
|
99
105
|
def flush_dns_cache_to_disk():
|
|
100
106
|
global DNS_CACHE_DIRTY
|
|
@@ -107,6 +113,18 @@ def flush_dns_cache_to_disk():
|
|
|
107
113
|
except Exception:
|
|
108
114
|
pass
|
|
109
115
|
|
|
116
|
+
def clear_dns_cache_and_disk():
|
|
117
|
+
"""Bug 12 Fix: Clears memory cache and wipes disk persistence file completely."""
|
|
118
|
+
global DNS_CACHE, DNS_CACHE_DIRTY
|
|
119
|
+
with DNS_LOCK:
|
|
120
|
+
DNS_CACHE.clear()
|
|
121
|
+
DNS_CACHE_DIRTY = False
|
|
122
|
+
try:
|
|
123
|
+
if os.path.exists(DNS_CACHE_FILE):
|
|
124
|
+
os.remove(DNS_CACHE_FILE)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
110
128
|
def doh_resolve(hostname):
|
|
111
129
|
global DNS_CACHE_DIRTY
|
|
112
130
|
now = time.time()
|
|
@@ -282,24 +300,31 @@ class DoHProxyHandler(http.server.BaseHTTPRequestHandler):
|
|
|
282
300
|
pass
|
|
283
301
|
|
|
284
302
|
def start_proxy_server():
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if hasattr(socket, "SO_REUSEPORT"):
|
|
291
|
-
try:
|
|
292
|
-
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
293
|
-
except Exception:
|
|
294
|
-
pass
|
|
295
|
-
super().server_bind()
|
|
303
|
+
# Bug 23 Fix: Prevent multiple TCP server instances & duplicate worker threads
|
|
304
|
+
global _PROXY_STARTED
|
|
305
|
+
with _PROXY_START_LOCK:
|
|
306
|
+
if _PROXY_STARTED:
|
|
307
|
+
return
|
|
296
308
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
309
|
+
class ReusableTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
|
310
|
+
allow_reuse_address = True
|
|
311
|
+
daemon_threads = True
|
|
312
|
+
|
|
313
|
+
def server_bind(self):
|
|
314
|
+
if hasattr(socket, "SO_REUSEPORT"):
|
|
315
|
+
try:
|
|
316
|
+
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
317
|
+
except Exception:
|
|
318
|
+
pass
|
|
319
|
+
super().server_bind()
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
server = ReusableTCPServer(("127.0.0.1", LOCAL_PROXY_PORT), DoHProxyHandler)
|
|
323
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
324
|
+
threading.Thread(target=dns_flush_worker, daemon=True).start()
|
|
325
|
+
_PROXY_STARTED = True
|
|
326
|
+
except Exception as e:
|
|
327
|
+
print(f"[Proxy Warning] Could not start local proxy server: {e}")
|
|
303
328
|
|
|
304
329
|
def dns_flush_worker():
|
|
305
330
|
while True:
|
pybrowser/incognito.py
CHANGED
pybrowser/settings.py
CHANGED
|
@@ -11,6 +11,7 @@ import re
|
|
|
11
11
|
import html
|
|
12
12
|
import tempfile
|
|
13
13
|
import shutil
|
|
14
|
+
import copy
|
|
14
15
|
|
|
15
16
|
UA_PRESETS = {
|
|
16
17
|
"safari_18": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
|
|
@@ -23,7 +24,7 @@ DEFAULT_URL = "https://www.google.com"
|
|
|
23
24
|
DATA_DIR = os.path.expanduser("~/data")
|
|
24
25
|
os.makedirs(DATA_DIR, exist_ok=True)
|
|
25
26
|
|
|
26
|
-
_cur_dir = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
+
_cur_dir = os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals() else os.getcwd()
|
|
27
28
|
_old_candidates = [
|
|
28
29
|
os.path.abspath(os.path.join(_cur_dir, "..", "browser_data")),
|
|
29
30
|
os.path.abspath(os.path.join(os.getcwd(), "browser_data")),
|
|
@@ -87,29 +88,40 @@ def sanitize_for_json(obj):
|
|
|
87
88
|
"""
|
|
88
89
|
if obj is None:
|
|
89
90
|
return None
|
|
90
|
-
|
|
91
|
+
|
|
91
92
|
type_name = type(obj).__name__
|
|
92
|
-
|
|
93
|
+
|
|
94
|
+
# PyObjC booleans (__NSCFBoolean) must use boolValue() to accurately preserve False/True
|
|
95
|
+
if hasattr(obj, "boolValue") and ("Boolean" in type_name or "bool" in type_name.lower()):
|
|
96
|
+
return bool(obj.boolValue())
|
|
97
|
+
|
|
98
|
+
# Standard Python booleans
|
|
99
|
+
if isinstance(obj, bool) or type_name == "bool":
|
|
93
100
|
return bool(obj)
|
|
101
|
+
|
|
102
|
+
# Number conversion for PyObjC NSNumber (e.g. float/double or int)
|
|
103
|
+
if hasattr(obj, "doubleValue") and not isinstance(obj, (int, float, str, bytes)):
|
|
104
|
+
val = obj.doubleValue()
|
|
105
|
+
return int(val) if val.is_integer() else val
|
|
106
|
+
|
|
94
107
|
if isinstance(obj, (int, float)):
|
|
95
108
|
return obj
|
|
109
|
+
|
|
96
110
|
if isinstance(obj, (str, bytes)):
|
|
97
111
|
return str(obj)
|
|
112
|
+
|
|
98
113
|
if hasattr(obj, "items") or isinstance(obj, dict):
|
|
99
114
|
return {str(k): sanitize_for_json(v) for k, v in obj.items()}
|
|
115
|
+
|
|
100
116
|
if hasattr(obj, "__iter__") or isinstance(obj, (list, tuple, set)):
|
|
101
117
|
return [sanitize_for_json(i) for i in obj]
|
|
118
|
+
|
|
102
119
|
try:
|
|
103
|
-
# Number conversion for PyObjC NSNumber
|
|
104
|
-
if hasattr(obj, "boolValue") and ("Boolean" in type_name or "bool" in str(type(obj)).lower()):
|
|
105
|
-
return bool(obj.boolValue())
|
|
106
|
-
if hasattr(obj, "doubleValue"):
|
|
107
|
-
val = obj.doubleValue()
|
|
108
|
-
return int(val) if val.is_integer() else val
|
|
109
120
|
if hasattr(obj, "stringValue"):
|
|
110
121
|
return str(obj.stringValue())
|
|
111
122
|
except Exception:
|
|
112
123
|
pass
|
|
124
|
+
|
|
113
125
|
return str(obj)
|
|
114
126
|
|
|
115
127
|
def validate_and_get_download_dir(target_path=None):
|
|
@@ -177,7 +189,7 @@ class SettingsManager:
|
|
|
177
189
|
"saved_logins": {},
|
|
178
190
|
"history_enabled": True
|
|
179
191
|
}
|
|
180
|
-
self.settings =
|
|
192
|
+
self.settings = copy.deepcopy(self.defaults)
|
|
181
193
|
self.load()
|
|
182
194
|
|
|
183
195
|
def load(self):
|
|
@@ -209,7 +221,8 @@ class SettingsManager:
|
|
|
209
221
|
print(f"[Settings Save Error] {e}")
|
|
210
222
|
|
|
211
223
|
def reset_defaults(self):
|
|
212
|
-
|
|
224
|
+
# Bug 25 Fix: Deep copy to prevent mutation of defaults dictionary
|
|
225
|
+
self.settings = copy.deepcopy(self.defaults)
|
|
213
226
|
self.save()
|
|
214
227
|
|
|
215
228
|
def get_search_url(self, query):
|
|
@@ -635,7 +648,9 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
|
|
|
635
648
|
window._toastTimer = setTimeout(() => t.classList.remove('show'), 2400);
|
|
636
649
|
}}
|
|
637
650
|
function saveSetting(key, val, msg) {{
|
|
638
|
-
window.webkit.messageHandlers.bridge
|
|
651
|
+
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
|
|
652
|
+
window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'save_setting', key: key, val: val }});
|
|
653
|
+
}}
|
|
639
654
|
showToast(msg);
|
|
640
655
|
}}
|
|
641
656
|
function onDownloadInputChange(val) {{
|
|
@@ -662,12 +677,16 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
|
|
|
662
677
|
document.getElementById('custom-startup-row').style.display = (val === 'custom_url') ? 'flex' : 'none';
|
|
663
678
|
}}
|
|
664
679
|
function browseDownloadFolder() {{
|
|
665
|
-
window.webkit.messageHandlers.bridge
|
|
680
|
+
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
|
|
681
|
+
window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'choose_download_directory' }});
|
|
682
|
+
}}
|
|
666
683
|
}}
|
|
667
684
|
function updateAdblockFilters() {{
|
|
668
685
|
const b = document.getElementById('update-filters-btn');
|
|
669
686
|
b.innerText = "Syncing Lists..."; b.disabled = true;
|
|
670
|
-
window.webkit.messageHandlers.bridge
|
|
687
|
+
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
|
|
688
|
+
window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'update_adblock_filters' }});
|
|
689
|
+
}}
|
|
671
690
|
}}
|
|
672
691
|
function searchDomain() {{
|
|
673
692
|
const q = document.getElementById('inspector-query').value.trim();
|
|
@@ -675,11 +694,16 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
|
|
|
675
694
|
const box = document.getElementById('inspector-results');
|
|
676
695
|
box.style.display = 'block';
|
|
677
696
|
box.innerHTML = 'Searching blocklists...';
|
|
678
|
-
window.webkit.messageHandlers.bridge
|
|
697
|
+
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
|
|
698
|
+
window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'search_blocked_domains', query: q }});
|
|
699
|
+
}}
|
|
679
700
|
}}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
701
|
+
// Bug 14 Fix: Accept either pre-parsed Array or JSON string safely
|
|
702
|
+
window.renderBlockedResults = function(data) {{
|
|
703
|
+
let items = Array.isArray(data) ? data : [];
|
|
704
|
+
if (!items.length && typeof data === 'string') {{
|
|
705
|
+
try {{ items = JSON.parse(data); }} catch(e) {{ items = []; }}
|
|
706
|
+
}}
|
|
683
707
|
const box = document.getElementById('inspector-results');
|
|
684
708
|
if (!items || items.length === 0) {{
|
|
685
709
|
box.innerHTML = '<span style="color:#D93025;">No matches found. Domain is NOT currently blocked.</span>';
|
|
@@ -687,7 +711,7 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
|
|
|
687
711
|
}}
|
|
688
712
|
let out = '<div style="margin-bottom:6px;font-weight:600;color:var(--primary);">' + items.length + ' Matches Found:</div>';
|
|
689
713
|
items.forEach(it => {{
|
|
690
|
-
out += '<div><strong>[BLOCKED]</strong> ' + it.domain + ' <span style="opacity:0.6;">(' + it.source + ')</span></div>';
|
|
714
|
+
out += '<div><strong>[BLOCKED]</strong> ' + (it.domain || '') + ' <span style="opacity:0.6;">(' + (it.source || '') + ')</span></div>';
|
|
691
715
|
}});
|
|
692
716
|
box.innerHTML = out;
|
|
693
717
|
}};
|
|
@@ -698,12 +722,16 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
|
|
|
698
722
|
if (document.getElementById('clr-favicons').checked) targets.push('favicons');
|
|
699
723
|
if (document.getElementById('clr-dns').checked) targets.push('dns_cache');
|
|
700
724
|
if (targets.length === 0) {{ showToast('Select at least one category'); return; }}
|
|
701
|
-
window.webkit.messageHandlers.bridge
|
|
725
|
+
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
|
|
726
|
+
window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'clear_browsing_data', targets: targets }});
|
|
727
|
+
}}
|
|
702
728
|
showToast('Browsing data cleared');
|
|
703
729
|
}}
|
|
704
730
|
function executeResetDefaults() {{
|
|
705
731
|
if (confirm("Restore all browser settings to factory defaults?")) {{
|
|
706
|
-
window.webkit.messageHandlers.bridge
|
|
732
|
+
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
|
|
733
|
+
window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'reset_defaults' }});
|
|
734
|
+
}}
|
|
707
735
|
}}
|
|
708
736
|
}}
|
|
709
737
|
function jumpTo(id, navBtn) {{
|
pybrowser/task_manager.py
CHANGED
|
@@ -114,11 +114,15 @@ class ProcessManagerController(Cocoa.NSWindowController):
|
|
|
114
114
|
|
|
115
115
|
def onPurgeMemory_(self, sender):
|
|
116
116
|
try:
|
|
117
|
-
from pybrowser.doh_server import
|
|
118
|
-
|
|
119
|
-
DNS_CACHE.clear()
|
|
117
|
+
from pybrowser.doh_server import clear_dns_cache_and_disk
|
|
118
|
+
clear_dns_cache_and_disk()
|
|
120
119
|
except Exception:
|
|
121
|
-
|
|
120
|
+
try:
|
|
121
|
+
from pybrowser.doh_server import DNS_CACHE, DNS_LOCK
|
|
122
|
+
with DNS_LOCK:
|
|
123
|
+
DNS_CACHE.clear()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
122
126
|
|
|
123
127
|
try:
|
|
124
128
|
from pybrowser.browser import FaviconCache
|
|
@@ -363,4 +367,4 @@ class ProcessManagerController(Cocoa.NSWindowController):
|
|
|
363
367
|
session = target[1]
|
|
364
368
|
if session:
|
|
365
369
|
session.close()
|
|
366
|
-
self.refresh_data()
|
|
370
|
+
self.refresh_data()
|
pybrowser/terminal.py
CHANGED
|
@@ -222,11 +222,12 @@ class TerminalSession:
|
|
|
222
222
|
env["CARGO_HTTP_PROXY"] = proxy_url
|
|
223
223
|
|
|
224
224
|
# Inject PyBrowser CLI tools into PATH
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
225
|
+
# $$$$$
|
|
226
|
+
# bin_dir = os.path.join(get_data_dir(), "bin")
|
|
227
|
+
# ensure_doh_cli_helper(bin_dir, port)
|
|
228
|
+
# cur_path = env.get("PATH", "/usr/bin:/bin:/usr/sbin:/sbin")
|
|
229
|
+
# if bin_dir not in cur_path:
|
|
230
|
+
# env["PATH"] = f"{bin_dir}:{cur_path}"
|
|
230
231
|
|
|
231
232
|
# PyBrowser session indicators
|
|
232
233
|
env["PYBROWSER_DOH_PROXY"] = proxy_url
|
|
@@ -401,7 +402,8 @@ class TerminalSession:
|
|
|
401
402
|
class TerminalManager:
|
|
402
403
|
def __init__(self):
|
|
403
404
|
self.sessions = {}
|
|
404
|
-
|
|
405
|
+
# Bug 13 Fix: Use RLock to prevent reentrant deadlocks when session.close() calls on_close
|
|
406
|
+
self.lock = threading.RLock()
|
|
405
407
|
|
|
406
408
|
def get_or_create(self, webview, cols=None, rows=None):
|
|
407
409
|
with self.lock:
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
pybrowser/__init__.py,sha256=bgYpxNkjslEJjbQpWEwcro_Kc4dbmud1g31hd8J1TN0,218
|
|
2
|
+
pybrowser/browser.py,sha256=1zU9Joa-qcB7nrTYbPoBYJTidqT7fETHun8UzOX92Bc,170052
|
|
3
|
+
pybrowser/doh_server.py,sha256=MqmKNzYL3xqhVA1oY7B7DoiA6rEb9-Vxs343305HCog,16109
|
|
4
|
+
pybrowser/incognito.py,sha256=qg8b0Ie6-4sr9R7RFJ-bTn52f-rRQBJQcA77oF9Mmks,1676
|
|
5
|
+
pybrowser/main.py,sha256=DwtxNLZ0PrhH0RPUn3UzPFSHQgHcnO7AHriElxxOP2A,2332
|
|
6
|
+
pybrowser/settings.py,sha256=UrD88IMKt9goxFIalRQBFGMzM5TBO7dBSKd_S4Q_oTY,42357
|
|
7
|
+
pybrowser/task_manager.py,sha256=rNt_eCrGoi5OYgYDRg-jv-u326bqQLN0wSJnrGuLwPA,15252
|
|
8
|
+
pybrowser/terminal.py,sha256=8RChiArtp76OG6n3rbY8U81kU3V_Gybkl9hxq-nRd84,15285
|
|
9
|
+
stupidpythonbrowser-1.0.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
10
|
+
stupidpythonbrowser-1.0.4.dist-info/METADATA,sha256=GPXR_DEI9jFFi1ue6gwH30dExMlBFzjZQfiLqD92zZM,977
|
|
11
|
+
stupidpythonbrowser-1.0.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
stupidpythonbrowser-1.0.4.dist-info/entry_points.txt,sha256=ntsWkCj6QvxpckUUie3agZV8YJQdV5tQMzYfPK4NopM,46
|
|
13
|
+
stupidpythonbrowser-1.0.4.dist-info/top_level.txt,sha256=-Qio7UVGVcEO-lZkeLbyefDgD31D6lsx3CxPpKMipKE,10
|
|
14
|
+
stupidpythonbrowser-1.0.4.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
pybrowser/__init__.py,sha256=GVtXkxt_j9dwHYFRqGEWp4HzMX2VTp6HQAPyt8lqYAY,218
|
|
2
|
-
pybrowser/browser.py,sha256=MhhhLRxGZjUiVg-BAaGBwAZas6tK2f-QZI2WLxvt81s,159687
|
|
3
|
-
pybrowser/doh_server.py,sha256=uLlyu5wvn0cXSr2aDKa2EjOlXN4Mhzz81WM53jSA0s4,15253
|
|
4
|
-
pybrowser/incognito.py,sha256=GEc-xeVkElsDPzFIVLhTgSXtDHK3whTzxdnz7wYVJ0o,1675
|
|
5
|
-
pybrowser/main.py,sha256=DwtxNLZ0PrhH0RPUn3UzPFSHQgHcnO7AHriElxxOP2A,2332
|
|
6
|
-
pybrowser/settings.py,sha256=1b1iaZ-DJImRVzDtQH8QFYjiBBDZmPqMnrOr8sG3L5E,41304
|
|
7
|
-
pybrowser/task_manager.py,sha256=DRTazfw6MAqM360DkiMGIC0oZq6XuMBCpkMgHZAJ-II,15079
|
|
8
|
-
pybrowser/terminal.py,sha256=vdu64F3DRf_3KlU_BVzfllPUhb6dGRFjhVqoVr5Xetw,15159
|
|
9
|
-
stupidpythonbrowser-1.0.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
10
|
-
stupidpythonbrowser-1.0.2.dist-info/METADATA,sha256=xOLz3ZmY8CofHyqRAbgEFhxoDa54sDzBHp4wKrJWoJc,977
|
|
11
|
-
stupidpythonbrowser-1.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
-
stupidpythonbrowser-1.0.2.dist-info/entry_points.txt,sha256=ntsWkCj6QvxpckUUie3agZV8YJQdV5tQMzYfPK4NopM,46
|
|
13
|
-
stupidpythonbrowser-1.0.2.dist-info/top_level.txt,sha256=-Qio7UVGVcEO-lZkeLbyefDgD31D6lsx3CxPpKMipKE,10
|
|
14
|
-
stupidpythonbrowser-1.0.2.dist-info/RECORD,,
|
|
File without changes
|
{stupidpythonbrowser-1.0.2.dist-info → stupidpythonbrowser-1.0.4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{stupidpythonbrowser-1.0.2.dist-info → stupidpythonbrowser-1.0.4.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|