pake-cli 3.15.6 → 3.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +574 -168
- package/llms.txt +15 -2
- package/package.json +3 -4
- package/src-tauri/Cargo.lock +6 -5
- package/src-tauri/Cargo.toml +7 -2
- package/src-tauri/src/app/auth.rs +478 -0
- package/src-tauri/src/app/config.rs +5 -0
- package/src-tauri/src/app/mod.rs +1 -1
- package/src-tauri/src/app/window.rs +93 -32
- package/src-tauri/src/inject/event.js +65 -89
- package/src-tauri/src/inject/find.js +39 -24
- package/src-tauri/src/inject/frame_links.js +62 -0
- package/src-tauri/src/inject/fullscreen.js +9 -4
- package/src-tauri/src/inject/link_policy.js +60 -0
- package/src-tauri/src/inject/style.js +28 -18
- package/src-tauri/src/inject/styles.js +100 -0
- package/src-tauri/tauri.conf.json +1 -1
- package/src-tauri/src/app/cert.rs +0 -187
|
@@ -5,6 +5,10 @@ use crate::util::{
|
|
|
5
5
|
};
|
|
6
6
|
#[cfg(target_os = "macos")]
|
|
7
7
|
use dispatch::Queue;
|
|
8
|
+
#[cfg(target_os = "macos")]
|
|
9
|
+
use objc2::MainThreadMarker;
|
|
10
|
+
#[cfg(target_os = "macos")]
|
|
11
|
+
use objc2_web_kit::WKUserContentController;
|
|
8
12
|
#[cfg(target_os = "windows")]
|
|
9
13
|
use std::{os::windows::ffi::OsStrExt, ptr, sync::OnceLock};
|
|
10
14
|
use std::{
|
|
@@ -28,6 +32,24 @@ use tauri::Theme;
|
|
|
28
32
|
#[cfg(target_os = "macos")]
|
|
29
33
|
use tauri::TitleBarStyle;
|
|
30
34
|
|
|
35
|
+
#[cfg(target_os = "macos")]
|
|
36
|
+
fn prepare_macos_new_window_configuration(features: &NewWindowFeatures) -> tauri::Result<()> {
|
|
37
|
+
let mtm = MainThreadMarker::new().ok_or_else(|| {
|
|
38
|
+
std::io::Error::other("macOS new-window configuration must run on the main thread")
|
|
39
|
+
})?;
|
|
40
|
+
// WebKit requires the exact target configuration it supplied to be used
|
|
41
|
+
// for the new page. Replace only the inherited content controller so Wry
|
|
42
|
+
// can register Pake's IPC handler and scripts once on the new webview.
|
|
43
|
+
let controller = unsafe { WKUserContentController::new(mtm) };
|
|
44
|
+
unsafe {
|
|
45
|
+
features
|
|
46
|
+
.opener()
|
|
47
|
+
.target_configuration
|
|
48
|
+
.setUserContentController(&controller);
|
|
49
|
+
}
|
|
50
|
+
Ok(())
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
#[cfg(target_os = "windows")]
|
|
32
54
|
fn build_proxy_browser_arg(url: &Url) -> Option<String> {
|
|
33
55
|
let host = url.host_str()?;
|
|
@@ -340,6 +362,11 @@ fn build_window(
|
|
|
340
362
|
visible,
|
|
341
363
|
new_window_features,
|
|
342
364
|
} = opts;
|
|
365
|
+
#[cfg(target_os = "macos")]
|
|
366
|
+
let use_native_window_tabbing = config.multi_window && new_window_features.is_none();
|
|
367
|
+
#[cfg(target_os = "macos")]
|
|
368
|
+
let prefer_native_window_tabbing = use_native_window_tabbing && label != "pake";
|
|
369
|
+
|
|
343
370
|
let package_name = tauri_config
|
|
344
371
|
.product_name
|
|
345
372
|
.clone()
|
|
@@ -353,10 +380,13 @@ fn build_window(
|
|
|
353
380
|
))
|
|
354
381
|
})?;
|
|
355
382
|
|
|
383
|
+
// On macOS both HTTP Basic auth and certificate bypass use the same
|
|
384
|
+
// navigation-delegate proxy. Start on a neutral page so the proxy is in
|
|
385
|
+
// place before the target can issue its first authentication challenge.
|
|
356
386
|
#[cfg(target_os = "macos")]
|
|
357
|
-
let
|
|
358
|
-
&& window_config.ignore_certificate_errors
|
|
387
|
+
let auth_target = if label == "pake"
|
|
359
388
|
&& window_config.url_type == "web"
|
|
389
|
+
&& (config.basic_auth || window_config.ignore_certificate_errors)
|
|
360
390
|
{
|
|
361
391
|
Url::parse(&window_config.url).ok()
|
|
362
392
|
} else {
|
|
@@ -366,7 +396,7 @@ fn build_window(
|
|
|
366
396
|
// The delegate must be installed before the first TLS challenge. Start on
|
|
367
397
|
// a neutral page, then navigate from the with_webview callback below.
|
|
368
398
|
#[cfg(target_os = "macos")]
|
|
369
|
-
let url = if
|
|
399
|
+
let url = if auth_target.is_some() {
|
|
370
400
|
WebviewUrl::CustomProtocol(
|
|
371
401
|
Url::parse("about:blank").expect("about:blank must be a valid URL"),
|
|
372
402
|
)
|
|
@@ -467,12 +497,18 @@ fn build_window(
|
|
|
467
497
|
// any script that reads it (e.g. fullscreen polyfill checks for an opt-out
|
|
468
498
|
// flag), and toast must register `window.pakeToast` before Rust code
|
|
469
499
|
// calls show_toast().
|
|
470
|
-
window_builder = window_builder
|
|
500
|
+
window_builder = window_builder
|
|
501
|
+
.initialization_script_for_all_frames(&config_script)
|
|
502
|
+
.initialization_script_for_all_frames(include_str!("../inject/link_policy.js"))
|
|
503
|
+
.initialization_script_for_all_frames(include_str!("../inject/auth.js"))
|
|
504
|
+
.initialization_script_for_all_frames(include_str!("../inject/frame_links.js"));
|
|
471
505
|
|
|
472
506
|
// find.js is opt-in via --enable-find and no-ops at runtime when disabled,
|
|
473
507
|
// so only inject its ~700 lines when the feature is on. Avoids parsing the
|
|
474
508
|
// find UI on every page load in the common (find-off) case. Matches the
|
|
475
509
|
// enable_find gating already applied to the Find menu item.
|
|
510
|
+
window_builder = window_builder.initialization_script(include_str!("../inject/styles.js"));
|
|
511
|
+
|
|
476
512
|
if window_config.enable_find {
|
|
477
513
|
window_builder = window_builder.initialization_script(include_str!("../inject/find.js"));
|
|
478
514
|
}
|
|
@@ -483,7 +519,6 @@ fn build_window(
|
|
|
483
519
|
.initialization_script(include_str!("../inject/event.js"))
|
|
484
520
|
.initialization_script(include_str!("../inject/style.js"))
|
|
485
521
|
.initialization_script(include_str!("../inject/theme_refresh.js"))
|
|
486
|
-
.initialization_script(include_str!("../inject/auth.js"))
|
|
487
522
|
.initialization_script(include_str!("../inject/custom.js"));
|
|
488
523
|
|
|
489
524
|
#[cfg(target_os = "windows")]
|
|
@@ -545,6 +580,22 @@ fn build_window(
|
|
|
545
580
|
};
|
|
546
581
|
window_builder = window_builder.title_bar_style(title_bar_style);
|
|
547
582
|
window_builder = window_builder.theme(theme);
|
|
583
|
+
|
|
584
|
+
// Tauri disables automatic tabbing unless an identifier is provided.
|
|
585
|
+
// Existing multi-window apps already have a stable bundle identifier,
|
|
586
|
+
// so use it without exposing another CLI/config option. Web-created
|
|
587
|
+
// popups keep their own native window.
|
|
588
|
+
if use_native_window_tabbing {
|
|
589
|
+
window_builder = window_builder
|
|
590
|
+
.tabbing_identifier(&tauri_config.identifier)
|
|
591
|
+
.on_document_title_changed(|window, title| {
|
|
592
|
+
if !title.trim().is_empty() {
|
|
593
|
+
if let Err(error) = window.set_title(&title) {
|
|
594
|
+
eprintln!("[Pake] Failed to update the macOS tab title: {error}");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
}
|
|
548
599
|
}
|
|
549
600
|
|
|
550
601
|
// Windows and Linux: set data_directory before proxy_url
|
|
@@ -594,26 +645,10 @@ fn build_window(
|
|
|
594
645
|
}
|
|
595
646
|
|
|
596
647
|
if let Some(features) = new_window_features {
|
|
597
|
-
// Reuse only opener-provided position/size on macOS; sharing the opener
|
|
598
|
-
// WKWebViewConfiguration triggers duplicate WKScriptMessageHandler
|
|
599
|
-
// registrations on macOS 26+ and crashes the app (issue #1194).
|
|
600
648
|
#[cfg(target_os = "macos")]
|
|
601
|
-
|
|
602
|
-
if let Some(position) = features.position() {
|
|
603
|
-
window_builder = window_builder.position(position.x, position.y);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if let Some(size) = features.size() {
|
|
607
|
-
window_builder = window_builder.inner_size(size.width, size.height);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
window_builder = window_builder.focused(true);
|
|
611
|
-
}
|
|
649
|
+
prepare_macos_new_window_configuration(&features)?;
|
|
612
650
|
|
|
613
|
-
|
|
614
|
-
{
|
|
615
|
-
window_builder = window_builder.window_features(features).focused(true);
|
|
616
|
-
}
|
|
651
|
+
window_builder = window_builder.window_features(features).focused(true);
|
|
617
652
|
}
|
|
618
653
|
|
|
619
654
|
// Capture webview-initiated downloads (blob:, data:, Content-Disposition,
|
|
@@ -680,25 +715,51 @@ fn build_window(
|
|
|
680
715
|
|
|
681
716
|
let window = window_builder.build()?;
|
|
682
717
|
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
//
|
|
718
|
+
// A shared identifier alone leaves each NSWindow in automatic mode.
|
|
719
|
+
// Prefer tabs only for Cmd+N clones so they join the main window's tab
|
|
720
|
+
// group. The main window stays bar-less until another tab exists, while
|
|
721
|
+
// web-auth and window.open popups remain separate native windows.
|
|
722
|
+
#[cfg(target_os = "macos")]
|
|
723
|
+
if prefer_native_window_tabbing {
|
|
724
|
+
let tabbing_window = window.clone();
|
|
725
|
+
Queue::main().exec_async(move || match tabbing_window.ns_window() {
|
|
726
|
+
Ok(ns_window_ptr) => unsafe {
|
|
727
|
+
let Some(ns_window) =
|
|
728
|
+
objc2::rc::Retained::retain(ns_window_ptr as *mut objc2_app_kit::NSWindow)
|
|
729
|
+
else {
|
|
730
|
+
eprintln!("[Pake] Failed to retain the macOS window for tabbing.");
|
|
731
|
+
return;
|
|
732
|
+
};
|
|
733
|
+
ns_window.setTabbingMode(objc2_app_kit::NSWindowTabbingMode::Preferred);
|
|
734
|
+
},
|
|
735
|
+
Err(error) => {
|
|
736
|
+
eprintln!("[Pake] Failed to access the macOS window for tabbing: {error}");
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// WKWebView does not show an HTTP Basic login dialog and ignores Chromium's
|
|
742
|
+
// certificate-error flag. Install one host-scoped delegate for both flows
|
|
743
|
+
// on the process-lifetime main window, then navigate to the real target.
|
|
687
744
|
#[cfg(target_os = "macos")]
|
|
688
|
-
if let Some(target_url) =
|
|
745
|
+
if let Some(target_url) = auth_target {
|
|
689
746
|
let allowed_host = target_url
|
|
690
747
|
.host_str()
|
|
691
748
|
.expect("web URLs must have a host")
|
|
692
749
|
.to_owned();
|
|
693
|
-
let
|
|
750
|
+
let prompt_for_basic_auth = config.basic_auth;
|
|
751
|
+
let allow_invalid_certificates = window_config.ignore_certificate_errors;
|
|
752
|
+
let auth_window = window.clone();
|
|
694
753
|
Queue::main().exec_async(move || {
|
|
695
|
-
if let Err(error) =
|
|
696
|
-
if !crate::app::
|
|
754
|
+
if let Err(error) = auth_window.with_webview(move |webview| {
|
|
755
|
+
if !crate::app::auth::install_auth_delegate_and_navigate(
|
|
697
756
|
webview.inner(),
|
|
698
757
|
allowed_host,
|
|
699
758
|
target_url.to_string(),
|
|
759
|
+
prompt_for_basic_auth,
|
|
760
|
+
allow_invalid_certificates,
|
|
700
761
|
) {
|
|
701
|
-
eprintln!("[Pake] Failed to configure macOS
|
|
762
|
+
eprintln!("[Pake] Failed to configure macOS authentication handling.");
|
|
702
763
|
}
|
|
703
764
|
}) {
|
|
704
765
|
eprintln!("[Pake] Failed to access the macOS webview: {error}");
|
|
@@ -529,52 +529,6 @@ function isDownloadableFile(url) {
|
|
|
529
529
|
}
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
-
// Public suffixes where the registrable domain needs more than two labels.
|
|
533
|
-
// Not exhaustive; covers common packaging targets that last-two-label
|
|
534
|
-
// matching would collapse incorrectly (e.g. amazon.co.uk vs evil.co.uk,
|
|
535
|
-
// or every *.github.io site into one "domain").
|
|
536
|
-
const MULTI_PART_PUBLIC_SUFFIXES = [
|
|
537
|
-
"co.uk",
|
|
538
|
-
"org.uk",
|
|
539
|
-
"ac.uk",
|
|
540
|
-
"gov.uk",
|
|
541
|
-
"com.au",
|
|
542
|
-
"net.au",
|
|
543
|
-
"org.au",
|
|
544
|
-
"co.jp",
|
|
545
|
-
"ne.jp",
|
|
546
|
-
"or.jp",
|
|
547
|
-
"co.kr",
|
|
548
|
-
"co.in",
|
|
549
|
-
"com.br",
|
|
550
|
-
"com.cn",
|
|
551
|
-
"com.tw",
|
|
552
|
-
"com.hk",
|
|
553
|
-
"com.sg",
|
|
554
|
-
"github.io",
|
|
555
|
-
"gitlab.io",
|
|
556
|
-
"pages.dev",
|
|
557
|
-
];
|
|
558
|
-
|
|
559
|
-
function getRootDomain(hostname) {
|
|
560
|
-
const normalized = String(hostname || "").toLowerCase();
|
|
561
|
-
if (!normalized) {
|
|
562
|
-
return "";
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const parts = normalized.split(".").filter(Boolean);
|
|
566
|
-
if (parts.length <= 1) {
|
|
567
|
-
return normalized;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const lastTwo = parts.slice(-2).join(".");
|
|
571
|
-
if (MULTI_PART_PUBLIC_SUFFIXES.includes(lastTwo) && parts.length >= 3) {
|
|
572
|
-
return parts.slice(-3).join(".");
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
return lastTwo;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
532
|
function normalizeAnchorHref(rawHref) {
|
|
579
533
|
return typeof rawHref === "string" ? rawHref.trim() : "";
|
|
580
534
|
}
|
|
@@ -591,7 +545,10 @@ function shouldBypassPakeLinkHandling(rawHref) {
|
|
|
591
545
|
}
|
|
592
546
|
|
|
593
547
|
function shouldNavigateAuthInCurrentWindow() {
|
|
594
|
-
return
|
|
548
|
+
// WKWebView can abort on auth popups, while WebKitGTK may return a truthy
|
|
549
|
+
// proxy even when the native side denies the window. Keep those platforms
|
|
550
|
+
// in-place without changing the working WebView2 popup path on Windows.
|
|
551
|
+
return /mac|linux/i.test(getDesktopPlatform());
|
|
595
552
|
}
|
|
596
553
|
|
|
597
554
|
function canNavigateAuthUrl(url) {
|
|
@@ -639,21 +596,55 @@ function openAuthNavigation(originalWindowOpen, url, name, specs) {
|
|
|
639
596
|
return authWindow;
|
|
640
597
|
}
|
|
641
598
|
|
|
599
|
+
// Install the receiver at document start: a subframe can open a link before
|
|
600
|
+
// the main page's DOMContentLoaded routing setup has run.
|
|
601
|
+
let openFrameLink = null;
|
|
602
|
+
const pendingFrameLinks = [];
|
|
603
|
+
function isDescendantFrame(source, parent = window) {
|
|
604
|
+
for (let index = 0; index < parent.frames.length; index++) {
|
|
605
|
+
const frame = parent.frames[index];
|
|
606
|
+
if (frame === source || isDescendantFrame(source, frame)) return true;
|
|
607
|
+
}
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
function routeFrameLink(source, href) {
|
|
611
|
+
try {
|
|
612
|
+
// Recheck on delivery because the frame may have been removed meanwhile.
|
|
613
|
+
if (!isDescendantFrame(source)) return;
|
|
614
|
+
const url = new URL(href);
|
|
615
|
+
if (!["http:", "https:", "mailto:", "tel:"].includes(url.protocol)) return;
|
|
616
|
+
if (openFrameLink) {
|
|
617
|
+
openFrameLink.call(window, url.href, "_blank");
|
|
618
|
+
} else {
|
|
619
|
+
pendingFrameLinks.push({ source, href: url.href });
|
|
620
|
+
}
|
|
621
|
+
} catch (error) {
|
|
622
|
+
console.error("[Pake] Failed to route frame link:", error);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
window.addEventListener("message", (event) => {
|
|
626
|
+
if (
|
|
627
|
+
event.data?.type !== "pake:frame-external-link" ||
|
|
628
|
+
typeof event.data.url !== "string" ||
|
|
629
|
+
!event.source ||
|
|
630
|
+
event.source === window
|
|
631
|
+
)
|
|
632
|
+
return;
|
|
633
|
+
routeFrameLink(event.source, event.data.url);
|
|
634
|
+
});
|
|
635
|
+
window.addEventListener("pagehide", () => {
|
|
636
|
+
pendingFrameLinks.length = 0;
|
|
637
|
+
});
|
|
638
|
+
|
|
642
639
|
document.addEventListener("DOMContentLoaded", () => {
|
|
643
640
|
const tauri = window.__TAURI__;
|
|
644
641
|
const appWindow = tauri.window.getCurrentWindow();
|
|
645
642
|
const invoke = tauri.core.invoke;
|
|
646
643
|
const pakeConfig = window["pakeConfig"] || {};
|
|
647
644
|
const forceInternalNavigation = pakeConfig.force_internal_navigation === true;
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
try {
|
|
652
|
-
internalUrlPattern = new RegExp(internalUrlRegex);
|
|
653
|
-
} catch (e) {
|
|
654
|
-
console.error("[Pake] Invalid internal_url_regex pattern:", e);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
645
|
+
const matchesInternalUrl = createInternalUrlMatcher(
|
|
646
|
+
pakeConfig.internal_url_regex,
|
|
647
|
+
);
|
|
657
648
|
|
|
658
649
|
if (!document.getElementById("pake-top-dom") && hasImmersiveHeader()) {
|
|
659
650
|
const topDom = document.createElement("div");
|
|
@@ -728,39 +719,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
728
719
|
});
|
|
729
720
|
};
|
|
730
721
|
|
|
731
|
-
|
|
732
|
-
const isSameDomain = (url) => {
|
|
733
|
-
try {
|
|
734
|
-
const linkUrl = new URL(url);
|
|
735
|
-
const currentUrl = new URL(window.location.href);
|
|
736
|
-
|
|
737
|
-
if (linkUrl.hostname === currentUrl.hostname) return true;
|
|
738
|
-
|
|
739
|
-
// e.g. www.bilibili.com and m.bilibili.com share bilibili.com;
|
|
740
|
-
// amazon.co.uk must not share a root with evil.co.uk.
|
|
741
|
-
return (
|
|
742
|
-
getRootDomain(currentUrl.hostname) === getRootDomain(linkUrl.hostname)
|
|
743
|
-
);
|
|
744
|
-
} catch (e) {
|
|
745
|
-
return false;
|
|
746
|
-
}
|
|
747
|
-
};
|
|
748
|
-
|
|
749
|
-
// Check if URL should be treated as internal based on regex pattern or domain
|
|
750
|
-
const isInternalUrl = (url) => {
|
|
751
|
-
// If regex pattern is configured, use it as the primary check
|
|
752
|
-
if (internalUrlPattern) {
|
|
753
|
-
try {
|
|
754
|
-
return internalUrlPattern.test(url);
|
|
755
|
-
} catch (e) {
|
|
756
|
-
console.error("[Pake] Error testing internal_url_regex:", e);
|
|
757
|
-
// Fall back to domain check on error
|
|
758
|
-
return isSameDomain(url);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
// Default to domain-based check
|
|
762
|
-
return isSameDomain(url);
|
|
763
|
-
};
|
|
722
|
+
const isInternalUrl = (url) => matchesInternalUrl(url, window.location.href);
|
|
764
723
|
|
|
765
724
|
const detectAnchorElementClick = (e) => {
|
|
766
725
|
// Safety check: ensure e.target exists and is an Element with closest method
|
|
@@ -944,6 +903,23 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
944
903
|
}
|
|
945
904
|
};
|
|
946
905
|
|
|
906
|
+
// The sender is untrusted even when it belongs to this webview. This bridge
|
|
907
|
+
// may only open external links, never navigate the top page or create auth
|
|
908
|
+
// windows on a sandboxed frame's behalf.
|
|
909
|
+
openFrameLink = (url) => {
|
|
910
|
+
if (
|
|
911
|
+
forceInternalNavigation ||
|
|
912
|
+
isInternalUrl(url) ||
|
|
913
|
+
window.isAuthLink(url)
|
|
914
|
+
) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
handleExternalLink(url);
|
|
918
|
+
};
|
|
919
|
+
for (const { source, href } of pendingFrameLinks.splice(0)) {
|
|
920
|
+
routeFrameLink(source, href);
|
|
921
|
+
}
|
|
922
|
+
|
|
947
923
|
// Set the default zoom, There are problems with Loop without using try-catch.
|
|
948
924
|
try {
|
|
949
925
|
setDefaultZoom();
|
|
@@ -199,9 +199,7 @@
|
|
|
199
199
|
return;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
const
|
|
203
|
-
style.id = STYLE_ID;
|
|
204
|
-
style.textContent = `
|
|
202
|
+
const css = `
|
|
205
203
|
#${PANEL_ID} {
|
|
206
204
|
position: fixed;
|
|
207
205
|
top: 14px;
|
|
@@ -312,9 +310,16 @@
|
|
|
312
310
|
}
|
|
313
311
|
`;
|
|
314
312
|
|
|
315
|
-
(
|
|
316
|
-
|
|
317
|
-
|
|
313
|
+
if (typeof window.__PAKE_INJECT_STYLE__ === "function") {
|
|
314
|
+
window.__PAKE_INJECT_STYLE__(css, STYLE_ID);
|
|
315
|
+
} else {
|
|
316
|
+
const style = document.createElement("style");
|
|
317
|
+
style.id = STYLE_ID;
|
|
318
|
+
style.textContent = css;
|
|
319
|
+
(document.head || document.body || document.documentElement)?.appendChild(
|
|
320
|
+
style,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
318
323
|
}
|
|
319
324
|
|
|
320
325
|
function createButton(label, title, onClick) {
|
|
@@ -522,29 +527,39 @@
|
|
|
522
527
|
}
|
|
523
528
|
|
|
524
529
|
function runSearch(query = state.query) {
|
|
530
|
+
clearTimeout(state.searchTimer);
|
|
525
531
|
state.query = query;
|
|
526
|
-
|
|
532
|
+
// A full scan includes any pending page edits. Disconnect while replacing
|
|
533
|
+
// DOM marks so our own mutations cannot schedule another search.
|
|
534
|
+
stopObservingDocumentChanges();
|
|
535
|
+
try {
|
|
536
|
+
clearHighlights();
|
|
537
|
+
|
|
538
|
+
if (!query) {
|
|
539
|
+
state.matches = [];
|
|
540
|
+
state.activeIndex = -1;
|
|
541
|
+
state.truncated = false;
|
|
542
|
+
updateCounter();
|
|
543
|
+
return getState();
|
|
544
|
+
}
|
|
527
545
|
|
|
528
|
-
|
|
529
|
-
state.matches =
|
|
530
|
-
state.
|
|
531
|
-
state.
|
|
532
|
-
updateCounter();
|
|
533
|
-
return getState();
|
|
534
|
-
}
|
|
546
|
+
const result = collectMatches(query);
|
|
547
|
+
state.matches = result.matches;
|
|
548
|
+
state.truncated = result.truncated;
|
|
549
|
+
state.activeIndex = state.matches.length > 0 ? 0 : -1;
|
|
535
550
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
state.activeIndex = state.matches.length > 0 ? 0 : -1;
|
|
551
|
+
if (!applyCustomHighlights()) {
|
|
552
|
+
applyDomHighlights();
|
|
553
|
+
}
|
|
540
554
|
|
|
541
|
-
|
|
542
|
-
|
|
555
|
+
updateCounter();
|
|
556
|
+
scrollActiveIntoView();
|
|
557
|
+
return getState();
|
|
558
|
+
} finally {
|
|
559
|
+
if (state.isOpen) {
|
|
560
|
+
observeDocumentChanges();
|
|
561
|
+
}
|
|
543
562
|
}
|
|
544
|
-
|
|
545
|
-
updateCounter();
|
|
546
|
-
scrollActiveIntoView();
|
|
547
|
-
return getState();
|
|
548
563
|
}
|
|
549
564
|
|
|
550
565
|
function debounceSearch(query) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Subframes do not receive the main page's Tauri/event injection on WebKit.
|
|
2
|
+
// Forward only known external destinations; native internal/auth/blank popup
|
|
3
|
+
// behavior, including its WindowProxy return value, stays with the frame.
|
|
4
|
+
(function () {
|
|
5
|
+
if (window === window.top) return;
|
|
6
|
+
const config = window.pakeConfig || {};
|
|
7
|
+
if (config.force_internal_navigation === true) return;
|
|
8
|
+
const isInternalUrl = createInternalUrlMatcher(config.internal_url_regex);
|
|
9
|
+
const originalOpen = window.open;
|
|
10
|
+
|
|
11
|
+
function externalDestination(rawUrl, name) {
|
|
12
|
+
if (
|
|
13
|
+
typeof rawUrl !== "string" ||
|
|
14
|
+
!rawUrl.trim() ||
|
|
15
|
+
rawUrl.trim().startsWith("#")
|
|
16
|
+
) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const url = new URL(rawUrl, document.baseURI);
|
|
21
|
+
if (!["http:", "https:", "mailto:", "tel:"].includes(url.protocol))
|
|
22
|
+
return null;
|
|
23
|
+
if (window.isAuthPopup(url.href, name)) return null;
|
|
24
|
+
if (isInternalUrl(url.href, config.url)) return null;
|
|
25
|
+
return url.href;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function forward(url) {
|
|
32
|
+
window.top.postMessage({ type: "pake:frame-external-link", url }, "*");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
window.open = function (url, name, specs) {
|
|
36
|
+
// Named targets can navigate an existing frame and must retain its proxy.
|
|
37
|
+
if (name && String(name).toLowerCase() !== "_blank") {
|
|
38
|
+
return originalOpen.call(window, url, name, specs);
|
|
39
|
+
}
|
|
40
|
+
const external = externalDestination(url, name);
|
|
41
|
+
if (!external) return originalOpen.call(window, url, name, specs);
|
|
42
|
+
forward(external);
|
|
43
|
+
return null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
document.addEventListener(
|
|
47
|
+
"click",
|
|
48
|
+
(event) => {
|
|
49
|
+
const anchor = event.target?.closest?.("a[href]");
|
|
50
|
+
if (!anchor || anchor.hasAttribute("download")) return;
|
|
51
|
+
// Named browsing contexts may be part of the page's own frame layout.
|
|
52
|
+
if (anchor.target && !["_blank", "_new", "_self"].includes(anchor.target))
|
|
53
|
+
return;
|
|
54
|
+
const external = externalDestination(anchor.getAttribute("href"), "");
|
|
55
|
+
if (!external) return;
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
event.stopImmediatePropagation();
|
|
58
|
+
forward(external);
|
|
59
|
+
},
|
|
60
|
+
true,
|
|
61
|
+
);
|
|
62
|
+
})();
|
|
@@ -25,9 +25,7 @@
|
|
|
25
25
|
let monitorId = null;
|
|
26
26
|
|
|
27
27
|
if (!document.getElementById("pake-fullscreen-style")) {
|
|
28
|
-
const
|
|
29
|
-
styleEl.id = "pake-fullscreen-style";
|
|
30
|
-
styleEl.textContent = `
|
|
28
|
+
const css = `
|
|
31
29
|
body.pake-fullscreen-active {
|
|
32
30
|
overflow: hidden !important;
|
|
33
31
|
}
|
|
@@ -51,7 +49,14 @@
|
|
|
51
49
|
object-fit: contain !important;
|
|
52
50
|
}
|
|
53
51
|
`;
|
|
54
|
-
|
|
52
|
+
if (typeof window.__PAKE_INJECT_STYLE__ === "function") {
|
|
53
|
+
window.__PAKE_INJECT_STYLE__(css, "pake-fullscreen-style");
|
|
54
|
+
} else {
|
|
55
|
+
const styleEl = document.createElement("style");
|
|
56
|
+
styleEl.id = "pake-fullscreen-style";
|
|
57
|
+
styleEl.textContent = css;
|
|
58
|
+
document.head.appendChild(styleEl);
|
|
59
|
+
}
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
function startFullscreenMonitor() {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Shared by the main page and the lightweight subframe bridge.
|
|
2
|
+
// This list intentionally preserves Pake's existing domain routing policy.
|
|
3
|
+
const MULTI_PART_PUBLIC_SUFFIXES = [
|
|
4
|
+
"co.uk",
|
|
5
|
+
"org.uk",
|
|
6
|
+
"ac.uk",
|
|
7
|
+
"gov.uk",
|
|
8
|
+
"com.au",
|
|
9
|
+
"net.au",
|
|
10
|
+
"org.au",
|
|
11
|
+
"co.jp",
|
|
12
|
+
"ne.jp",
|
|
13
|
+
"or.jp",
|
|
14
|
+
"co.kr",
|
|
15
|
+
"co.in",
|
|
16
|
+
"com.br",
|
|
17
|
+
"com.cn",
|
|
18
|
+
"com.tw",
|
|
19
|
+
"com.hk",
|
|
20
|
+
"com.sg",
|
|
21
|
+
"github.io",
|
|
22
|
+
"gitlab.io",
|
|
23
|
+
"pages.dev",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function getRootDomain(hostname) {
|
|
27
|
+
const normalized = String(hostname || "").toLowerCase();
|
|
28
|
+
if (!normalized) return "";
|
|
29
|
+
const parts = normalized.split(".").filter(Boolean);
|
|
30
|
+
if (parts.length <= 1) return normalized;
|
|
31
|
+
const lastTwo = parts.slice(-2).join(".");
|
|
32
|
+
if (MULTI_PART_PUBLIC_SUFFIXES.includes(lastTwo) && parts.length >= 3) {
|
|
33
|
+
return parts.slice(-3).join(".");
|
|
34
|
+
}
|
|
35
|
+
return lastTwo;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createInternalUrlMatcher(pattern) {
|
|
39
|
+
let regex = null;
|
|
40
|
+
if (pattern) {
|
|
41
|
+
try {
|
|
42
|
+
regex = new RegExp(pattern);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error("[Pake] Invalid internal_url_regex pattern:", error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return (url, baseUrl) => {
|
|
48
|
+
if (regex) return regex.test(url);
|
|
49
|
+
try {
|
|
50
|
+
const target = new URL(url);
|
|
51
|
+
const current = new URL(baseUrl);
|
|
52
|
+
return (
|
|
53
|
+
target.hostname === current.hostname ||
|
|
54
|
+
getRootDomain(target.hostname) === getRootDomain(current.hostname)
|
|
55
|
+
);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|