stupidpythonbrowser 1.0.4__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/PKG-INFO +1 -1
  2. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/pybrowser/browser.py +169 -64
  3. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/pybrowser/doh_server.py +61 -22
  4. stupidpythonbrowser-1.1.0/pybrowser/legacy.py +1024 -0
  5. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/pybrowser/settings.py +9 -3
  6. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/pybrowser/terminal.py +16 -11
  7. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/pyproject.toml +2 -1
  8. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/stupidpythonbrowser.egg-info/PKG-INFO +1 -1
  9. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/stupidpythonbrowser.egg-info/SOURCES.txt +1 -0
  10. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/stupidpythonbrowser.egg-info/entry_points.txt +1 -0
  11. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/LICENSE +0 -0
  12. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/README.md +0 -0
  13. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/pybrowser/__init__.py +0 -0
  14. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/pybrowser/incognito.py +0 -0
  15. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/pybrowser/main.py +0 -0
  16. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/pybrowser/task_manager.py +0 -0
  17. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/setup.cfg +0 -0
  18. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/stupidpythonbrowser.egg-info/dependency_links.txt +0 -0
  19. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/stupidpythonbrowser.egg-info/requires.txt +0 -0
  20. {stupidpythonbrowser-1.0.4 → stupidpythonbrowser-1.1.0}/stupidpythonbrowser.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stupidpythonbrowser
3
- Version: 1.0.4
3
+ Version: 1.1.0
4
4
  Summary: Native macOS Chromium/Material 3 Web Browser in PyObjC & WebKit
5
5
  Author: avevall
6
6
  Classifier: Environment :: MacOS X :: Cocoa
@@ -39,7 +39,7 @@ from pybrowser.settings import (
39
39
  sanitize_for_json
40
40
  )
