pake-cli 3.15.7 → 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 +520 -158
- package/llms.txt +15 -2
- package/package.json +2 -2
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +2 -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 +69 -14
- 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
|
@@ -362,6 +362,11 @@ fn build_window(
|
|
|
362
362
|
visible,
|
|
363
363
|
new_window_features,
|
|
364
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
|
+
|
|
365
370
|
let package_name = tauri_config
|
|
366
371
|
.product_name
|
|
367
372
|
.clone()
|
|
@@ -375,10 +380,13 @@ fn build_window(
|
|
|
375
380
|
))
|
|
376
381
|
})?;
|
|
377
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.
|
|
378
386
|
#[cfg(target_os = "macos")]
|
|
379
|
-
let
|
|
380
|
-
&& window_config.ignore_certificate_errors
|
|
387
|
+
let auth_target = if label == "pake"
|
|
381
388
|
&& window_config.url_type == "web"
|
|
389
|
+
&& (config.basic_auth || window_config.ignore_certificate_errors)
|
|
382
390
|
{
|
|
383
391
|
Url::parse(&window_config.url).ok()
|
|
384
392
|
} else {
|
|
@@ -388,7 +396,7 @@ fn build_window(
|
|
|
388
396
|
// The delegate must be installed before the first TLS challenge. Start on
|
|
389
397
|
// a neutral page, then navigate from the with_webview callback below.
|
|
390
398
|
#[cfg(target_os = "macos")]
|
|
391
|
-
let url = if
|
|
399
|
+
let url = if auth_target.is_some() {
|
|
392
400
|
WebviewUrl::CustomProtocol(
|
|
393
401
|
Url::parse("about:blank").expect("about:blank must be a valid URL"),
|
|
394
402
|
)
|
|
@@ -489,12 +497,18 @@ fn build_window(
|
|
|
489
497
|
// any script that reads it (e.g. fullscreen polyfill checks for an opt-out
|
|
490
498
|
// flag), and toast must register `window.pakeToast` before Rust code
|
|
491
499
|
// calls show_toast().
|
|
492
|
-
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"));
|
|
493
505
|
|
|
494
506
|
// find.js is opt-in via --enable-find and no-ops at runtime when disabled,
|
|
495
507
|
// so only inject its ~700 lines when the feature is on. Avoids parsing the
|
|
496
508
|
// find UI on every page load in the common (find-off) case. Matches the
|
|
497
509
|
// enable_find gating already applied to the Find menu item.
|
|
510
|
+
window_builder = window_builder.initialization_script(include_str!("../inject/styles.js"));
|
|
511
|
+
|
|
498
512
|
if window_config.enable_find {
|
|
499
513
|
window_builder = window_builder.initialization_script(include_str!("../inject/find.js"));
|
|
500
514
|
}
|
|
@@ -505,7 +519,6 @@ fn build_window(
|
|
|
505
519
|
.initialization_script(include_str!("../inject/event.js"))
|
|
506
520
|
.initialization_script(include_str!("../inject/style.js"))
|
|
507
521
|
.initialization_script(include_str!("../inject/theme_refresh.js"))
|
|
508
|
-
.initialization_script(include_str!("../inject/auth.js"))
|
|
509
522
|
.initialization_script(include_str!("../inject/custom.js"));
|
|
510
523
|
|
|
511
524
|
#[cfg(target_os = "windows")]
|
|
@@ -567,6 +580,22 @@ fn build_window(
|
|
|
567
580
|
};
|
|
568
581
|
window_builder = window_builder.title_bar_style(title_bar_style);
|
|
569
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
|
+
}
|
|
570
599
|
}
|
|
571
600
|
|
|
572
601
|
// Windows and Linux: set data_directory before proxy_url
|
|
@@ -686,25 +715,51 @@ fn build_window(
|
|
|
686
715
|
|
|
687
716
|
let window = window_builder.build()?;
|
|
688
717
|
|
|
689
|
-
//
|
|
690
|
-
//
|
|
691
|
-
//
|
|
692
|
-
//
|
|
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.
|
|
693
744
|
#[cfg(target_os = "macos")]
|
|
694
|
-
if let Some(target_url) =
|
|
745
|
+
if let Some(target_url) = auth_target {
|
|
695
746
|
let allowed_host = target_url
|
|
696
747
|
.host_str()
|
|
697
748
|
.expect("web URLs must have a host")
|
|
698
749
|
.to_owned();
|
|
699
|
-
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();
|
|
700
753
|
Queue::main().exec_async(move || {
|
|
701
|
-
if let Err(error) =
|
|
702
|
-
if !crate::app::
|
|
754
|
+
if let Err(error) = auth_window.with_webview(move |webview| {
|
|
755
|
+
if !crate::app::auth::install_auth_delegate_and_navigate(
|
|
703
756
|
webview.inner(),
|
|
704
757
|
allowed_host,
|
|
705
758
|
target_url.to_string(),
|
|
759
|
+
prompt_for_basic_auth,
|
|
760
|
+
allow_invalid_certificates,
|
|
706
761
|
) {
|
|
707
|
-
eprintln!("[Pake] Failed to configure macOS
|
|
762
|
+
eprintln!("[Pake] Failed to configure macOS authentication handling.");
|
|
708
763
|
}
|
|
709
764
|
}) {
|
|
710
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
|
+
}
|
|
@@ -240,20 +240,18 @@ window.addEventListener("DOMContentLoaded", (_event) => {
|
|
|
240
240
|
position: fixed !important;
|
|
241
241
|
top: 12px !important;
|
|
242
242
|
right: 16px !important;
|
|
243
|
+
left: auto !important;
|
|
244
|
+
width: 182px !important;
|
|
243
245
|
}
|
|
244
246
|
|
|
245
|
-
#react-root [data-testid="sidebarColumn"] input[
|
|
246
|
-
width:
|
|
247
|
+
#react-root [data-testid="sidebarColumn"] form[role="search"] input[data-testid="SearchBox_Search_Input"] {
|
|
248
|
+
width: 100% !important;
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
#react-root [data-testid="sidebarColumn"] form[role="search"]:focus-within {
|
|
250
252
|
width: 280px !important;
|
|
251
253
|
backdrop-filter: blur(12px) !important;
|
|
252
254
|
}
|
|
253
|
-
|
|
254
|
-
#react-root [data-testid="sidebarColumn"] input[placeholder="Search"]:focus {
|
|
255
|
-
width: 234px !important;
|
|
256
|
-
}
|
|
257
255
|
}
|
|
258
256
|
|
|
259
257
|
@media only screen and (min-width: 1265px) {
|
|
@@ -262,10 +260,12 @@ window.addEventListener("DOMContentLoaded", (_event) => {
|
|
|
262
260
|
position: fixed !important;
|
|
263
261
|
top: 12px !important;
|
|
264
262
|
right: 16px !important;
|
|
263
|
+
left: auto !important;
|
|
264
|
+
width: 182px !important;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
#react-root [data-testid="sidebarColumn"] input[
|
|
268
|
-
width:
|
|
267
|
+
#react-root [data-testid="sidebarColumn"] form[role="search"] input[data-testid="SearchBox_Search_Input"] {
|
|
268
|
+
width: 100% !important;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
#react-root [data-testid="sidebarColumn"] form[role="search"]:focus-within {
|
|
@@ -273,10 +273,6 @@ window.addEventListener("DOMContentLoaded", (_event) => {
|
|
|
273
273
|
backdrop-filter: blur(12px) !important;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
#react-root [data-testid="sidebarColumn"] input[placeholder="Search"]:focus {
|
|
277
|
-
width: 328px !important;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
276
|
#react-root div[style*="left: -12px"] {
|
|
281
277
|
left: unset !important;
|
|
282
278
|
}
|
|
@@ -324,9 +320,14 @@ window.addEventListener("DOMContentLoaded", (_event) => {
|
|
|
324
320
|
padding-top: 36px;
|
|
325
321
|
}
|
|
326
322
|
`;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
323
|
+
if (typeof window.__PAKE_INJECT_STYLE__ === "function") {
|
|
324
|
+
window.__PAKE_INJECT_STYLE__(contentCSS, "pake-content-style");
|
|
325
|
+
} else {
|
|
326
|
+
const contentStyleElement = document.createElement("style");
|
|
327
|
+
contentStyleElement.id = "pake-content-style";
|
|
328
|
+
contentStyleElement.textContent = contentCSS;
|
|
329
|
+
document.head.appendChild(contentStyleElement);
|
|
330
|
+
}
|
|
330
331
|
|
|
331
332
|
// Top spacing adapts to head-hiding scenarios
|
|
332
333
|
const topPaddingCSS = `
|
|
@@ -503,8 +504,7 @@ window.addEventListener("DOMContentLoaded", (_event) => {
|
|
|
503
504
|
`;
|
|
504
505
|
const isMac = /Mac/i.test(navigator.userAgent);
|
|
505
506
|
if (hasImmersiveHeader(window["pakeConfig"])) {
|
|
506
|
-
const
|
|
507
|
-
topPaddingStyleElement.textContent = isMac
|
|
507
|
+
const topPaddingCSSForPlatform = isMac
|
|
508
508
|
? topPaddingCSS
|
|
509
509
|
: `
|
|
510
510
|
#pake-top-dom:active {
|
|
@@ -525,6 +525,16 @@ window.addEventListener("DOMContentLoaded", (_event) => {
|
|
|
525
525
|
z-index: 99999;
|
|
526
526
|
}
|
|
527
527
|
`;
|
|
528
|
-
|
|
528
|
+
if (typeof window.__PAKE_INJECT_STYLE__ === "function") {
|
|
529
|
+
window.__PAKE_INJECT_STYLE__(
|
|
530
|
+
topPaddingCSSForPlatform,
|
|
531
|
+
"pake-top-padding-style",
|
|
532
|
+
);
|
|
533
|
+
} else {
|
|
534
|
+
const topPaddingStyleElement = document.createElement("style");
|
|
535
|
+
topPaddingStyleElement.id = "pake-top-padding-style";
|
|
536
|
+
topPaddingStyleElement.textContent = topPaddingCSSForPlatform;
|
|
537
|
+
document.head.appendChild(topPaddingStyleElement);
|
|
538
|
+
}
|
|
529
539
|
}
|
|
530
540
|
});
|