41
41
  from pybrowser.doh_server import (
42
- DNS_CACHE, DNS_LOCK, STEVENBLACK_DOMAINS, BASE_ADBLOCK_DOMAINS, ADBLOCK_LOCK,
42
+ DNS_CACHE, DNS_LOCK, STEVENBLACK_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
45
  compile_webkit_adblock_rules, update_adblock_filters_from_web,
@@ -210,18 +210,27 @@ try:
210
210
  SecKeychainFindGenericPassword = sec.SecKeychainFindGenericPassword
211
211
  SecKeychainFindGenericPassword.argtypes = [c_void_p, c_uint32, c_char_p, c_uint32, c_char_p, POINTER(c_uint32), POINTER(c_void_p), POINTER(c_void_p)]
212
212
  SecKeychainItemFreeContent = sec.SecKeychainItemFreeContent
213
+ SecKeychainItemFreeContent.argtypes = [c_void_p, c_void_p]
213
214
  SecKeychainItemDelete = sec.SecKeychainItemDelete
214
215
  SecKeychainItemDelete.argtypes = [c_void_p]
216
+
217
+ cf = ctypes.cdll.LoadLibrary("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")
218
+ CFRelease = cf.CFRelease
219
+ CFRelease.argtypes = [c_void_p]
215
220
  except Exception:
216
- pass
221
+ CFRelease = None
217
222
 
218
223
  def save_to_keychain(host, username, password):
219
224
  try:
220
225
  srv, acc, pwd = host.encode('utf-8'), username.encode('utf-8'), password.encode('utf-8')
221
226
  length, data_ptr, item_ref = c_uint32(), c_void_p(), c_void_p()
222
227
  if SecKeychainFindGenericPassword(None, len(srv), srv, len(acc), acc, byref(length), byref(data_ptr), byref(item_ref)) == 0:
223
- if data_ptr.value: SecKeychainItemFreeContent(None, data_ptr)
224
- if item_ref.value: SecKeychainItemDelete(item_ref.value)
228
+ if data_ptr.value:
229
+ SecKeychainItemFreeContent(None, data_ptr.value)
230
+ if item_ref.value:
231
+ SecKeychainItemDelete(item_ref.value)
232
+ elif item_ref.value and CFRelease:
233
+ CFRelease(item_ref.value)
225
234
  SecKeychainAddGenericPassword(None, len(srv), srv, len(acc), acc, len(pwd), pwd, None)
226
235
  except Exception:
227
236
  pass
@@ -232,12 +241,14 @@ def get_from_keychain(host, username):
232
241
  length, data_ptr, item_ref = c_uint32(), c_void_p(), c_void_p()
233
242
  if SecKeychainFindGenericPassword(None, len(srv), srv, len(acc), acc, byref(length), byref(data_ptr), byref(item_ref)) == 0:
234
243
  pwd = string_at(data_ptr.value, length.value).decode('utf-8')
235
- SecKeychainItemFreeContent(None, data_ptr)
244
+ if data_ptr.value:
245
+ SecKeychainItemFreeContent(None, data_ptr.value)
246
+ if item_ref.value and CFRelease:
247
+ CFRelease(item_ref.value)
236
248
  return pwd
237
249
  except Exception:
238
250
  pass
239
251
  return None
240
-
241
252
  # ============================================================================
242
253
  # USERSCRIPTS & EXTENSIONS (CRX)
243
254
  # ============================================================================
@@ -361,7 +372,7 @@ def init_history_db():
361
372
  print(f"[History DB Error] {e}")
362
373
 
363
374
  def record_history(url, title):
364
- if not url or url.startswith("browser://"):
375
+ if not url or url.startswith("browser://") or url.startswith("about:"):
365
376
  return
366
377
  try:
367
378
  with sqlite3.connect(HISTORY_FILE) as conn:
@@ -867,6 +878,7 @@ class ChromeTabView(Cocoa.NSView):
867
878
  if target_idx != self.index:
868
879
  from_idx = self.index
869
880
  self.controller.move_tab(from_idx, target_idx)
881
+ self.index = target_idx
870
882
  self.drag_start = cur_pt
871
883
  break
872
884
 
@@ -1016,22 +1028,26 @@ class DownloadDelegate(Cocoa.NSObject):
1016
1028
  handler(Foundation.NSURL.fileURLWithPath_(dest_path))
1017
1029
 
1018
1030
  def downloadDidFinish_(self, download):
1019
- if hasattr(download, "progress") and download.progress():
1031
+ ctrl = self.controller
1032
+ if ctrl and hasattr(download, "progress") and download.progress():
1020
1033
  try:
1021
- download.progress().removeObserver_forKeyPath_(self.controller, "fractionCompleted")
1034
+ download.progress().removeObserver_forKeyPath_(ctrl, "fractionCompleted")
1022
1035
  except Exception:
1023
1036
  pass
1024
1037
  path = self.dest_paths.pop(download, None)
1025
- self.controller.finish_download(download, "Completed", dl_path=path)
1038
+ if ctrl:
1039
+ ctrl.finish_download(download, "Completed", dl_path=path)
1026
1040
 
1027
1041
  def download_didFailWithError_resumeData_(self, download, error, resumeData):
1028
- if hasattr(download, "progress") and download.progress():
1042
+ ctrl = self.controller
1043
+ if ctrl and hasattr(download, "progress") and download.progress():
1029
1044
  try:
1030
- download.progress().removeObserver_forKeyPath_(self.controller, "fractionCompleted")
1045
+ download.progress().removeObserver_forKeyPath_(ctrl, "fractionCompleted")
1031
1046
  except Exception:
1032
1047
  pass
1033
1048
  path = self.dest_paths.pop(download, None)
1034
- self.controller.finish_download(download, "Failed", dl_path=path)
1049
+ if ctrl:
1050
+ ctrl.finish_download(download, "Failed", dl_path=path)
1035
1051
 
1036
1052
  # ============================================================================
1037
1053
  # SCHEME HANDLER
@@ -1151,11 +1167,11 @@ term.onData(d => {{
1151
1167
  window.webkit.messageHandlers.terminalInput.postMessage(d);
1152
1168
  }}
1153
1169
  }});
1154
-
1170
+ const termDecoder = new TextDecoder();
1155
1171
  window.writeToTerm = b => {{
1156
1172
  const s = atob(b), u = new Uint8Array(s.length);
1157
1173
  for (let i = 0; i < s.length; i++) u[i] = s.charCodeAt(i);
1158
- term.write(new TextDecoder().decode(u));
1174
+ term.write(termDecoder.decode(u, {{ stream: true }}));
1159
1175
  }};
1160
1176
  </script></body></html>"""
1161
1177
 
@@ -1171,34 +1187,55 @@ window.writeToTerm = b => {{
1171
1187
  for b in bms:
1172
1188
  if isinstance(b, dict) and b.get("url"):
1173
1189
  u = b["url"]
1174
- # Bug 16 Fix: Guard title against None
1175
1190
  t = b.get("title") or u or ""
1176
1191
  safe_u = json.dumps(u)
1177
1192
  esc_u = html.escape(u)
1178
1193
  esc_t = html.escape(t)
1179
1194
  host = urllib.parse.urlparse(u).netloc
1180
1195
  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>")
1196
+ 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='deleteBm({safe_u})' style='margin-left:12px;padding:3px 8px;'>Delete</button></li>")
1183
1197
  rows = "".join(items)
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>"
1198
+ return f"""<!DOCTYPE html><html><head><meta charset='utf-8'><script>
1199
+ function deleteBm(url) {{
1200
+ if (window.webkit?.messageHandlers?.bridge) {{
1201
+ window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_bookmark', url: url}});
1202
+ }}
1203
+ setTimeout(() => location.reload(), 100);
1204
+ }}
1205
+ function addBm() {{
1206
+ const t = document.getElementById('t').value;
1207
+ const u = document.getElementById('u').value;
1208
+ if (u && window.webkit?.messageHandlers?.bridge) {{
1209
+ window.webkit.messageHandlers.bridge.postMessage({{cmd: 'add_bookmark', title: t, url: u}});
1210
+ }}
1211
+ setTimeout(() => location.reload(), 100);
1212
+ }}
1213
+ </script></head><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='addBm()' style='padding:4px 12px;'>Add</button></div><ul style='list-style:none;padding:0;'>{rows or '<li>No bookmarks</li>'}</ul></body></html>"""
1185
1214
 
1186
1215
  def get_history_html(self):
1187
1216
  records = query_history()
1188
1217
  items = []
1189
1218
  for r in records:
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
1219
  title_text = r[2] or r[1] or ""
1193
1220
  url_str = r[1] or ""
1194
1221
  esc_url = html.escape(url_str)
1195
1222
  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>")
1223
+ items.append(f"<tr><td>{r[3]}</td><td><a href='{esc_url}'>{esc_title}</a></td><td><button onclick='deleteHist({r[0]}, this)'>✕</button></td></tr>")
1197
1224
  rows = "".join(items)
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>"
1199
-
1200
- def get_downloads_html(self):
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>"
1225
+ return f"""<!DOCTYPE html><html><head><meta charset='utf-8'><script>
1226
+ function deleteHist(id, btn) {{
1227
+ if (window.webkit?.messageHandlers?.bridge) {{
1228
+ window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_history_item', id: id}});
1229
+ }}
1230
+ btn.closest('tr')?.remove();
1231
+ }}
1232
+ function clearAllHist() {{
1233
+ if (window.webkit?.messageHandlers?.bridge) {{
1234
+ window.webkit.messageHandlers.bridge.postMessage({{cmd: 'clear_browsing_data', targets: ['history']}});
1235
+ }}
1236
+ setTimeout(() => location.reload(), 100);
1237
+ }}
1238
+ </script></head><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>History</h2><button onclick='clearAllHist()'>Clear All</button><table style='width:100%;margin-top:15px;'>{rows or '<tr><td>No history</td></tr>'}</table></body></html>"""
1202
1239
 
1203
1240
  def get_extensions_html(self):
1204
1241
  scripts = get_installed_userscripts()
@@ -1207,17 +1244,35 @@ window.writeToTerm = b => {{
1207
1244
  f = s["file"]
1208
1245
  safe_f = json.dumps(f)
1209
1246
  checked = "checked" if s["enabled"] else ""
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);}}"
1212
1247
  items.append(
1213
1248
  f"<div style='border:1px solid #ccc;padding:10px;margin-bottom:8px;'>"
1214
1249
  f"<b>{html.escape(s['name'])}</b> ({html.escape(f)}) "
1215
- f"<label><input type='checkbox' {checked} onchange='{toggle_call}'> Enabled</label> "
1216
- f"<button onclick=\"{del_call}\">Delete</button>"
1250
+ f"<label><input type='checkbox' {checked} onchange='toggleScript({safe_f}, this.checked)'> Enabled</label> "
1251
+ f"<button onclick='deleteScript({safe_f})'>Delete</button>"
1217
1252
  f"</div>"
1218
1253
  )
1219
1254
  rows = "".join(items)
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>"
1255
+ return f"""<!DOCTYPE html><html><head><meta charset='utf-8'><script>
1256
+ function toggleScript(file, enabled) {{
1257
+ if (window.webkit?.messageHandlers?.bridge) {{
1258
+ window.webkit.messageHandlers.bridge.postMessage({{cmd: 'toggle_script', file: file, enabled: enabled}});
1259
+ }}
1260
+ }}
1261
+ function deleteScript(file) {{
1262
+ if (confirm('Delete this userscript?')) {{
1263
+ if (window.webkit?.messageHandlers?.bridge) {{
1264
+ window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_script', file: file}});
1265
+ }}
1266
+ setTimeout(() => location.reload(), 100);
1267
+ }}
1268
+ }}
1269
+ function installCrxPrompt() {{
1270
+ const id = prompt('CRX Extension ID or URL:');
1271
+ if (id && window.webkit?.messageHandlers?.bridge) {{
1272
+ window.webkit.messageHandlers.bridge.postMessage({{cmd: 'install_crx', id: id}});
1273
+ }}
1274
+ }}
1275
+ </script></head><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Extensions & Userscripts</h2><button onclick='installCrxPrompt()'>Install CRX</button><div style='margin-top:15px;'>{rows or 'No userscripts installed.'}</div></body></html>"""
1221
1276
 
1222
1277
  # ============================================================================
1223
1278
  # WEBKIT SUBCLASS & IMAGE CONTEXT MENU
@@ -1421,9 +1476,11 @@ class OmniboxPanelCoordinator:
1421
1476
  for h in query_history(query_str)[:4]:
1422
1477
  results.append({"type": "history", "text": h[2] or h[1] or "", "url": h[1]})
1423
1478
 
1479
+ # Show local suggestions immediately without waiting for remote network I/O
1480
+ self.show(results)
1481
+
1424
1482
  def fetch_remote():
1425
1483
  try:
1426
- # Secured endpoint via HTTPS to avoid keystroke sniffing
1427
1484
  url = f"https://suggestqueries.google.com/complete/search?client=chrome&q={urllib.parse.quote(query_str)}"
1428
1485
  req = urllib.request.Request(url, headers={"User-Agent": UA_PRESETS["chrome_mac"]})
1429
1486
  with urllib.request.urlopen(req, timeout=1.0) as resp:
@@ -1432,8 +1489,7 @@ class OmniboxPanelCoordinator:
1432
1489
  if req_id == self.current_req_id:
1433
1490
  dispatch_to_main(lambda: self.show(results + remote))
1434
1491
  except Exception:
1435
- if req_id == self.current_req_id:
1436
- dispatch_to_main(lambda: self.show(results))
1492
+ pass
1437
1493
 
1438
1494
  threading.Thread(target=fetch_remote, daemon=True).start()
1439
1495
 
@@ -1477,8 +1533,9 @@ class OmniboxPanelCoordinator:
1477
1533
 
1478
1534
  def hide(self):
1479
1535
  if self.suggestion_window and self.suggestion_window.isVisible():
1480
- if self.suggestion_window.parentWindow():
1481
- self.wc.window().removeChildWindow_(self.suggestion_window)
1536
+ parent = self.suggestion_window.parentWindow()
1537
+ if parent:
1538
+ parent.removeChildWindow_(self.suggestion_window)
1482
1539
  self.suggestion_window.orderOut_(None)
1483
1540
 
1484
1541
  def select_suggestion(self, sugg):
@@ -1876,9 +1933,16 @@ class BrowserWindowController(Cocoa.NSWindowController):
1876
1933
  def save_session(self):
1877
1934
  if self.is_incognito:
1878
1935
  return
1936
+ valid_tabs = [
1937
+ {"url": t.get("url"), "group_id": t.get("group_id"), "pinned": t.get("pinned", False)}
1938
+ for t in self.tabs
1939
+ if t.get("url") and not t.get("url").startswith("browser://") and not t.get("url").startswith("about:")
1940
+ ]
1941
+ if not valid_tabs and os.path.exists(SESSION_FILE):
1942
+ return
1879
1943
  data = {
1880
- "active_index": self.active_tab_index,
1881
- "tabs": [{"url": t.get("url"), "group_id": t.get("group_id"), "pinned": t.get("pinned", False)} for t in self.tabs if t.get("url") and not t.get("url").startswith("browser://")],
1944
+ "active_index": max(0, min(self.active_tab_index, len(valid_tabs) - 1)) if valid_tabs else 0,
1945
+ "tabs": valid_tabs,
1882
1946
  "groups": [asdict(g) for g in self.groups]
1883
1947
  }
1884
1948
  atomic_write_json(SESSION_FILE, data)
@@ -2169,7 +2233,22 @@ class BrowserWindowController(Cocoa.NSWindowController):
2169
2233
  uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(IN_WINDOW_FULLSCREEN_JS, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
2170
2234
 
2171
2235
  if SETTINGS.settings.get("cosmetic_adblock_enabled", True):
2172
- cosmetic_js = "(function() { const st = document.createElement('style'); st.id = '__pybrowser_cosmetic_adblock'; st.textContent = `" + COSMETIC_ADBLOCK_CSS + "`; (document.head || document.documentElement).appendChild(st); })();"
2236
+ cosmetic_js = """(function() {
2237
+ function inject() {
2238
+ const target = document.head || document.documentElement;
2239
+ if (target && !document.getElementById('__pybrowser_cosmetic_adblock')) {
2240
+ const st = document.createElement('style');
2241
+ st.id = '__pybrowser_cosmetic_adblock';
2242
+ st.textContent = `""" + COSMETIC_ADBLOCK_CSS + """`;
2243
+ target.appendChild(st);
2244
+ return true;
2245
+ }
2246
+ return false;
2247
+ }
2248
+ if (!inject()) {
2249
+ document.addEventListener('DOMContentLoaded', inject);
2250
+ }
2251
+ })();"""
2173
2252
  uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(cosmetic_js, WebKit.WKUserScriptInjectionTimeAtDocumentStart, False))
2174
2253
 
2175
2254
  if SETTINGS.settings.get("yt_adblock_enabled", True):
@@ -2899,25 +2978,31 @@ class BrowserWindowController(Cocoa.NSWindowController):
2899
2978
  if cid:
2900
2979
  threading.Thread(target=lambda: install_crx(cid), daemon=True).start()
2901
2980
  elif cmd == "prompt_save_password":
2902
- self.show_password_banner(body.get("host", ""), body.get("username", ""), body.get("password", ""))
2981
+ calling_wv = message.webView()
2982
+ verified_host = calling_wv.URL().host() if (calling_wv and calling_wv.URL()) else None
2983
+ if verified_host:
2984
+ self.show_password_banner(verified_host, body.get("username", ""), body.get("password", ""))
2903
2985
  elif cmd == "check_autofill":
2904
- # Bug 18 & 19 Fix: Apply credentials specifically to requesting webview using safe JSON encoding
2905
- host = body.get("host")
2906
- saved_user = SETTINGS.settings.get("saved_logins", {}).get(host)
2986
+ calling_wv = message.webView()
2987
+ if not calling_wv or not calling_wv.URL():
2988
+ return
2989
+ verified_host = calling_wv.URL().host()
2990
+ if not verified_host:
2991
+ return
2992
+ saved_user = SETTINGS.settings.get("saved_logins", {}).get(verified_host)
2907
2993
  if saved_user:
2908
- pwd = get_from_keychain(host, saved_user)
2994
+ pwd = get_from_keychain(verified_host, saved_user)
2909
2995
  if pwd:
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)
2996
+ pwd_json = json.dumps(pwd)
2997
+ user_json = json.dumps(saved_user)
2998
+ js = f"""(function() {{
2999
+ let pw = document.querySelector('input[type=password]');
3000
+ let user = document.querySelector('input[type=text], input[type=email]');
3001
+ if (pw) pw.value = {pwd_json};
3002
+ if (user) user.value = {user_json};
3003
+ }})();"""
3004
+ calling_wv.evaluateJavaScript_completionHandler_(js, None)
3005
+
2921
3006
  elif cmd == "save_setting":
2922
3007
  raw_key, raw_val = body.get("key"), body.get("val")
2923
3008
  if raw_key is None:
@@ -3110,9 +3195,11 @@ class BrowserWindowController(Cocoa.NSWindowController):
3110
3195
  elif pending_action == "copy_address": self.perform_copy_image_address(img_url)
3111
3196
  return None
3112
3197
 
3113
- url = act.request().URL().absoluteString() if act.request().URL() else None
3114
- if url:
3115
- self.add_tab(url)
3198
+ url = act.request().URL().absoluteString() if act.request().URL() else ""
3199
+ new_tab_idx = len(self.tabs)
3200
+ self.add_tab(url or "about:blank")
3201
+ if 0 <= new_tab_idx < len(self.tabs):
3202
+ return self.tabs[new_tab_idx].get("webview")
3116
3203
  return None
3117
3204
 
3118
3205
  def webView_decidePolicyForNavigationAction_decisionHandler_(self, wv, action, handler):
@@ -3247,13 +3334,23 @@ class BrowserWindowController(Cocoa.NSWindowController):
3247
3334
  self.omnibox_panel.hide()
3248
3335
 
3249
3336
  def windowWillClose_(self, notif):
3250
- # Bug 4 Fix: Invalidate repeating NSTimer to prevent memory retain cycle
3251
3337
  if hasattr(self, 'sleep_timer') and self.sleep_timer:
3252
3338
  self.sleep_timer.invalidate()
3253
3339
  self.sleep_timer = None
3254
3340
 
3255
- if hasattr(self, 'omnibox_panel') and self.omnibox_panel.is_visible():
3256
- self.omnibox_panel.hide()
3341
+ if hasattr(self, 'omnibox_panel') and self.omnibox_panel:
3342
+ if self.omnibox_panel.is_visible():
3343
+ self.omnibox_panel.hide()
3344
+ self.omnibox_panel.wc = None
3345
+
3346
+ # Unregister KVO observers from active downloads to prevent deallocation crash
3347
+ for d in self.downloads:
3348
+ dl_obj = d.get("obj")
3349
+ if dl_obj and hasattr(dl_obj, "progress") and dl_obj.progress():
3350
+ try:
3351
+ dl_obj.progress().removeObserver_forKeyPath_(self, "fractionCompleted")
3352
+ except Exception:
3353
+ pass
3257
3354
 
3258
3355
  self.save_session()
3259
3356
  for t in self.tabs:
@@ -3265,13 +3362,21 @@ class BrowserWindowController(Cocoa.NSWindowController):
3265
3362
  wv.removeObserver_forKeyPath_(self, k)
3266
3363
  except Exception:
3267
3364
  pass
3268
- # Bug 20 Fix: Clean up userContentController message handlers
3269
3365
  uc = wv.configuration().userContentController()
3270
3366
  for name in SCRIPT_HANDLER_NAMES:
3271
3367
  try:
3272
3368
  uc.removeScriptMessageHandlerForName_(name)
3273
3369
  except Exception:
3274
3370
  pass
3371
+
3372
+ # Break circular references
3373
+ if hasattr(self, 'download_delegate'):
3374
+ self.download_delegate.controller = None
3375
+ if hasattr(self, 'find_bar'):
3376
+ self.find_bar.controller = None
3377
+ if hasattr(self, 'bookmarks_bar'):
3378
+ self.bookmarks_bar.controller = None
3379
+
3275
3380
  app_delegate = Cocoa.NSApp.delegate()
3276
3381
  if app_delegate and hasattr(app_delegate, "controllers") and self in app_delegate.controllers:
3277
3382
  app_delegate.controllers.remove(self)
@@ -3299,7 +3404,7 @@ class AppDelegate(Cocoa.NSObject):
3299
3404
  Cocoa.NSApp.activateIgnoringOtherApps_(True)
3300
3405
 
3301
3406
  def click_monitor(event):
3302
- for c in self.controllers:
3407
+ for c in list(self.controllers):
3303
3408
  if c.omnibox_panel.is_visible():
3304
3409
  if c.omnibox_panel.suggestion_window and event.window() == c.omnibox_panel.suggestion_window:
3305
3410
  return event
@@ -3326,7 +3431,7 @@ class AppDelegate(Cocoa.NSObject):
3326
3431
  if win and isinstance(win.delegate(), BrowserWindowController):
3327
3432
  wc = win.delegate()
3328
3433
  elif hasattr(self, 'controllers') and self.controllers:
3329
- for c in self.controllers:
3434
+ for c in list(self.controllers):
3330
3435
  if c.window() == win:
3331
3436
  wc = c
3332
3437
  break
@@ -4,6 +4,8 @@ Provides encrypted local DNS proxying and WebKit content rule compilation.
4
4
  """
5
5
 
6
6
  import os
7
+ import sys
8
+ import tempfile
7
9
  import json
8
10
  import time
9
11
  import threading
@@ -104,12 +106,23 @@ def load_dns_cache():
104
106
 
105
107
  def flush_dns_cache_to_disk():
106
108
  global DNS_CACHE_DIRTY
109
+ snapshot = None
107
110
  with DNS_LOCK:
108
111
  if DNS_CACHE_DIRTY:
112
+ snapshot = dict(DNS_CACHE)
113
+ DNS_CACHE_DIRTY = False
114
+
115
+ if snapshot is not None:
116
+ try:
117
+ dir_name = os.path.dirname(DNS_CACHE_FILE) or "."
118
+ fd, temp_path = tempfile.mkstemp(dir=dir_name)
119
+ with os.fdopen(fd, 'w', encoding='utf-8') as f:
120
+ json.dump(snapshot, f, indent=2)
121
+ os.replace(temp_path, DNS_CACHE_FILE)
122
+ except Exception:
109
123
  try:
110
- with open(DNS_CACHE_FILE, 'w', encoding='utf-8') as f:
111
- json.dump(DNS_CACHE, f, indent=2)
112
- DNS_CACHE_DIRTY = False
124
+ if 'temp_path' in locals() and os.path.exists(temp_path):
125
+ os.remove(temp_path)
113
126
  except Exception:
114
127
  pass
115
128
 
@@ -130,7 +143,10 @@ def doh_resolve(hostname):
130
143
  now = time.time()
131
144
  clean_host = hostname.strip("[]").strip()
132
145
 
133
- # If already an IPv4 or IPv6 address literal, bypass DNS lookup
146
+ # Localhost and IP literals bypass external DoH
147
+ if clean_host in ("localhost", "127.0.0.1", "::1") or clean_host.endswith(".local"):
148
+ return "127.0.0.1" if clean_host in ("localhost", "127.0.0.1") else clean_host
149
+
134
150
  try:
135
151
  ipaddress.ip_address(clean_host)
136
152
  return clean_host
@@ -207,30 +223,49 @@ def doh_resolve(hostname):
207
223
  return clean_host
208
224
 
209
225
  class DoHProxyHandler(http.server.BaseHTTPRequestHandler):
226
+ rbufsize = 0
227
+ wbufsize = 0
228
+
210
229
  def log_message(self, format, *args):
211
230
  pass
212
231
 
213
232
  def do_CONNECT(self):
233
+ self.close_connection = True
214
234
  try:
215
235
  host, port = (self.path.rsplit(":", 1) if ":" in self.path else (self.path, 443))
216
236
  port = int(port)
217
237
  host = host.strip("[]")
218
238
  except Exception:
219
- self.send_error(400, "Bad Request")
239
+ try:
240
+ self.send_error(400, "Bad Request")
241
+ except (BrokenPipeError, ConnectionResetError, OSError):
242
+ pass
220
243
  return
221
244
 
222
245
  if SETTINGS.settings.get("adblock_enabled", True) and is_domain_blocked(host):
223
- self.send_error(403, "Blocked by PyBrowser uBlock Engine")
246
+ try:
247
+ self.send_error(403, "Blocked by PyBrowser uBlock Engine")
248
+ except (BrokenPipeError, ConnectionResetError, OSError):
249
+ pass
224
250
  return
225
251
 
226
252
  ip = doh_resolve(host)
253
+ upstream = None
227
254
  try:
228
255
  upstream = socket.create_connection((ip, port), timeout=6)
229
256
  self.send_response(200, "Connection Established")
230
257
  self.end_headers()
231
258
  self._tunnel(self.connection, upstream)
232
259
  except Exception:
233
- self.send_error(502, "Bad Gateway")
260
+ if upstream:
261
+ try:
262
+ upstream.close()
263
+ except Exception:
264
+ pass
265
+ try:
266
+ self.send_error(502, "Bad Gateway")
267
+ except (BrokenPipeError, ConnectionResetError, OSError):
268
+ pass
234
269
 
235
270
  def do_GET(self): self._proxy_plain_http()
236
271
  def do_POST(self): self._proxy_plain_http()
@@ -299,25 +334,29 @@ class DoHProxyHandler(http.server.BaseHTTPRequestHandler):
299
334
  except Exception:
300
335
  pass
301
336
 
337
+ class ReusableTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
338
+ allow_reuse_address = True
339
+ daemon_threads = True
340
+
341
+ def server_bind(self):
342
+ if hasattr(socket, "SO_REUSEPORT"):
343
+ try:
344
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
345
+ except Exception:
346
+ pass
347
+ super().server_bind()
348
+
349
+ def handle_error(self, request, client_address):
350
+ exc_type = sys.exc_info()[0]
351
+ if exc_type in (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
352
+ return
353
+ super().handle_error(request, client_address)
354
+
302
355
  def start_proxy_server():
303
- # Bug 23 Fix: Prevent multiple TCP server instances & duplicate worker threads
304
356
  global _PROXY_STARTED
305
357
  with _PROXY_START_LOCK:
306
358
  if _PROXY_STARTED:
307
359
  return
308
-
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
360
  try:
322
361
  server = ReusableTCPServer(("127.0.0.1", LOCAL_PROXY_PORT), DoHProxyHandler)
323
362
  threading.Thread(target=server.serve_forever, daemon=True).start()
@@ -445,4 +484,4 @@ def update_adblock_filters_from_web():
445
484
  return len(STEVENBLACK_DOMAINS)
446
485
  except Exception as e:
447
486
  print(f"[Adblock Web Update Error] {e}")
448
- return len(STEVENBLACK_DOMAINS)
487
+ return len(STEVENBLACK_DOMAINS)