diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a3458e..7697d07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ All notable changes to kage are recorded here. The format follows ## [Unreleased] +### Fixed + +- `go install github.com/tamnd/kage/cmd/kage@latest` works again ([#72](https://github.com/tamnd/kage/issues/72)). + The module no longer carries a `replace` directive for `github.com/ysmood/leakless`. + Headless Chrome is driven with [chromedp](https://github.com/chromedp/chromedp), so the antivirus-flagged leakless helper is not linked at all (also keeping [#68](https://github.com/tamnd/kage/issues/68) fixed without a replace). + +### Changed + +- Page renders no longer apply [go-rod-stealth](https://github.com/go-rod/stealth)'s anti-detection evasions, which the chromedp migration does not carry over. + Chrome still launches with automation flags off, but pages now see headless Chrome's default user agent on renders (asset and robots fetches still use `--user-agent`). + Sites with aggressive bot detection may serve a clone different content than before. +- `--settle` is now a fixed wait after page load instead of a network-idle watch: chromedp has no equivalent of go-rod's request-idle helper. + A page that keeps fetching past the settle window may be snapshotted slightly earlier than before. + ## [0.3.11] - 2026-08-01 ### Fixed diff --git a/README.md b/README.md index 76195d2..7cc9465 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ The repo is split by concern: cmd/kage/ thin main: pins the main thread, then hands off to cli.Execute cli/ the cobra command tree and flag wiring clone/ the crawl: frontier, render workers, asset workers, resume state -browser/ headless Chrome control and DOM snapshotting +browser/ headless Chrome control (chromedp) and DOM snapshotting sanitize/ strip scripts, handlers, and javascript: URLs from the DOM asset/ download and localise CSS, images, and fonts urlx/ the deterministic URL-to-path mapping diff --git a/browser/launcher.go b/browser/launcher.go deleted file mode 100644 index 27b9aa8..0000000 --- a/browser/launcher.go +++ /dev/null @@ -1,13 +0,0 @@ -package browser - -// chromeLauncher is the small part of Rod's launcher kage needs. The -// platform-specific implementations keep Rod's launcher (and therefore its -// embedded leakless watchdog) out of Windows binaries while retaining the -// upstream launcher on platforms where leakless is enabled. -type chromeLauncher interface { - Leakless(bool) chromeLauncher - Headless(bool) chromeLauncher - Set(string, ...string) chromeLauncher - Bin(string) chromeLauncher - Launch() (string, error) -} diff --git a/browser/launcher_other.go b/browser/launcher_other.go deleted file mode 100644 index afbffda..0000000 --- a/browser/launcher_other.go +++ /dev/null @@ -1,44 +0,0 @@ -//go:build !windows - -package browser - -import ( - "github.com/go-rod/rod/lib/launcher" - "github.com/go-rod/rod/lib/launcher/flags" -) - -type rodChromeLauncher struct { - launcher *launcher.Launcher -} - -func newChromeLauncher() chromeLauncher { - return &rodChromeLauncher{launcher: launcher.New()} -} - -func (l *rodChromeLauncher) Leakless(enable bool) chromeLauncher { - l.launcher.Leakless(enable) - return l -} - -func (l *rodChromeLauncher) Headless(enable bool) chromeLauncher { - l.launcher.Headless(enable) - return l -} - -func (l *rodChromeLauncher) Set(name string, values ...string) chromeLauncher { - l.launcher.Set(flags.Flag(name), values...) - return l -} - -func (l *rodChromeLauncher) Bin(path string) chromeLauncher { - l.launcher.Bin(path) - return l -} - -func (l *rodChromeLauncher) Launch() (string, error) { - return l.launcher.Launch() -} - -func launcherLookPath() (string, bool) { - return launcher.LookPath() -} diff --git a/browser/launcher_windows.go b/browser/launcher_windows.go deleted file mode 100644 index f0eeeea..0000000 --- a/browser/launcher_windows.go +++ /dev/null @@ -1,271 +0,0 @@ -//go:build windows - -package browser - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "log" - "net" - "net/http" - "os" - "os/exec" - "path/filepath" - "sort" - "strconv" - "strings" - "syscall" - "time" - - "github.com/ysmood/fetchup" -) - -const windowsChromiumRevision = 1321438 - -type windowsChromeLauncher struct { - bin string - flags map[string][]string -} - -func newChromeLauncher() chromeLauncher { - return &windowsChromeLauncher{flags: map[string][]string{ - "headless": nil, - "no-first-run": nil, - "no-startup-window": nil, - "disable-background-networking": nil, - "disable-background-timer-throttling": nil, - "disable-backgrounding-occluded-windows": nil, - "disable-breakpad": nil, - "disable-client-side-phishing-detection": nil, - "disable-default-apps": nil, - "disable-hang-monitor": nil, - "disable-popup-blocking": nil, - "disable-prompt-on-repost": nil, - "disable-renderer-backgrounding": nil, - "disable-sync": nil, - "disable-site-isolation-trials": nil, - "enable-automation": nil, - "enable-features": {"NetworkService", "NetworkServiceInProcess"}, - "force-color-profile": {"srgb"}, - "metrics-recording-only": nil, - "use-mock-keychain": nil, - "disable-features": {"site-per-process", "TranslateUI"}, - "disable-component-extensions-with-background-pages": nil, - }} -} - -// Leakless is intentionally a no-op. kage has always disabled Rod's watchdog -// on Windows, and avoiding Rod's launcher import is what keeps the watchdog's -// embedded executable out of kage.exe. -func (l *windowsChromeLauncher) Leakless(bool) chromeLauncher { return l } - -func (l *windowsChromeLauncher) Headless(enable bool) chromeLauncher { - if enable { - l.flags["headless"] = nil - } else { - delete(l.flags, "headless") - } - return l -} - -func (l *windowsChromeLauncher) Set(name string, values ...string) chromeLauncher { - l.flags[strings.TrimLeft(name, "-")] = values - return l -} - -func (l *windowsChromeLauncher) Bin(path string) chromeLauncher { - l.bin = path - return l -} - -func (l *windowsChromeLauncher) Launch() (string, error) { - bin := l.bin - if bin == "" { - var err error - bin, err = windowsChrome() - if err != nil { - return "", err - } - } - - userDir, err := os.MkdirTemp("", "kage-chrome-") - if err != nil { - return "", fmt.Errorf("create Chrome profile: %w", err) - } - - port, err := freeLocalPort() - if err != nil { - _ = os.RemoveAll(userDir) - return "", fmt.Errorf("reserve Chrome debug port: %w", err) - } - l.flags["user-data-dir"] = []string{userDir} - l.flags["remote-debugging-address"] = []string{"127.0.0.1"} - l.flags["remote-debugging-port"] = []string{strconv.Itoa(port)} - - var output bytes.Buffer - cmd := exec.Command(bin, l.formatArgs()...) - cmd.Stdout = &output - cmd.Stderr = &output - cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} - if err := cmd.Start(); err != nil { - _ = os.RemoveAll(userDir) - return "", fmt.Errorf("start Chrome: %w", err) - } - - exited := make(chan error, 1) - go func() { - exited <- cmd.Wait() - _ = os.RemoveAll(userDir) - }() - - endpoint := fmt.Sprintf("http://127.0.0.1:%d/json/version", port) - deadline := time.NewTimer(15 * time.Second) - defer deadline.Stop() - ticker := time.NewTicker(50 * time.Millisecond) - defer ticker.Stop() - - for { - if u, ok := debuggerURL(endpoint); ok { - return u, nil - } - select { - case err := <-exited: - return "", fmt.Errorf("Chrome exited before DevTools was ready: %w\n%s", err, tail(output.String(), 4096)) - case <-deadline.C: - _ = cmd.Process.Kill() - <-exited - return "", fmt.Errorf("Chrome DevTools did not become ready\n%s", tail(output.String(), 4096)) - case <-ticker.C: - } - } -} - -func (l *windowsChromeLauncher) formatArgs() []string { - args := make([]string, 0, len(l.flags)) - for name, values := range l.flags { - arg := "--" + name - if values != nil { - arg += "=" + strings.Join(values, ",") - } - args = append(args, arg) - } - sort.Strings(args) - return args -} - -func launcherLookPath() (string, bool) { - for _, name := range []string{"chrome", "msedge", "chromium"} { - if bin, err := exec.LookPath(name); err == nil { - return bin, true - } - } - for _, bin := range windowsBrowserCandidates() { - if _, err := os.Stat(bin); err == nil { - return bin, true - } - } - if bin := cachedWindowsChromium(); bin != "" { - return bin, true - } - return "", false -} - -func windowsBrowserCandidates() []string { - var candidates []string - for _, root := range []string{ - os.Getenv("PROGRAMFILES"), - os.Getenv("PROGRAMFILES(X86)"), - os.Getenv("LOCALAPPDATA"), - } { - if root == "" { - continue - } - candidates = append(candidates, - filepath.Join(root, "Google", "Chrome", "Application", "chrome.exe"), - filepath.Join(root, "Chromium", "Application", "chrome.exe"), - filepath.Join(root, "Microsoft", "Edge", "Application", "msedge.exe"), - ) - } - return candidates -} - -func windowsChrome() (string, error) { - if bin, ok := launcherLookPath(); ok { - return bin, nil - } - - root := windowsChromiumRoot() - dir := filepath.Join(root, fmt.Sprintf("chromium-%d", windowsChromiumRevision)) - urls := []string{ - fmt.Sprintf("https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/%d/chrome-win.zip", windowsChromiumRevision), - fmt.Sprintf("https://registry.npmmirror.com/-/binary/chromium-browser-snapshots/Win_x64/%d/chrome-win.zip", windowsChromiumRevision), - } - download := fetchup.New(dir, urls...) - download.Logger = log.New(io.Discard, "", 0) - if err := download.Fetch(); err != nil { - return "", fmt.Errorf("find or download Chrome: %w", err) - } - if err := fetchup.StripFirstDir(dir); err != nil { - return "", fmt.Errorf("unpack Chrome: %w", err) - } - - bin := filepath.Join(dir, "chrome.exe") - if _, err := os.Stat(bin); err != nil { - return "", fmt.Errorf("downloaded Chrome executable: %w", err) - } - return bin, nil -} - -func cachedWindowsChromium() string { - matches, _ := filepath.Glob(filepath.Join(windowsChromiumRoot(), "chromium-*", "chrome.exe")) - sort.Sort(sort.Reverse(sort.StringSlice(matches))) - if len(matches) == 0 { - return "" - } - return matches[0] -} - -func windowsChromiumRoot() string { - root := os.Getenv("APPDATA") - if root == "" { - root, _ = os.UserCacheDir() - } - return filepath.Join(root, "rod", "browser") -} - -func freeLocalPort() (int, error) { - listener, err := net.Listen("tcp4", "127.0.0.1:0") - if err != nil { - return 0, err - } - port := listener.Addr().(*net.TCPAddr).Port - return port, listener.Close() -} - -func debuggerURL(endpoint string) (string, bool) { - client := http.Client{Timeout: 250 * time.Millisecond} - response, err := client.Get(endpoint) - if err != nil { - return "", false - } - defer func() { _ = response.Body.Close() }() - if response.StatusCode != http.StatusOK { - return "", false - } - var version struct { - WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"` - } - if json.NewDecoder(response.Body).Decode(&version) != nil || version.WebSocketDebuggerURL == "" { - return "", false - } - return version.WebSocketDebuggerURL, true -} - -func tail(value string, limit int) string { - if len(value) <= limit { - return value - } - return value[len(value)-limit:] -} diff --git a/browser/leakless.go b/browser/leakless.go deleted file mode 100644 index f1f6944..0000000 --- a/browser/leakless.go +++ /dev/null @@ -1,7 +0,0 @@ -package browser - -import "runtime" - -func launcherLeakless() bool { - return runtime.GOOS != "windows" -} diff --git a/browser/pool.go b/browser/pool.go index 24ea367..84097ba 100644 --- a/browser/pool.go +++ b/browser/pool.go @@ -3,20 +3,27 @@ // through here: navigate, let the page settle, then serialise the final DOM — // the same markup a human would have seen — which the rest of the pipeline then // strips of scripts and localises. +// +// Chrome is launched by this package directly (os/exec + remote debugging), not +// through go-rod's launcher. That keeps github.com/ysmood/leakless — and the +// antivirus-flagged embedded helper it ships — out of the dependency graph, so +// go install and Windows package managers stay clean (issues #68, #72). package browser import ( "context" "fmt" "os" + "os/exec" "runtime" "strings" "sync" "time" - "github.com/go-rod/rod/lib/proto" - "github.com/tamnd/kage/internal/rod" - "github.com/tamnd/kage/internal/stealth" + "github.com/chromedp/cdproto/browser" + "github.com/chromedp/cdproto/network" + cdpruntime "github.com/chromedp/cdproto/runtime" + "github.com/chromedp/chromedp" ) // Options configure a Pool. @@ -46,9 +53,10 @@ type Pool struct { opts Options sem chan struct{} - mu sync.Mutex - browser *rod.Browser - closed bool + mu sync.Mutex + allocCtx context.Context + cancel context.CancelFunc + closed bool } // New creates a Pool. Chrome is launched lazily on the first Render. @@ -91,138 +99,137 @@ func (p *Pool) Render(ctx context.Context, rawURL string) (RenderResult, error) return RenderResult{}, ctx.Err() } - b, err := p.getBrowser() - if err != nil { + if err := p.ensureBrowser(); err != nil { return RenderResult{}, err } - page, err := stealth.Page(b) - if err != nil { - return RenderResult{}, fmt.Errorf("new page: %w", err) - } - defer func() { _ = page.Close() }() - - page = page.Context(ctx).Timeout(p.opts.RenderTimeout) - - // Watch the main document's response so a navigation that turns out to be a - // non-HTML resource (a zip, a CSV, a bare image) is caught and handed back for - // the asset downloader, rather than rendered as a broken page or, with downloads - // denied, left as an aborted navigation (issue #32). The content type arrives in - // the response headers whether Chrome renders the body or aborts it as a denied - // download, so this catches both. - mainContentType := watchMainDocument(page) - - navErr := page.Navigate(rawURL) - // A denied download aborts the navigation, so inspect the captured content type - // before treating a navigation error as a failure. waitFor gives the response - // event a brief moment to be processed; for an HTML page it returns at once. + tabCtx, cancel := chromedp.NewContext(p.allocCtx) + defer cancel() + // The tab context is rooted at the pool's allocator, which outlives any + // single Render; forward the caller's cancellation so an interrupt (Ctrl-C + // during a clone) aborts an in-flight page at once instead of waiting out + // the render timeout below. + stop := context.AfterFunc(ctx, cancel) + defer stop() + + timeout := p.opts.RenderTimeout + if timeout <= 0 { + timeout = 30 * time.Second + } + tabCtx, cancelTimeout := context.WithTimeout(tabCtx, timeout) + defer cancelTimeout() + + // Enable network events and deny browser-initiated downloads before any + // navigation so a zip/CSV never lands in the user's Downloads folder and so + // the main-document content-type watcher can classify non-HTML navigations + // (issue #32). Best-effort: if a call is unsupported, the content-type + // watcher below still keeps binaries out of the mirror. + _ = chromedp.Run(tabCtx, chromedp.ActionFunc(func(ctx context.Context) error { + if err := network.Enable().Do(ctx); err != nil { + return err + } + return browser.SetDownloadBehavior(browser.SetDownloadBehaviorBehaviorDeny).Do(ctx) + })) + mainContentType := watchMainDocument(tabCtx) + + // chromedp.Navigate waits for the frame's load event. A denied download + // aborts navigation, so inspect the captured content type before treating a + // navigation error as a hard failure. + navErr := chromedp.Run(tabCtx, chromedp.Navigate(rawURL)) if ct := waitFor(ctx, mainContentType, 2*time.Second); ct != "" && !isHTML(ct) { return RenderResult{}, &ErrNotHTML{URL: rawURL, ContentType: ct} } - if navErr != nil { + if navErr != nil && !isObjRefChainError(navErr) { + // Object-reference-chain errors from Chrome are non-fatal when the + // document still loaded (issue #36). return RenderResult{}, fmt.Errorf("navigate %s: %w", rawURL, navErr) } - if err := page.WaitLoad(); err != nil { - // Chrome's DevTools Protocol may return "Object reference chain is too - // long" when a page's JavaScript builds deeply nested object graphs. - // The page has still loaded its HTML — the error is only about Chrome's - // internal object tracking, not about the document. Log the warning and - // continue rendering rather than failing the entire page (issue #36). - if !isObjRefChainError(err) { - return RenderResult{}, fmt.Errorf("wait load %s: %w", rawURL, err) - } - } - settle(page, p.opts.Settle) - if p.opts.Scroll { - autoScroll(page) - settle(page, p.opts.Settle) - } - html, err := page.HTML() - if err != nil { - return RenderResult{}, fmt.Errorf("serialise %s: %w", rawURL, err) + settle(tabCtx, p.opts.Settle) + if p.opts.Scroll { + autoScroll(tabCtx) + settle(tabCtx, p.opts.Settle) + } + + var html, finalURL, title string + if err := chromedp.Run(tabCtx, + chromedp.OuterHTML("html", &html, chromedp.ByQuery), + chromedp.Location(&finalURL), + chromedp.Title(&title), + ); err != nil { + if html == "" { + return RenderResult{}, fmt.Errorf("serialise %s: %w", rawURL, err) + } + // Partial success: the DOM serialised but a follow-up read (final URL or + // title) failed. Keep the rendered page and say so, rather than dropping + // it or failing silently. + fmt.Fprintf(os.Stderr, "kage: serialise %s: %v (keeping the rendered DOM)\n", rawURL, err) } - - res := RenderResult{HTML: html, FinalURL: rawURL} - if info, err := page.Info(); err == nil && info != nil { - res.FinalURL = info.URL - res.Title = info.Title + if finalURL == "" { + finalURL = rawURL } - return res, nil + return RenderResult{HTML: html, FinalURL: finalURL, Title: title}, nil } -// getBrowser lazily connects to or launches Chrome. -func (p *Pool) getBrowser() (*rod.Browser, error) { +// ensureBrowser lazily connects to or launches Chrome. +func (p *Pool) ensureBrowser() error { p.mu.Lock() defer p.mu.Unlock() if p.closed { - return nil, fmt.Errorf("pool is closed") - } - if p.browser != nil { - return p.browser, nil - } - - controlURL := p.opts.ControlURL - if controlURL == "" { - l := newChromeLauncher().Leakless(launcherLeakless()). - Headless(p.opts.Headless). - Set("disable-blink-features", "AutomationControlled"). - Set("disable-gpu", "") - - // Chrome's sandbox is the main line of defense when rendering pages from - // the open web, so kage keeps it on by default (issue #10). It is dropped - // only where it genuinely cannot initialize: inside a container, or when - // running as root, where Chrome otherwise refuses to start. The decision - // is logged so it is never silent. - if off, reason := disableSandbox(); off { - l = l.Set("no-sandbox", "") - warnSandboxDisabled(reason) - } + return fmt.Errorf("pool is closed") + } + if p.allocCtx != nil { + return nil + } - // In a container, the default /dev/shm is only 64 MB, too small for - // Chrome's renderer on large pages, so steer it to a temp file instead. - // Outside a container /dev/shm is roomy and faster, so leave it alone. - // - // The "chrome_crashpad_handler: --database is required" abort seen in - // containers (issue #7) is not fixed here: the crash-reporter flags do not - // stop Chrome from spawning the handler. Its real cause is an unwritable - // HOME, which leaves the crash database path empty; the image keeps HOME - // writable instead (see the Dockerfile). - if inContainer() { - l = l.Set("disable-dev-shm-usage", "") - } + if p.opts.ControlURL != "" { + allocCtx, cancel := chromedp.NewRemoteAllocator(context.Background(), p.opts.ControlURL) + p.allocCtx = allocCtx + p.cancel = cancel + return nil + } - if bin := p.chromeBin(); bin != "" { - l = l.Bin(bin) - } - u, err := l.Launch() - if err != nil { - return nil, fmt.Errorf("launch Chrome: %w", err) - } - controlURL = u - } - - b := rod.New().ControlURL(controlURL) - if err := b.Connect(); err != nil { - return nil, fmt.Errorf("connect Chrome: %w", err) - } - - // kage never wants Chrome to write a file to disk. Every asset is fetched - // through kage's own downloader, which applies the size and media policy, so a - // Chrome-initiated download is only ever an accident: navigating an link - // that turns out to be a binary (a zip, an installer, a CSV) makes Chrome save - // it to the user's Downloads folder, a surprise side effect of a crawl - // (issue #32). Denying downloads browser-wide stops that. The navigation is - // aborted instead, and Render's non-HTML detection reroutes the URL through the - // asset downloader, where the asset policy decides its fate. This is - // best-effort: if the call is unsupported, the non-HTML detection still keeps - // the binary out of the saved mirror. - _ = proto.BrowserSetDownloadBehavior{ - Behavior: proto.BrowserSetDownloadBehaviorBehaviorDeny, - }.Call(b) - - p.browser = b - return b, nil + opts := append(chromedp.DefaultExecAllocatorOptions[:], + chromedp.Flag("disable-blink-features", "AutomationControlled"), + chromedp.Flag("disable-gpu", true), + chromedp.Flag("enable-automation", false), + ) + if p.opts.Headless { + opts = append(opts, chromedp.Headless) + } else { + opts = append(opts, chromedp.Flag("headless", false)) + } + + // Chrome's sandbox is the main line of defense when rendering pages from + // the open web, so kage keeps it on by default (issue #10). It is dropped + // only where it genuinely cannot initialize: inside a container, or when + // running as root, where Chrome otherwise refuses to start. + if off, reason := disableSandbox(); off { + opts = append(opts, chromedp.NoSandbox) + warnSandboxDisabled(reason) + } + // In a container, the default /dev/shm is only 64 MB, too small for + // Chrome's renderer on large pages (issue #7 notes related container pain). + if inContainer() { + opts = append(opts, chromedp.Flag("disable-dev-shm-usage", true)) + } + if bin := p.chromeBin(); bin != "" { + opts = append(opts, chromedp.ExecPath(bin)) + } + + allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) + // Touch the browser once so launch failures surface here, not on first page. + browserCtx, browserCancel := chromedp.NewContext(allocCtx) + if err := chromedp.Run(browserCtx); err != nil { + browserCancel() + cancel() + return fmt.Errorf("launch Chrome: %w", err) + } + browserCancel() + + p.allocCtx = allocCtx + p.cancel = cancel + return nil } // Close shuts down the managed Chrome process. @@ -230,36 +237,39 @@ func (p *Pool) Close() error { p.mu.Lock() defer p.mu.Unlock() p.closed = true - if p.browser == nil { - return nil + if p.cancel != nil { + p.cancel() + p.cancel = nil + p.allocCtx = nil } - err := p.browser.Close() - p.browser = nil - return err + return nil } // LookChrome reports the path of a usable Chrome/Chromium binary and whether one -// was found, checking KAGE_CHROME, CHROME_BIN, rod's own lookup, and the common -// system install locations. Tests use it to skip when no browser is present. +// was found, checking KAGE_CHROME, CHROME_BIN, and the common system install +// locations. Tests use it to skip when no browser is present. func LookChrome() (string, bool) { for _, env := range []string{"KAGE_CHROME", "CHROME_BIN"} { if v := os.Getenv(env); v != "" { return v, true } } - if bin, ok := launcherLookPath(); ok { - return bin, true - } for _, c := range systemChromeCandidates() { if _, err := os.Stat(c); err == nil { return c, true } } + // chromedp's default lookup (google-chrome, chromium, …) on PATH. + for _, name := range []string{"google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "chrome"} { + if p, err := exec.LookPath(name); err == nil && p != "" { + return p, true + } + } return "", false } // chromeBin returns an explicit Chrome path from options or the environment, or -// "" to let the launcher find/download one. +// "" to let the allocator find one. func (p *Pool) chromeBin() string { if p.opts.ChromeBin != "" { return p.opts.ChromeBin @@ -269,9 +279,6 @@ func (p *Pool) chromeBin() string { return v } } - if bin, ok := launcherLookPath(); ok { - return bin - } for _, c := range systemChromeCandidates() { if _, err := os.Stat(c); err == nil { return c @@ -330,10 +337,6 @@ func warnSandboxDisabled(reason string) { // inContainer reports whether kage is running inside a container, where Chrome // needs container-specific flags. It honors IN_DOCKER (set it in your image) // and the /.dockerenv marker that Docker writes into every container. -// -// Keeping the sandbox on by default and dropping it only here was prompted by -// Dimitrios Prasakis (issue #10); the IN_DOCKER opt-in was suggested on Hacker -// News (https://news.ycombinator.com/item?id=48534865). Thanks to both. func inContainer() bool { if envTrue("IN_DOCKER") { return true @@ -379,31 +382,23 @@ func envBool(name string) (val, ok bool) { // watchMainDocument subscribes to network responses and returns an accessor for // the main document's content type. The first Document-type response is the main // frame's navigation; later Document responses are sub-frames (iframes), whose -// type kage does not police, so only the first is kept. The accessor is safe to -// call from another goroutine. Any setup error leaves the accessor returning "", -// which the caller reads as "unknown, render normally". -func watchMainDocument(page *rod.Page) func() string { +// type kage does not police, so only the first is kept. +func watchMainDocument(ctx context.Context) func() string { var ( mu sync.Mutex ct string ) - if err := (proto.NetworkEnable{}).Call(page); err != nil { - return func() string { return "" } - } - wait := page.EachEvent(func(e *proto.NetworkResponseReceived) { - if e.Type != proto.NetworkResourceTypeDocument || e.Response == nil { + chromedp.ListenTarget(ctx, func(ev interface{}) { + e, ok := ev.(*network.EventResponseReceived) + if !ok || e.Type != network.ResourceTypeDocument || e.Response == nil { return } mu.Lock() if ct == "" { - ct = e.Response.MIMEType + ct = e.Response.MimeType } mu.Unlock() }) - // EachEvent's wait blocks until the page context ends, draining events as they - // arrive; run it for the page's lifetime. The deferred page.Close in Render - // cancels the context and unblocks it. - go wait() return func() string { mu.Lock() defer mu.Unlock() @@ -412,10 +407,7 @@ func watchMainDocument(page *rod.Page) func() string { } // waitFor polls get until it returns a non-empty value, the deadline passes, or -// the context is cancelled, then returns whatever it last saw. It exists because -// the network response is processed on another goroutine, so the value may not be -// set the instant Navigate returns; an HTML page sets it within a few -// milliseconds, while a never-arriving response simply waits out the deadline. +// the context is cancelled, then returns whatever it last saw. func waitFor(ctx context.Context, get func() string, deadline time.Duration) string { const step = 20 * time.Millisecond for waited := time.Duration(0); waited < deadline; waited += step { @@ -433,9 +425,8 @@ func waitFor(ctx context.Context, get func() string, deadline time.Duration) str // isHTML reports whether a document content type is one kage renders and saves as // a page. HTML and XHTML qualify; an empty type is treated as HTML so an unlabelled -// response still renders. Anything else (a zip, a CSV, a PDF, a bare image or -// JSON) is an asset that reached the page worker because its link carried no file -// extension to classify it by. +// response still renders. Anything else is an asset that reached the page worker +// because its link carried no file extension to classify it by. func isHTML(contentType string) bool { mt := strings.ToLower(strings.TrimSpace(contentType)) if i := strings.IndexByte(mt, ';'); i >= 0 { @@ -453,29 +444,29 @@ func isObjRefChainError(err error) bool { return err != nil && strings.Contains(err.Error(), "Object reference chain is too long") } -// settle waits for the network to go quiet for d, recovering from any rod -// panic and capping the wait so a chatty page can never hang the worker. -func settle(page *rod.Page, d time.Duration) { +// settle waits a fixed quiet window d after load so late-arriving DOM changes +// land in the snapshot. It approximates network idle with a plain sleep — +// chromedp has no built-in equivalent of rod's WaitRequestIdle — bounded by +// ctx so a cancelled or timed-out render never hangs the worker. +func settle(ctx context.Context, d time.Duration) { if d <= 0 { return } - defer func() { _ = recover() }() - done := make(chan struct{}) - go func() { - defer func() { _ = recover(); close(done) }() - wait := page.WaitRequestIdle(d, nil, nil, []proto.NetworkResourceType{}) - wait() - }() select { - case <-done: - case <-time.After(d + 5*time.Second): + case <-ctx.Done(): + case <-time.After(d): } } -// autoScroll scrolls to the bottom in steps to trigger lazy-loaded images. -func autoScroll(page *rod.Page) { - defer func() { _ = recover() }() - _, _ = page.Eval(`() => new Promise((resolve) => { +// autoScroll scrolls to the bottom in steps to trigger lazy-loaded images. The +// evaluation awaits the scroll promise — chromedp's Evaluate does not await +// promises unless asked, unlike rod's Eval — so Render only continues once the +// page has been walked to the bottom and back. +func autoScroll(ctx context.Context) { + await := func(p *cdpruntime.EvaluateParams) *cdpruntime.EvaluateParams { + return p.WithAwaitPromise(true) + } + _ = chromedp.Run(ctx, chromedp.Evaluate(`(() => new Promise((resolve) => { let total = 0; const step = 800; const timer = setInterval(() => { @@ -487,5 +478,5 @@ func autoScroll(page *rod.Page) { resolve(true); } }, 100); - })`) + }))()`, nil, await)) } diff --git a/browser/pool_test.go b/browser/pool_test.go index 4c14a78..00674f4 100644 --- a/browser/pool_test.go +++ b/browser/pool_test.go @@ -6,7 +6,6 @@ import ( "net/http" "net/http/httptest" "os" - "runtime" "strings" "testing" "time" @@ -84,14 +83,6 @@ func TestDisableSandboxContainer(t *testing.T) { } } -func TestLauncherLeaklessDisabledOnWindows(t *testing.T) { - got := launcherLeakless() - want := runtime.GOOS != "windows" - if got != want { - t.Errorf("launcherLeakless() = %v on %s; want %v", got, runtime.GOOS, want) - } -} - func TestRenderCapturesFinalDOM(t *testing.T) { if testing.Short() { t.Skip("render test drives Chrome; skipped under -short") @@ -208,3 +199,78 @@ func TestRenderRoutesNonHTML(t *testing.T) { } } } + +func TestRenderAbortsOnCallerCancel(t *testing.T) { + if testing.Short() { + t.Skip("render test drives Chrome; skipped under -short") + } + if _, ok := LookChrome(); !ok { + t.Skip("no Chrome/Chromium found; skipping render test") + } + + // A server that never answers, so the render can only end by cancellation. + // The caller's context must abort the in-flight navigation (Ctrl-C during a + // clone), not wait out the render timeout. + release := make(chan struct{}) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + <-release + })) + defer func() { close(release); srv.Close() }() + + p := New(Options{Headless: true, Workers: 1, RenderTimeout: 30 * time.Second}) + defer func() { _ = p.Close() }() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + start := time.Now() + _, err := p.Render(ctx, srv.URL) + if err == nil { + t.Error("Render against a hanging server: got nil error, want a cancellation error") + } + if el := time.Since(start); el > 10*time.Second { + t.Errorf("Render blocked %v with a 2s caller context and 30s render timeout; want a prompt abort", el) + } +} + +func TestRenderScrollCapturesLazyContent(t *testing.T) { + if testing.Short() { + t.Skip("render test drives Chrome; skipped under -short") + } + if _, ok := LookChrome(); !ok { + t.Skip("no Chrome/Chromium found; skipping render test") + } + + // A tall page that injects content only once the visitor scrolls far down. + // The snapshot must wait for the scroll to finish, or the node is missed. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = w.Write([]byte(` +
+ +`)) + })) + defer srv.Close() + + p := New(Options{Headless: true, Workers: 1, Settle: 300 * time.Millisecond, RenderTimeout: 20 * time.Second, Scroll: true}) + defer func() { _ = p.Close() }() + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + res, err := p.Render(ctx, srv.URL) + if err != nil { + t.Fatalf("render: %v", err) + } + if !strings.Contains(res.HTML, "loaded-on-scroll") { + t.Errorf("scroll-triggered content missing from the snapshot:\n%s", res.HTML) + } +} diff --git a/docs/content/getting-started/installation.md b/docs/content/getting-started/installation.md index 6152047..1c3f5db 100644 --- a/docs/content/getting-started/installation.md +++ b/docs/content/getting-started/installation.md @@ -85,7 +85,7 @@ kage clone example.com --chrome /path/to/chromium export KAGE_CHROME=/path/to/chromium ``` -If no browser is found, kage's launcher can download a private copy of Chromium -on first use. +kage does not download Chromium for you: install Chrome or Chromium, or use the +container image above. Next: [the quick start](/getting-started/quick-start/). diff --git a/docs/content/reference/configuration.md b/docs/content/reference/configuration.md index e271a89..0d3cb51 100644 --- a/docs/content/reference/configuration.md +++ b/docs/content/reference/configuration.md @@ -16,7 +16,8 @@ locating the browser. | `CHROME_BIN` | Fallback Chrome path, read if `KAGE_CHROME` is unset. | If neither is set and no system Chrome is found in the usual install locations, -kage's launcher can download a private copy of Chromium on first use. +kage does not download Chromium for you; install a system browser or use the +container image. ## Output layout diff --git a/docs/content/reference/release-notes.md b/docs/content/reference/release-notes.md index 8177947..e842983 100644 --- a/docs/content/reference/release-notes.md +++ b/docs/content/reference/release-notes.md @@ -6,6 +6,10 @@ weight: 40 The authoritative, commit-level history lives in [`CHANGELOG.md`](https://github.com/tamnd/kage/blob/main/CHANGELOG.md) and on the [releases page](https://github.com/tamnd/kage/releases). This page summarises each version. +## Unreleased + +- **`go install` works again.** The module no longer uses a `replace` for leakless. Chrome is driven with chromedp instead of go-rod, so the antivirus-flagged helper is not linked and Windows package installs stay clean ([#72](https://github.com/tamnd/kage/issues/72), [#68](https://github.com/tamnd/kage/issues/68)). + ## v0.3.11 - **`go install ...@latest` works again.** The v0.3.9 antivirus fix replaced Rod's leakless dependency with a local stub. That kept the flagged helper out of `kage.exe`, but Go refuses versioned installation of a module containing a dependency-changing `replace` directive ([#72](https://github.com/tamnd/kage/issues/72)). Windows now launches Chrome through a small platform-specific launcher that never imports leakless. Other platforms keep Rod's launcher, the Windows binary remains free of the flagged helper, and the module no longer needs `replace`. diff --git a/go.mod b/go.mod index 0cdc66b..e4fde2e 100644 --- a/go.mod +++ b/go.mod @@ -5,16 +5,13 @@ go 1.26.5 require ( charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 github.com/charmbracelet/fang v1.0.0 - github.com/go-rod/rod v0.116.2 + github.com/chromedp/cdproto v0.0.0-20260719223732-95f6af754cfe + github.com/chromedp/chromedp v0.16.0 github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.18.6 github.com/parquet-go/parquet-go v0.30.1 github.com/spf13/cobra v1.10.2 github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 - github.com/ysmood/fetchup v0.2.3 - github.com/ysmood/goob v0.4.0 - github.com/ysmood/got v0.40.0 - github.com/ysmood/gson v0.7.3 golang.org/x/image v0.42.0 golang.org/x/net v0.56.0 golang.org/x/time v0.15.0 @@ -29,9 +26,14 @@ require ( github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/chromedp/sysutil v1.1.0 // indirect github.com/clipperhouse/displaywidth v0.4.1 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.4.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect @@ -47,9 +49,8 @@ require ( github.com/spf13/pflag v1.0.9 // indirect github.com/twpayne/go-geom v1.6.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/ysmood/leakless v0.9.0 // indirect golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect + golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.39.0 // indirect google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/go.sum b/go.sum index 1d786e5..5f94666 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,12 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8 github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/chromedp/cdproto v0.0.0-20260719223732-95f6af754cfe h1:PmhRwLZ8qLtldQCBiydwdPFJI8WVQ936ux1cpgHLRb8= +github.com/chromedp/cdproto v0.0.0-20260719223732-95f6af754cfe/go.mod h1:RwFsSODCtFExll+GhHM6R92SARHR3Z3oipaxLHj46C0= +github.com/chromedp/chromedp v0.16.0 h1:rOO4deOm4CbZgBCa8mD9g2rDyIoNs0BkgvNrlbp5ouk= +github.com/chromedp/chromedp v0.16.0/go.mod h1:rbuGKFT1vMcFcFqKfPIO1GpX/N+2s8onm2qMxZLbU5U= +github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= +github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= github.com/clipperhouse/displaywidth v0.4.1 h1:uVw9V8UDfnggg3K2U84VWY1YLQ/x2aKSCtkRyYozfoU= github.com/clipperhouse/displaywidth v0.4.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= @@ -37,8 +43,14 @@ github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= -github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= +github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg= +github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -49,6 +61,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= @@ -63,6 +77,8 @@ github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/parquet-go/bitpack v1.0.0 h1:AUqzlKzPPXf2bCdjfj4sTeacrUwsT7NlcYDMUQxPcQA= github.com/parquet-go/bitpack v1.0.0/go.mod h1:XnVk9TH+O40eOOmvpAVZ7K2ocQFrQwysLMnc6M/8lgs= github.com/parquet-go/jsonlite v1.0.0 h1:87QNdi56wOfsE5bdgas0vRzHPxfJgzrXGml1zZdd7VU= @@ -90,20 +106,6 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= -github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= -github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= -github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= -github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= -github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= -github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= -github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= -github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= -github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= -github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= -github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= -github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= -github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= @@ -113,8 +115,9 @@ golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= diff --git a/internal/rod/LICENSE b/internal/rod/LICENSE deleted file mode 100644 index 2a0a30d..0000000 --- a/internal/rod/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -The MIT License - -Copyright 2019 Yad Smood - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/internal/rod/README.md b/internal/rod/README.md deleted file mode 100644 index b963b94..0000000 --- a/internal/rod/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Rod controller - -This directory contains the controller layer from -[go-rod/rod v0.116.2](https://github.com/go-rod/rod/tree/v0.116.2), under its -original MIT license. - -kage always launches Chrome itself and connects through an explicit DevTools -URL. The upstream controller also imports Rod's launcher as an automatic -fallback. That launcher imports `github.com/ysmood/leakless`, whose Windows -package embeds a prebuilt helper that Windows Defender flags. Even with -leakless disabled at runtime, the import links the helper into `kage.exe`. - -`browser.go` therefore differs in two functional places: `Connect` requires the -explicit URL kage already supplies, and the unused monitor-opening shortcut is -disabled. A few lint-only spellings are updated for kage's current Go toolchain. -The controller continues to use Rod's public `lib/*` packages so protocol types -and behavior stay aligned with the pinned module version. diff --git a/internal/rod/browser.go b/internal/rod/browser.go deleted file mode 100644 index 3499a91..0000000 --- a/internal/rod/browser.go +++ /dev/null @@ -1,541 +0,0 @@ -//go:generate go run ./lib/utils/setup -//go:generate go run ./lib/proto/generate -//go:generate go run ./lib/js/generate -//go:generate go run ./lib/assets/generate -//go:generate go run ./lib/utils/lint - -// Package rod is a high-level driver directly based on DevTools Protocol. -package rod - -import ( - "context" - "errors" - "reflect" - "strings" - "sync" - "time" - - "github.com/go-rod/rod/lib/cdp" - "github.com/go-rod/rod/lib/defaults" - "github.com/go-rod/rod/lib/devices" - "github.com/go-rod/rod/lib/proto" - "github.com/go-rod/rod/lib/utils" - "github.com/ysmood/goob" -) - -// Browser implements these interfaces. -var ( - _ proto.Client = &Browser{} - _ proto.Contextable = &Browser{} -) - -// Browser represents the browser. -// It doesn't depends on file system, it should work with remote browser seamlessly. -// To check the env var you can use to quickly enable options from CLI, check here: -// https://pkg.go.dev/github.com/go-rod/rod/lib/defaults -type Browser struct { - // BrowserContextID is the id for incognito window - BrowserContextID proto.BrowserBrowserContextID - - e eFunc - - ctx context.Context - - sleeper func() utils.Sleeper - - logger utils.Logger - - slowMotion time.Duration // see defaults.slow - trace bool // see defaults.Trace - monitor string - - defaultDevice devices.Device - - controlURL string - client CDPClient - event *goob.Observable // all the browser events from cdp client - targetsLock *sync.Mutex - - // stores all the previous cdp call of same type. Browser doesn't have enough API - // for us to retrieve all its internal states. This is an workaround to map them to local. - // For example you can't use cdp API to get the current position of mouse. - states *sync.Map -} - -// New creates a controller. -// DefaultDevice to emulate is set to [devices.LaptopWithMDPIScreen].Landscape(), it will change the default -// user-agent and can make the actual view area smaller than the browser window on headful mode, -// you can use [Browser.NoDefaultDevice] to disable it. -func New() *Browser { - return (&Browser{ - ctx: context.Background(), - sleeper: DefaultSleeper, - controlURL: defaults.URL, - slowMotion: defaults.Slow, - trace: defaults.Trace, - monitor: defaults.Monitor, - logger: DefaultLogger, - defaultDevice: devices.LaptopWithMDPIScreen.Landscape(), - targetsLock: &sync.Mutex{}, - states: &sync.Map{}, - }).WithPanic(utils.Panic) -} - -// Incognito creates a new incognito browser. -func (b *Browser) Incognito() (*Browser, error) { - res, err := proto.TargetCreateBrowserContext{}.Call(b) - if err != nil { - return nil, err - } - - incognito := *b - incognito.BrowserContextID = res.BrowserContextID - - return &incognito, nil -} - -// ControlURL set the url to remote control browser. -func (b *Browser) ControlURL(url string) *Browser { - b.controlURL = url - return b -} - -// SlowMotion set the delay for each control action, such as the simulation of the human inputs. -func (b *Browser) SlowMotion(delay time.Duration) *Browser { - b.slowMotion = delay - return b -} - -// Trace enables/disables the visual tracing of the input actions on the page. -func (b *Browser) Trace(enable bool) *Browser { - b.trace = enable - return b -} - -// Monitor address to listen if not empty. Shortcut for [Browser.ServeMonitor]. -func (b *Browser) Monitor(url string) *Browser { - b.monitor = url - return b -} - -// Logger overrides the default log functions for tracing. -func (b *Browser) Logger(l utils.Logger) *Browser { - b.logger = l - return b -} - -// Client set the cdp client. -func (b *Browser) Client(c CDPClient) *Browser { - b.client = c - return b -} - -// DefaultDevice sets the default device for new page to emulate in the future. -// Default is [devices.LaptopWithMDPIScreen]. -// Set it to [devices.Clear] to disable it. -func (b *Browser) DefaultDevice(d devices.Device) *Browser { - b.defaultDevice = d - return b -} - -// NoDefaultDevice is the same as [Browser.DefaultDevice](devices.Clear). -func (b *Browser) NoDefaultDevice() *Browser { - return b.DefaultDevice(devices.Clear) -} - -// Connect to the browser and start to control it. kage launches Chrome before -// connecting, so a control URL is required. Keeping the automatic launcher out -// of this controller is what prevents Rod's embedded leakless helper from being -// linked into the Windows binary. -func (b *Browser) Connect() error { - if b.client == nil { - u := b.controlURL - if u == "" { - return errors.New("browser control URL is required") - } - - c, err := cdp.StartWithURL(b.ctx, u, nil) - if err != nil { - return err - } - b.client = c - } else if b.controlURL != "" { - panic("Browser.Client and Browser.ControlURL can't be set at the same time") - } - - b.initEvents() - - if b.monitor != "" { - return errors.New("browser monitor is not supported by kage's controller") - } - - return proto.TargetSetDiscoverTargets{Discover: true}.Call(b) -} - -// Close the browser. -func (b *Browser) Close() error { - if b.BrowserContextID == "" { - return proto.BrowserClose{}.Call(b) - } - return proto.TargetDisposeBrowserContext{BrowserContextID: b.BrowserContextID}.Call(b) -} - -// Page creates a new browser tab. If opts.URL is empty, the default target will be "about:blank". -func (b *Browser) Page(opts proto.TargetCreateTarget) (p *Page, err error) { - req := opts - req.BrowserContextID = b.BrowserContextID - req.URL = "about:blank" - - target, err := req.Call(b) - if err != nil { - return nil, err - } - defer func() { - // If Navigate or PageFromTarget fails we should close the target to prevent leak - if err != nil { - _, _ = proto.TargetCloseTarget{TargetID: target.TargetID}.Call(b) - } - }() - - p, err = b.PageFromTarget(target.TargetID) - if err != nil { - return - } - - if opts.URL == "" { - return - } - - err = p.Navigate(opts.URL) - - return -} - -// Pages retrieves all visible pages. -func (b *Browser) Pages() (Pages, error) { - list, err := proto.TargetGetTargets{}.Call(b) - if err != nil { - return nil, err - } - - pageList := Pages{} - for _, target := range list.TargetInfos { - if target.Type != proto.TargetTargetInfoTypePage { - continue - } - - page, err := b.PageFromTarget(target.TargetID) - if err != nil { - return nil, err - } - pageList = append(pageList, page) - } - - return pageList, nil -} - -// Call implements the [proto.Client] to call raw cdp interface directly. -func (b *Browser) Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) { - res, err = b.client.Call(ctx, sessionID, methodName, params) - if err != nil { - return nil, err - } - - b.set(proto.TargetSessionID(sessionID), methodName, params) - return -} - -// PageFromSession is used for low-level debugging. -func (b *Browser) PageFromSession(sessionID proto.TargetSessionID) *Page { - sessionCtx, cancel := context.WithCancel(b.ctx) - return &Page{ - e: b.e, - ctx: sessionCtx, - sessionCancel: cancel, - sleeper: b.sleeper, - browser: b, - SessionID: sessionID, - } -} - -// PageFromTarget gets or creates a Page instance. -func (b *Browser) PageFromTarget(targetID proto.TargetTargetID) (*Page, error) { - b.targetsLock.Lock() - defer b.targetsLock.Unlock() - - page := b.loadCachedPage(targetID) - if page != nil { - return page, nil - } - - session, err := proto.TargetAttachToTarget{ - TargetID: targetID, - Flatten: true, // if it's not set no response will return - }.Call(b) - if err != nil { - return nil, err - } - - sessionCtx, cancel := context.WithCancel(b.ctx) - - page = &Page{ - e: b.e, - ctx: sessionCtx, - sessionCancel: cancel, - sleeper: b.sleeper, - browser: b, - TargetID: targetID, - SessionID: session.SessionID, - FrameID: proto.PageFrameID(targetID), - jsCtxLock: &sync.Mutex{}, - jsCtxID: new(proto.RuntimeRemoteObjectID), - helpersLock: &sync.Mutex{}, - } - - page.root = page - page.newKeyboard().newMouse().newTouch() - - if !b.defaultDevice.IsClear() { - err = page.Emulate(b.defaultDevice) - if err != nil { - return nil, err - } - } - - b.cachePage(page) - - page.initEvents() - - // If we don't enable it, it will cause a lot of unexpected browser behavior. - // Such as proto.PageAddScriptToEvaluateOnNewDocument won't work. - page.EnableDomain(&proto.PageEnable{}) - - return page, nil -} - -// EachEvent is similar to [Page.EachEvent], but catches events of the entire browser. -func (b *Browser) EachEvent(callbacks ...interface{}) (wait func()) { - return b.eachEvent("", callbacks...) -} - -// WaitEvent waits for the next event for one time. It will also load the data into the event object. -func (b *Browser) WaitEvent(e proto.Event) (wait func()) { - return b.waitEvent("", e) -} - -// waits for the next event for one time. It will also load the data into the event object. -func (b *Browser) waitEvent(sessionID proto.TargetSessionID, e proto.Event) (wait func()) { - valE := reflect.ValueOf(e) - valTrue := reflect.ValueOf(true) - - if valE.Kind() != reflect.Pointer { - valE = reflect.New(valE.Type()) - } - - // dynamically creates a function on runtime: - // - // func(ee proto.Event) bool { - // *e = *ee - // return true - // } - fnType := reflect.FuncOf([]reflect.Type{valE.Type()}, []reflect.Type{valTrue.Type()}, false) - fnVal := reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value { - valE.Elem().Set(args[0].Elem()) - return []reflect.Value{valTrue} - }) - - return b.eachEvent(sessionID, fnVal.Interface()) -} - -// If the any callback returns true the event loop will stop. -// It will enable the related domains if not enabled, and restore them after wait ends. -func (b *Browser) eachEvent(sessionID proto.TargetSessionID, callbacks ...interface{}) (wait func()) { - cbMap := map[string]reflect.Value{} - restores := []func(){} - - for _, cb := range callbacks { - cbVal := reflect.ValueOf(cb) - eType := cbVal.Type().In(0) - name := reflect.New(eType.Elem()).Interface().(proto.Event).ProtoEvent() //nolint: forcetypeassert - cbMap[name] = cbVal - - // Only enabled domains will emit events to cdp client. - // We enable the domains for the event types if it's not enabled. - // We restore the domains to their previous states after the wait ends. - domain, _ := proto.ParseMethodName(name) - if req := proto.GetType(domain + ".enable"); req != nil { - enable := reflect.New(req).Interface().(proto.Request) //nolint: forcetypeassert - restores = append(restores, b.EnableDomain(sessionID, enable)) - } - } - - b, cancel := b.WithCancel() - messages := b.Event() - - return func() { - if messages == nil { - panic("can't use wait function twice") - } - - defer func() { - cancel() - messages = nil - for _, restore := range restores { - restore() - } - }() - - for msg := range messages { - if sessionID != "" && msg.SessionID != sessionID { - continue - } - - if cbVal, has := cbMap[msg.Method]; has { - e := reflect.New(proto.GetType(msg.Method)) - msg.Load(e.Interface().(proto.Event)) //nolint: forcetypeassert - args := []reflect.Value{e} - if cbVal.Type().NumIn() == 2 { - args = append(args, reflect.ValueOf(msg.SessionID)) - } - res := cbVal.Call(args) - if len(res) > 0 { - if res[0].Bool() { - return - } - } - } - } - } -} - -// Event of the browser. -func (b *Browser) Event() <-chan *Message { - src := b.event.Subscribe(b.ctx) - dst := make(chan *Message) - go func() { - defer close(dst) - for { - select { - case <-b.ctx.Done(): - return - case e, ok := <-src: - if !ok { - return - } - select { - case <-b.ctx.Done(): - return - case dst <- e.(*Message): //nolint: forcetypeassert - } - } - } - }() - return dst -} - -func (b *Browser) initEvents() { - ctx, cancel := context.WithCancel(b.ctx) - b.event = goob.New(ctx) - event := b.client.Event() - - go func() { - defer cancel() - for e := range event { - b.event.Publish(&Message{ - SessionID: proto.TargetSessionID(e.SessionID), - Method: e.Method, - lock: &sync.Mutex{}, - data: e.Params, - }) - } - }() -} - -func (b *Browser) pageInfo(id proto.TargetTargetID) (*proto.TargetTargetInfo, error) { - res, err := proto.TargetGetTargetInfo{TargetID: id}.Call(b) - if err != nil { - return nil, err - } - return res.TargetInfo, nil -} - -func (b *Browser) isHeadless() (enabled bool) { - res, _ := proto.BrowserGetBrowserCommandLine{}.Call(b) - for _, v := range res.Arguments { - if strings.Contains(v, "headless") { - return true - } - } - return false -} - -// IgnoreCertErrors switch. If enabled, all certificate errors will be ignored. -func (b *Browser) IgnoreCertErrors(enable bool) error { - return proto.SecuritySetIgnoreCertificateErrors{Ignore: enable}.Call(b) -} - -// GetCookies from the browser. -func (b *Browser) GetCookies() ([]*proto.NetworkCookie, error) { - res, err := proto.StorageGetCookies{BrowserContextID: b.BrowserContextID}.Call(b) - if err != nil { - return nil, err - } - return res.Cookies, nil -} - -// SetCookies to the browser. If the cookies is nil it will clear all the cookies. -func (b *Browser) SetCookies(cookies []*proto.NetworkCookieParam) error { - if cookies == nil { - return proto.StorageClearCookies{BrowserContextID: b.BrowserContextID}.Call(b) - } - - return proto.StorageSetCookies{ - Cookies: cookies, - BrowserContextID: b.BrowserContextID, - }.Call(b) -} - -// WaitDownload returns a helper to get the next download file. -// The file path will be: -// -// filepath.Join(dir, info.GUID) -func (b *Browser) WaitDownload(dir string) func() (info *proto.PageDownloadWillBegin) { - var oldDownloadBehavior proto.BrowserSetDownloadBehavior - has := b.LoadState("", &oldDownloadBehavior) - - _ = proto.BrowserSetDownloadBehavior{ - Behavior: proto.BrowserSetDownloadBehaviorBehaviorAllowAndName, - BrowserContextID: b.BrowserContextID, - DownloadPath: dir, - }.Call(b) - - var start *proto.PageDownloadWillBegin - - waitProgress := b.EachEvent(func(e *proto.PageDownloadWillBegin) { - start = e - }, func(e *proto.PageDownloadProgress) bool { - return start != nil && start.GUID == e.GUID && e.State == proto.PageDownloadProgressStateCompleted - }) - - return func() *proto.PageDownloadWillBegin { - defer func() { - if has { - _ = oldDownloadBehavior.Call(b) - } else { - _ = proto.BrowserSetDownloadBehavior{ - Behavior: proto.BrowserSetDownloadBehaviorBehaviorDefault, - BrowserContextID: b.BrowserContextID, - }.Call(b) - } - }() - - waitProgress() - - return start - } -} - -// Version info of the browser. -func (b *Browser) Version() (*proto.BrowserGetVersionResult, error) { - return proto.BrowserGetVersion{}.Call(b) -} diff --git a/internal/rod/context.go b/internal/rod/context.go deleted file mode 100644 index b770695..0000000 --- a/internal/rod/context.go +++ /dev/null @@ -1,132 +0,0 @@ -package rod - -import ( - "context" - "time" - - "github.com/go-rod/rod/lib/utils" -) - -type ( - timeoutContextKey struct{} - timeoutContextVal struct { - parent context.Context - cancel context.CancelFunc - } -) - -// Context returns a clone with the specified ctx for chained sub-operations. -func (b *Browser) Context(ctx context.Context) *Browser { - newObj := *b - newObj.ctx = ctx - return &newObj -} - -// GetContext of current instance. -func (b *Browser) GetContext() context.Context { - return b.ctx -} - -// Timeout returns a clone with the specified total timeout of all chained sub-operations. -func (b *Browser) Timeout(d time.Duration) *Browser { - ctx, cancel := context.WithTimeout(b.ctx, d) - return b.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{b.ctx, cancel})) -} - -// CancelTimeout cancels the current timeout context and returns a clone with the parent context. -func (b *Browser) CancelTimeout() *Browser { - val := b.ctx.Value(timeoutContextKey{}).(*timeoutContextVal) //nolint:forcetypeassert - val.cancel() - return b.Context(val.parent) -} - -// WithCancel returns a clone with a context cancel function. -func (b *Browser) WithCancel() (*Browser, func()) { - ctx, cancel := context.WithCancel(b.ctx) - return b.Context(ctx), cancel -} - -// Sleeper returns a clone with the specified sleeper for chained sub-operations. -func (b *Browser) Sleeper(sleeper func() utils.Sleeper) *Browser { - newObj := *b - newObj.sleeper = sleeper - return &newObj -} - -// Context returns a clone with the specified ctx for chained sub-operations. -func (p *Page) Context(ctx context.Context) *Page { - p.helpersLock.Lock() - newObj := *p - p.helpersLock.Unlock() - newObj.ctx = ctx - return &newObj -} - -// GetContext of current instance. -func (p *Page) GetContext() context.Context { - return p.ctx -} - -// Timeout returns a clone with the specified total timeout of all chained sub-operations. -func (p *Page) Timeout(d time.Duration) *Page { - ctx, cancel := context.WithTimeout(p.ctx, d) - return p.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{p.ctx, cancel})) -} - -// CancelTimeout cancels the current timeout context and returns a clone with the parent context. -func (p *Page) CancelTimeout() *Page { - val := p.ctx.Value(timeoutContextKey{}).(*timeoutContextVal) //nolint: forcetypeassert - val.cancel() - return p.Context(val.parent) -} - -// WithCancel returns a clone with a context cancel function. -func (p *Page) WithCancel() (*Page, func()) { - ctx, cancel := context.WithCancel(p.ctx) - return p.Context(ctx), cancel -} - -// Sleeper returns a clone with the specified sleeper for chained sub-operations. -func (p *Page) Sleeper(sleeper func() utils.Sleeper) *Page { - newObj := *p - newObj.sleeper = sleeper - return &newObj -} - -// Context returns a clone with the specified ctx for chained sub-operations. -func (el *Element) Context(ctx context.Context) *Element { - newObj := *el - newObj.ctx = ctx - return &newObj -} - -// GetContext of current instance. -func (el *Element) GetContext() context.Context { - return el.ctx -} - -// Timeout returns a clone with the specified total timeout of all chained sub-operations. -func (el *Element) Timeout(d time.Duration) *Element { - ctx, cancel := context.WithTimeout(el.ctx, d) - return el.Context(context.WithValue(ctx, timeoutContextKey{}, &timeoutContextVal{el.ctx, cancel})) -} - -// CancelTimeout cancels the current timeout context and returns a clone with the parent context. -func (el *Element) CancelTimeout() *Element { - val := el.ctx.Value(timeoutContextKey{}).(*timeoutContextVal) //nolint: forcetypeassert - val.cancel() - return el.Context(val.parent) -} - -// WithCancel returns a clone with a context cancel function. -func (el *Element) WithCancel() (*Element, func()) { - ctx, cancel := context.WithCancel(el.ctx) - return el.Context(ctx), cancel -} - -// Sleeper returns a clone with the specified sleeper for chained sub-operations. -func (el *Element) Sleeper(sleeper func() utils.Sleeper) *Element { - newObj := *el - newObj.sleeper = sleeper - return &newObj -} diff --git a/internal/rod/dev_helpers.go b/internal/rod/dev_helpers.go deleted file mode 100644 index c452756..0000000 --- a/internal/rod/dev_helpers.go +++ /dev/null @@ -1,264 +0,0 @@ -// This file defines the helpers to develop automation. -// Such as when running automation we can use trace to visually -// see where the mouse going to click. - -package rod - -import ( - "encoding/json" - "fmt" - "html" - "net" - "net/http" - "strings" - "time" - - "github.com/go-rod/rod/lib/assets" - "github.com/go-rod/rod/lib/js" - "github.com/go-rod/rod/lib/proto" - "github.com/go-rod/rod/lib/utils" -) - -// TraceType for logger. -type TraceType string - -// String interface. -func (t TraceType) String() string { - return fmt.Sprintf("[%s]", string(t)) -} - -const ( - // TraceTypeWaitRequestsIdle type. - TraceTypeWaitRequestsIdle TraceType = "wait requests idle" - - // TraceTypeWaitRequests type. - TraceTypeWaitRequests TraceType = "wait requests" - - // TraceTypeQuery type. - TraceTypeQuery TraceType = "query" - - // TraceTypeWait type. - TraceTypeWait TraceType = "wait" - - // TraceTypeInput type. - TraceTypeInput TraceType = "input" -) - -// ServeMonitor starts the monitor server. -// The reason why not to use "chrome://inspect/#devices" is one target cannot be driven by multiple controllers. -func (b *Browser) ServeMonitor(host string) string { - u, mux, closeSvr := serve(host) - go func() { - <-b.ctx.Done() - utils.E(closeSvr()) - }() - - mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { - httHTML(w, assets.Monitor) - }) - mux.HandleFunc("/api/pages", func(w http.ResponseWriter, _ *http.Request) { - res, err := proto.TargetGetTargets{}.Call(b) //nolint: contextcheck - utils.E(err) - - list := []*proto.TargetTargetInfo{} - for _, info := range res.TargetInfos { - if info.Type == proto.TargetTargetInfoTypePage { - list = append(list, info) - } - } - - w.WriteHeader(http.StatusOK) - utils.E(w.Write(utils.MustToJSONBytes(list))) - }) - mux.HandleFunc("/page/", func(w http.ResponseWriter, _ *http.Request) { - httHTML(w, assets.MonitorPage) - }) - mux.HandleFunc("/api/page/", func(w http.ResponseWriter, r *http.Request) { - id := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] - info, err := b.pageInfo(proto.TargetTargetID(id)) //nolint: contextcheck - utils.E(err) - w.WriteHeader(http.StatusOK) - utils.E(w.Write(utils.MustToJSONBytes(info))) - }) - mux.HandleFunc("/screenshot/", func(w http.ResponseWriter, r *http.Request) { - id := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] - target := proto.TargetTargetID(id) - p := b.MustPageFromTargetID(target) - - w.Header().Add("Content-Type", "image/png;") - utils.E(w.Write(p.MustScreenshot())) //nolint: contextcheck - }) - - return u -} - -// check method and sleep if needed. -func (b *Browser) trySlowMotion() { - if b.slowMotion == 0 { - return - } - - time.Sleep(b.slowMotion) -} - -// ExposeHelpers helper functions to page's js context so that we can use the Devtools' console to debug them. -func (p *Page) ExposeHelpers(list ...*js.Function) { - p.MustEvaluate(evalHelper(&js.Function{ - Name: "_" + utils.RandString(8), // use a random name so it won't hit the cache - Definition: "() => { window.rod = functions }", - Dependencies: list, - })) -} - -// Overlay a rectangle on the main frame with specified message. -func (p *Page) Overlay(left, top, width, height float64, msg string) (remove func()) { - id := utils.RandString(8) - - _, _ = p.root.Evaluate(evalHelper(js.Overlay, - id, - left, - top, - width, - height, - msg, - ).ByPromise()) - - remove = func() { - _, _ = p.root.Evaluate(evalHelper(js.RemoveOverlay, id)) - } - - return -} - -func (p *Page) tryTrace(typ TraceType, msg ...interface{}) func() { - if !p.browser.trace { - return func() {} - } - - msg = append([]interface{}{typ}, msg...) - msg = append(msg, p) - - p.browser.logger.Println(msg...) - - return p.Overlay(0, 0, 500, 0, fmt.Sprint(msg)) -} - -func (p *Page) tryTraceQuery(opts *EvalOptions) func() { - if !p.browser.trace { - return func() {} - } - - p.browser.logger.Println(TraceTypeQuery, opts, p) - - msg := fmt.Sprintf("%s", html.EscapeString(opts.String())) - return p.Overlay(0, 0, 500, 0, msg) -} - -func (p *Page) tryTraceReq(includes, excludes []string) func(map[proto.NetworkRequestID]string) { - if !p.browser.trace { - return func(map[proto.NetworkRequestID]string) {} - } - - msg := map[string][]string{ - "includes": includes, - "excludes": excludes, - } - p.browser.logger.Println(TraceTypeWaitRequestsIdle, msg, p) - cleanup := p.Overlay(0, 0, 500, 0, utils.MustToJSON(msg)) - - ch := make(chan map[string]string) - update := func(list map[proto.NetworkRequestID]string) { - clone := map[string]string{} - for k, v := range list { - clone[string(k)] = v - } - ch <- clone - } - - go func() { - var waitList map[string]string - t := time.NewTicker(time.Second) - for { - select { - case <-p.ctx.Done(): - t.Stop() - cleanup() - return - case waitList = <-ch: - case <-t.C: - p.browser.logger.Println(TraceTypeWaitRequests, p, waitList) - } - } - }() - - return update -} - -// Overlay msg on the element. -func (el *Element) Overlay(msg string) (removeOverlay func()) { - id := utils.RandString(8) - - _, _ = el.Evaluate(evalHelper(js.ElementOverlay, - id, - msg, - ).ByPromise()) - - removeOverlay = func() { - _, _ = el.Evaluate(evalHelper(js.RemoveOverlay, id)) - } - - return -} - -func (el *Element) tryTrace(typ TraceType, msg ...interface{}) func() { - if !el.page.browser.trace { - return func() {} - } - - msg = append([]interface{}{typ}, msg...) - msg = append(msg, el) - - el.page.browser.logger.Println(msg...) - - return el.Overlay(fmt.Sprint(msg)) -} - -func (m *Mouse) initMouseTracer() { - _, _ = m.page.Evaluate(evalHelper(js.InitMouseTracer, m.id, assets.MousePointer).ByPromise()) -} - -func (m *Mouse) updateMouseTracer() bool { - res, err := m.page.Evaluate(evalHelper(js.UpdateMouseTracer, m.id, m.pos.X, m.pos.Y)) - if err != nil { - return true - } - return res.Value.Bool() -} - -// Serve a port, if host is empty a random port will be used. -func serve(host string) (string, *http.ServeMux, func() error) { - if host == "" { - host = "127.0.0.1:0" - } - - mux := http.NewServeMux() - srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if err := recover(); err != nil { - w.WriteHeader(http.StatusBadRequest) - utils.E(json.NewEncoder(w).Encode(err)) - } - }() - - mux.ServeHTTP(w, r) - })} - - l, err := net.Listen("tcp", host) - utils.E(err) - - go func() { _ = srv.Serve(l) }() - - url := "http://" + l.Addr().String() - - return url, mux, srv.Close -} diff --git a/internal/rod/element.go b/internal/rod/element.go deleted file mode 100644 index 97fceed..0000000 --- a/internal/rod/element.go +++ /dev/null @@ -1,754 +0,0 @@ -package rod - -import ( - "context" - "errors" - "fmt" - "reflect" - "strings" - "time" - - "github.com/go-rod/rod/lib/cdp" - "github.com/go-rod/rod/lib/input" - "github.com/go-rod/rod/lib/js" - "github.com/go-rod/rod/lib/proto" - "github.com/go-rod/rod/lib/utils" - "github.com/ysmood/gson" -) - -// Element implements these interfaces. -var ( - _ proto.Client = &Element{} - _ proto.Contextable = &Element{} - _ proto.Sessionable = &Element{} -) - -// Element represents the DOM element. -type Element struct { - Object *proto.RuntimeRemoteObject - - e eFunc - - ctx context.Context - - sleeper func() utils.Sleeper - - page *Page -} - -// GetSessionID interface. -func (el *Element) GetSessionID() proto.TargetSessionID { - return el.page.SessionID -} - -// String interface. -func (el *Element) String() string { - return fmt.Sprintf("<%s>", el.Object.Description) -} - -// Page of the element. -func (el *Element) Page() *Page { - return el.page -} - -// Focus sets focus on the specified element. -// Before the action, it will try to scroll to the element. -func (el *Element) Focus() error { - err := el.ScrollIntoView() - if err != nil { - return err - } - - _, err = el.Evaluate(Eval(`() => this.focus()`).ByUser()) - return err -} - -// ScrollIntoView scrolls the current element into the visible area of the browser -// window if it's not already within the visible area. -func (el *Element) ScrollIntoView() error { - defer el.tryTrace(TraceTypeInput, "scroll into view")() - el.page.browser.trySlowMotion() - - err := el.WaitStableRAF() - if err != nil { - return err - } - - return proto.DOMScrollIntoViewIfNeeded{ObjectID: el.id()}.Call(el) -} - -// Hover the mouse over the center of the element. -// Before the action, it will try to scroll to the element and wait until it's interactable. -func (el *Element) Hover() error { - pt, err := el.WaitInteractable() - if err != nil { - return err - } - - return el.page.Context(el.ctx).Mouse.MoveTo(*pt) -} - -// MoveMouseOut of the current element. -func (el *Element) MoveMouseOut() error { - shape, err := el.Shape() - if err != nil { - return err - } - box := shape.Box() - return el.page.Mouse.MoveTo(proto.NewPoint(box.X+box.Width, box.Y)) -} - -// Click will press then release the button just like a human. -// Before the action, it will try to scroll to the element, hover the mouse over it, -// wait until the it's interactable and enabled. -func (el *Element) Click(button proto.InputMouseButton, clickCount int) error { - err := el.Hover() - if err != nil { - return err - } - - err = el.WaitEnabled() - if err != nil { - return err - } - - defer el.tryTrace(TraceTypeInput, string(button)+" click")() - - return el.page.Context(el.ctx).Mouse.Click(button, clickCount) -} - -// Tap will scroll to the button and tap it just like a human. -// Before the action, it will try to scroll to the element and wait until it's interactable and enabled. -func (el *Element) Tap() error { - err := el.ScrollIntoView() - if err != nil { - return err - } - - err = el.WaitEnabled() - if err != nil { - return err - } - - pt, err := el.WaitInteractable() - if err != nil { - return err - } - - defer el.tryTrace(TraceTypeInput, "tap")() - - return el.page.Context(el.ctx).Touch.Tap(pt.X, pt.Y) -} - -// Interactable checks if the element is interactable with cursor. -// The cursor can be mouse, finger, stylus, etc. -// If not interactable err will be ErrNotInteractable, such as when covered by a modal,. -func (el *Element) Interactable() (pt *proto.Point, err error) { - noPointerEvents, err := el.Eval(`() => getComputedStyle(this).pointerEvents === 'none'`) - if err != nil { - return nil, err - } - - if noPointerEvents.Value.Bool() { - return nil, &NoPointerEventsError{el} - } - - shape, err := el.Shape() - if err != nil { - return nil, err - } - - pt = shape.OnePointInside() - if pt == nil { - err = &InvisibleShapeError{el} - return - } - - scroll, err := el.page.root.Context(el.ctx).Eval(`() => ({ x: window.scrollX, y: window.scrollY })`) - if err != nil { - return - } - - elAtPoint, err := el.page.Context(el.ctx).ElementFromPoint( - int(pt.X)+scroll.Value.Get("x").Int(), - int(pt.Y)+scroll.Value.Get("y").Int(), - ) - if err != nil { - if errors.Is(err, cdp.ErrNodeNotFoundAtPos) { - err = &InvisibleShapeError{el} - } - return - } - - isParent, err := el.ContainsElement(elAtPoint) - if err != nil { - return - } - - if !isParent { - err = &CoveredError{elAtPoint} - } - return -} - -// Shape of the DOM element content. The shape is a group of 4-sides polygons. -// A 4-sides polygon is not necessary a rectangle. 4-sides polygons can be apart from each other. -// For example, we use 2 4-sides polygons to describe the shape below: -// -// ____________ ____________ -// / ___/ = /___________/ + _________ -// /________/ /________/ -func (el *Element) Shape() (*proto.DOMGetContentQuadsResult, error) { - return proto.DOMGetContentQuads{ObjectID: el.id()}.Call(el) -} - -// Type is similar with Keyboard.Type. -// Before the action, it will try to scroll to the element and focus on it. -func (el *Element) Type(keys ...input.Key) error { - err := el.Focus() - if err != nil { - return err - } - return el.page.Context(el.ctx).Keyboard.Type(keys...) -} - -// KeyActions is similar with Page.KeyActions. -// Before the action, it will try to scroll to the element and focus on it. -func (el *Element) KeyActions() (*KeyActions, error) { - err := el.Focus() - if err != nil { - return nil, err - } - - return el.page.Context(el.ctx).KeyActions(), nil -} - -// SelectText selects the text that matches the regular expression. -// Before the action, it will try to scroll to the element and focus on it. -func (el *Element) SelectText(regex string) error { - err := el.Focus() - if err != nil { - return err - } - - defer el.tryTrace(TraceTypeInput, "select text: "+regex)() - el.page.browser.trySlowMotion() - - _, err = el.Evaluate(evalHelper(js.SelectText, regex).ByUser()) - return err -} - -// SelectAllText selects all text -// Before the action, it will try to scroll to the element and focus on it. -func (el *Element) SelectAllText() error { - err := el.Focus() - if err != nil { - return err - } - - defer el.tryTrace(TraceTypeInput, "select all text")() - el.page.browser.trySlowMotion() - - _, err = el.Evaluate(evalHelper(js.SelectAllText).ByUser()) - return err -} - -// Input focuses on the element and input text to it. -// Before the action, it will scroll to the element, wait until it's visible, enabled and writable. -// To empty the input you can use something like -// -// el.SelectAllText().MustInput("") -func (el *Element) Input(text string) error { - err := el.Focus() - if err != nil { - return err - } - - err = el.WaitEnabled() - if err != nil { - return err - } - - err = el.WaitWritable() - if err != nil { - return err - } - - err = el.page.Context(el.ctx).InsertText(text) - _, _ = el.Evaluate(evalHelper(js.InputEvent).ByUser()) - return err -} - -// InputTime focuses on the element and input time to it. -// Before the action, it will scroll to the element, wait until it's visible, enabled and writable. -// It will wait until the element is visible, enabled and writable. -func (el *Element) InputTime(t time.Time) error { - err := el.Focus() - if err != nil { - return err - } - - err = el.WaitEnabled() - if err != nil { - return err - } - - err = el.WaitWritable() - if err != nil { - return err - } - - defer el.tryTrace(TraceTypeInput, "input "+t.String())() - - _, err = el.Evaluate(evalHelper(js.InputTime, t.UnixNano()/1e6).ByUser()) - return err -} - -// InputColor focuses on the element and inputs a color string to it. -// Before the action, it will scroll to the element, wait until it's visible, enabled and writable. -func (el *Element) InputColor(color string) error { - err := el.Focus() - if err != nil { - return err - } - - err = el.WaitEnabled() - if err != nil { - return err - } - - err = el.WaitWritable() - if err != nil { - return err - } - - defer el.tryTrace(TraceTypeInput, "input "+color)() - - _, err = el.Evaluate(evalHelper(js.InputColor, color)) - return err -} - -// Blur removes focus from the element. -func (el *Element) Blur() error { - _, err := el.Evaluate(Eval("() => this.blur()").ByUser()) - return err -} - -// Select the children option elements that match the selectors. -// Before the action, it will scroll to the element, wait until it's visible. -// If no option matches the selectors, it will return [ErrElementNotFound]. -func (el *Element) Select(selectors []string, selected bool, t SelectorType) error { - err := el.Focus() - if err != nil { - return err - } - - defer el.tryTrace(TraceTypeInput, fmt.Sprintf(`select "%s"`, strings.Join(selectors, "; ")))() - el.page.browser.trySlowMotion() - - res, err := el.Evaluate(evalHelper(js.Select, selectors, selected, t).ByUser()) - if err != nil { - return err - } - if !res.Value.Bool() { - return &ElementNotFoundError{} - } - return nil -} - -// Matches checks if the element can be selected by the css selector. -func (el *Element) Matches(selector string) (bool, error) { - res, err := el.Eval(`s => this.matches(s)`, selector) - if err != nil { - return false, err - } - return res.Value.Bool(), nil -} - -// Attribute of the DOM object. -// Attribute vs Property: -// https://stackoverflow.com/questions/6003819/what-is-the-difference-between-properties-and-attributes-in-html -func (el *Element) Attribute(name string) (*string, error) { - attr, err := el.Eval("(n) => this.getAttribute(n)", name) - if err != nil { - return nil, err - } - - if attr.Value.Nil() { - return nil, nil //nolint: nilnil - } - - s := attr.Value.Str() - return &s, nil -} - -// Property of the DOM object. -// Property vs Attribute: -// https://stackoverflow.com/questions/6003819/what-is-the-difference-between-properties-and-attributes-in-html -func (el *Element) Property(name string) (gson.JSON, error) { - prop, err := el.Eval("(n) => this[n]", name) - if err != nil { - return gson.New(nil), err - } - - return prop.Value, nil -} - -// Disabled checks if the element is disabled. -func (el *Element) Disabled() (bool, error) { - prop, err := el.Property("disabled") - if err != nil { - return false, err - } - return prop.Bool(), nil -} - -// SetFiles of the current file input element. -func (el *Element) SetFiles(paths []string) error { - absPaths := utils.AbsolutePaths(paths) - - defer el.tryTrace(TraceTypeInput, fmt.Sprintf("set files: %v", absPaths))() - el.page.browser.trySlowMotion() - - err := proto.DOMSetFileInputFiles{ - Files: absPaths, - ObjectID: el.id(), - }.Call(el) - - return err -} - -// Describe the current element. The depth is the maximum depth at which children should be retrieved, defaults to 1, -// use -1 for the entire subtree or provide an integer larger than 0. -// The pierce decides whether or not iframes and shadow roots should be traversed when returning the subtree. -// The returned [proto.DOMNode.NodeID] will always be empty, -// because NodeID is not stable (when [proto.DOMDocumentUpdated] -// is fired all NodeID on the page will be reassigned to another value) -// we don't recommend using the NodeID, instead, use the [proto.DOMBackendNodeID] to identify the element. -func (el *Element) Describe(depth int, pierce bool) (*proto.DOMNode, error) { - val, err := proto.DOMDescribeNode{ObjectID: el.id(), Depth: gson.Int(depth), Pierce: pierce}.Call(el) - if err != nil { - return nil, err - } - return val.Node, nil -} - -// ShadowRoot returns the shadow root of this element. -func (el *Element) ShadowRoot() (*Element, error) { - node, err := el.Describe(1, false) - if err != nil { - return nil, err - } - - // though now it's an array, w3c changed the spec of it to be a single. - if len(node.ShadowRoots) == 0 { - return nil, &NoShadowRootError{el} - } - id := node.ShadowRoots[0].BackendNodeID - - shadowNode, err := proto.DOMResolveNode{BackendNodeID: id}.Call(el) - if err != nil { - return nil, err - } - - return el.page.Context(el.ctx).ElementFromObject(shadowNode.Object) -} - -// Frame creates a page instance that represents the iframe. -func (el *Element) Frame() (*Page, error) { - node, err := el.Describe(1, false) - if err != nil { - return nil, err - } - - clone := *el.page - clone.FrameID = node.FrameID - clone.jsCtxID = new(proto.RuntimeRemoteObjectID) - clone.element = el - clone.sleeper = el.sleeper - - return &clone, nil -} - -// ContainsElement check if the target is equal or inside the element. -func (el *Element) ContainsElement(target *Element) (bool, error) { - res, err := el.Evaluate(evalHelper(js.ContainsElement, target.Object)) - if err != nil { - return false, err - } - return res.Value.Bool(), nil -} - -// Text that the element displays. -func (el *Element) Text() (string, error) { - str, err := el.Evaluate(evalHelper(js.Text)) - if err != nil { - return "", err - } - return str.Value.String(), nil -} - -// HTML of the element. -func (el *Element) HTML() (string, error) { - res, err := proto.DOMGetOuterHTML{ObjectID: el.Object.ObjectID}.Call(el) - if err != nil { - return "", err - } - return res.OuterHTML, nil -} - -// Visible returns true if the element is visible on the page. -func (el *Element) Visible() (bool, error) { - res, err := el.Evaluate(evalHelper(js.Visible)) - if err != nil { - return false, err - } - return res.Value.Bool(), nil -} - -// WaitLoad for element like . -func (el *Element) WaitLoad() error { - defer el.tryTrace(TraceTypeWait, "load")() - _, err := el.Evaluate(evalHelper(js.WaitLoad).ByPromise()) - return err -} - -// WaitStable waits until no shape or position change for d duration. -// Be careful, d is not the max wait timeout, it's the least stable time. -// If you want to set a timeout you can use the [Element.Timeout] function. -func (el *Element) WaitStable(d time.Duration) error { - err := el.WaitVisible() - if err != nil { - return err - } - - defer el.tryTrace(TraceTypeWait, "stable")() - - shape, err := el.Shape() - if err != nil { - return err - } - - t := time.NewTicker(d) - defer t.Stop() - - for { - select { - case <-t.C: - case <-el.ctx.Done(): - return el.ctx.Err() - } - current, err := el.Shape() - if err != nil { - return err - } - if reflect.DeepEqual(shape, current) { - break - } - shape = current - } - return nil -} - -// WaitStableRAF waits until no shape or position change for 2 consecutive animation frames. -// If you want to wait animation that is triggered by JS not CSS, you'd better use [Element.WaitStable]. -// About animation frame: https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame -func (el *Element) WaitStableRAF() error { - err := el.WaitVisible() - if err != nil { - return err - } - - defer el.tryTrace(TraceTypeWait, "stable RAF")() - - var shape *proto.DOMGetContentQuadsResult - page := el.page.Context(el.ctx) - - for { - err = page.WaitRepaint() - if err != nil { - return err - } - - current, err := el.Shape() - if err != nil { - return err - } - if reflect.DeepEqual(shape, current) { - break - } - shape = current - } - return nil -} - -// WaitInteractable waits for the element to be interactable. -// It will try to scroll to the element on each try. -func (el *Element) WaitInteractable() (pt *proto.Point, err error) { - defer el.tryTrace(TraceTypeWait, "interactable")() - - err = utils.Retry(el.ctx, el.sleeper(), func() (bool, error) { - // For lazy loading page the element can be outside of the viewport. - // If we don't scroll to it, it will never be available. - err := el.ScrollIntoView() - if err != nil { - return true, err - } - - pt, err = el.Interactable() - if errors.Is(err, &CoveredError{}) { - return false, nil - } - return true, err - }) - return -} - -// Wait until the js returns true. -func (el *Element) Wait(opts *EvalOptions) error { - return el.page.Context(el.ctx).Sleeper(el.sleeper).Wait(opts.This(el.Object)) -} - -// WaitVisible until the element is visible. -func (el *Element) WaitVisible() error { - defer el.tryTrace(TraceTypeWait, "visible")() - return el.Wait(evalHelper(js.Visible)) -} - -// WaitEnabled until the element is not disabled. -// Doc for readonly: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/readonly -func (el *Element) WaitEnabled() error { - defer el.tryTrace(TraceTypeWait, "enabled")() - return el.Wait(Eval(`() => !this.disabled`)) -} - -// WaitWritable until the element is not readonly. -// Doc for disabled: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled -func (el *Element) WaitWritable() error { - defer el.tryTrace(TraceTypeWait, "writable")() - return el.Wait(Eval(`() => !this.readonly`)) -} - -// WaitInvisible until the element invisible. -func (el *Element) WaitInvisible() error { - defer el.tryTrace(TraceTypeWait, "invisible")() - return el.Wait(evalHelper(js.Invisible)) -} - -// CanvasToImage get image data of a canvas. -// The default format is image/png. -// The default quality is 0.92. -// doc: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL -func (el *Element) CanvasToImage(format string, quality float64) ([]byte, error) { - res, err := el.Eval(`(format, quality) => this.toDataURL(format, quality)`, format, quality) - if err != nil { - return nil, err - } - - _, bin := parseDataURI(res.Value.Str()) - return bin, nil -} - -// Resource returns the "src" content of current element. Such as the jpg of . -func (el *Element) Resource() ([]byte, error) { - src, err := el.Evaluate(evalHelper(js.Resource).ByPromise()) - if err != nil { - return nil, err - } - - return el.page.Context(el.ctx).GetResource(src.Value.String()) -} - -// BackgroundImage returns the css background-image of the element. -func (el *Element) BackgroundImage() ([]byte, error) { - res, err := el.Eval(`() => window.getComputedStyle(this).backgroundImage.replace(/^url\("/, '').replace(/"\)$/, '')`) - if err != nil { - return nil, err - } - - u := res.Value.Str() - - return el.page.Context(el.ctx).GetResource(u) -} - -// Screenshot of the area of the element. -func (el *Element) Screenshot(format proto.PageCaptureScreenshotFormat, quality int) ([]byte, error) { - err := el.ScrollIntoView() - if err != nil { - return nil, err - } - - opts := &proto.PageCaptureScreenshot{ - Quality: gson.Int(quality), - Format: format, - } - - bin, err := el.page.Context(el.ctx).Screenshot(false, opts) - if err != nil { - return nil, err - } - - // so that it won't clip the css-transformed element - shape, err := el.Shape() - if err != nil { - return nil, err - } - - box := shape.Box() - - // TODO: proto.PageCaptureScreenshot has a Clip option, but it's buggy, so now we do in Go. - return utils.CropImage(bin, quality, - int(box.X), - int(box.Y), - int(box.Width), - int(box.Height), - ) -} - -// Release is a shortcut for [Page.Release] current element. -func (el *Element) Release() error { - return el.page.Context(el.ctx).Release(el.Object) -} - -// Remove the element from the page. -func (el *Element) Remove() error { - _, err := el.Eval(`() => this.remove()`) - if err != nil { - return err - } - return el.Release() -} - -// Call implements the [proto.Client]. -func (el *Element) Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) { - return el.page.Call(ctx, sessionID, methodName, params) -} - -// Eval is a shortcut for [Element.Evaluate] with AwaitPromise, ByValue and AutoExp set to true. -func (el *Element) Eval(js string, params ...interface{}) (*proto.RuntimeRemoteObject, error) { - return el.Evaluate(Eval(js, params...).ByPromise()) -} - -// Evaluate is just a shortcut of [Page.Evaluate] with This set to current element. -func (el *Element) Evaluate(opts *EvalOptions) (*proto.RuntimeRemoteObject, error) { - return el.page.Context(el.ctx).Evaluate(opts.This(el.Object)) -} - -// Equal checks if the two elements are equal. -func (el *Element) Equal(elm *Element) (bool, error) { - res, err := el.Eval(`elm => this === elm`, elm.Object) - return res.Value.Bool(), err -} - -func (el *Element) id() proto.RuntimeRemoteObjectID { - return el.Object.ObjectID -} - -// GetXPath returns the xpath of the element. -func (el *Element) GetXPath(optimized bool) (string, error) { - str, err := el.Evaluate(evalHelper(js.GetXPath, optimized)) - if err != nil { - return "", err - } - return str.Value.String(), nil -} diff --git a/internal/rod/error.go b/internal/rod/error.go deleted file mode 100644 index c6a43c0..0000000 --- a/internal/rod/error.go +++ /dev/null @@ -1,193 +0,0 @@ -package rod - -import ( - "context" - "fmt" - - "github.com/go-rod/rod/lib/proto" - "github.com/go-rod/rod/lib/utils" -) - -// TryError error. -type TryError struct { - Value interface{} - Stack string -} - -func (e *TryError) Error() string { - return fmt.Sprintf("error value: %#v\n%s", e.Value, e.Stack) -} - -// Is interface. -func (e *TryError) Is(err error) bool { _, ok := err.(*TryError); return ok } - -// Unwrap stdlib interface. -func (e *TryError) Unwrap() error { - if err, ok := e.Value.(error); ok { - return err - } - return fmt.Errorf("%v", e.Value) -} - -// ExpectElementError error. -type ExpectElementError struct { - *proto.RuntimeRemoteObject -} - -func (e *ExpectElementError) Error() string { - return fmt.Sprintf("expect js to return an element, but got: %s", utils.MustToJSON(e)) -} - -// Is interface. -func (e *ExpectElementError) Is(err error) bool { _, ok := err.(*ExpectElementError); return ok } - -// ExpectElementsError error. -type ExpectElementsError struct { - *proto.RuntimeRemoteObject -} - -func (e *ExpectElementsError) Error() string { - return fmt.Sprintf("expect js to return an array of elements, but got: %s", utils.MustToJSON(e)) -} - -// Is interface. -func (e *ExpectElementsError) Is(err error) bool { _, ok := err.(*ExpectElementsError); return ok } - -// ElementNotFoundError error. -type ElementNotFoundError struct{} - -func (e *ElementNotFoundError) Error() string { - return "cannot find element" -} - -// NotFoundSleeper returns ErrElementNotFound on the first call. -func NotFoundSleeper() utils.Sleeper { - return func(context.Context) error { - return &ElementNotFoundError{} - } -} - -// ObjectNotFoundError error. -type ObjectNotFoundError struct { - *proto.RuntimeRemoteObject -} - -func (e *ObjectNotFoundError) Error() string { - return fmt.Sprintf("cannot find object: %s", utils.MustToJSON(e)) -} - -// Is interface. -func (e *ObjectNotFoundError) Is(err error) bool { _, ok := err.(*ObjectNotFoundError); return ok } - -// EvalError error. -type EvalError struct { - *proto.RuntimeExceptionDetails -} - -func (e *EvalError) Error() string { - exp := e.Exception - return fmt.Sprintf("eval js error: %s %s", exp.Description, exp.Value) -} - -// Is interface. -func (e *EvalError) Is(err error) bool { _, ok := err.(*EvalError); return ok } - -// NavigationError error. -type NavigationError struct { - Reason string -} - -func (e *NavigationError) Error() string { - return "navigation failed: " + e.Reason -} - -// Is interface. -func (e *NavigationError) Is(err error) bool { _, ok := err.(*NavigationError); return ok } - -// PageCloseCanceledError error. -type PageCloseCanceledError struct{} - -func (e *PageCloseCanceledError) Error() string { - return "page close canceled" -} - -// NotInteractableError error. Check the doc of Element.Interactable for details. -type NotInteractableError struct{} - -func (e *NotInteractableError) Error() string { - return "element is not cursor interactable" -} - -// InvisibleShapeError error. -type InvisibleShapeError struct { - *Element -} - -// Error ... -func (e *InvisibleShapeError) Error() string { - return fmt.Sprintf("element has no visible shape or outside the viewport: %s", e.String()) -} - -// Is interface. -func (e *InvisibleShapeError) Is(err error) bool { _, ok := err.(*InvisibleShapeError); return ok } - -// Unwrap ... -func (e *InvisibleShapeError) Unwrap() error { - return &NotInteractableError{} -} - -// CoveredError error. -type CoveredError struct { - *Element -} - -// Error ... -func (e *CoveredError) Error() string { - return fmt.Sprintf("element covered by: %s", e.String()) -} - -// Unwrap ... -func (e *CoveredError) Unwrap() error { - return &NotInteractableError{} -} - -// Is interface. -func (e *CoveredError) Is(err error) bool { _, ok := err.(*CoveredError); return ok } - -// NoPointerEventsError error. -type NoPointerEventsError struct { - *Element -} - -// Error ... -func (e *NoPointerEventsError) Error() string { - return fmt.Sprintf("element's pointer-events is none: %s", e.String()) -} - -// Unwrap ... -func (e *NoPointerEventsError) Unwrap() error { - return &NotInteractableError{} -} - -// Is interface. -func (e *NoPointerEventsError) Is(err error) bool { _, ok := err.(*NoPointerEventsError); return ok } - -// PageNotFoundError error. -type PageNotFoundError struct{} - -func (e *PageNotFoundError) Error() string { - return "cannot find page" -} - -// NoShadowRootError error. -type NoShadowRootError struct { - *Element -} - -// Error ... -func (e *NoShadowRootError) Error() string { - return fmt.Sprintf("element has no shadow root: %s", e.String()) -} - -// Is interface. -func (e *NoShadowRootError) Is(err error) bool { _, ok := err.(*NoShadowRootError); return ok } diff --git a/internal/rod/hijack.go b/internal/rod/hijack.go deleted file mode 100644 index 477dd99..0000000 --- a/internal/rod/hijack.go +++ /dev/null @@ -1,430 +0,0 @@ -package rod - -import ( - "bytes" - "context" - "io" - "net/http" - "net/url" - "regexp" - "strings" - - "github.com/go-rod/rod/lib/proto" - "github.com/go-rod/rod/lib/utils" - "github.com/ysmood/gson" -) - -// HijackRequests same as Page.HijackRequests, but can intercept requests of the entire browser. -func (b *Browser) HijackRequests() *HijackRouter { - return newHijackRouter(b, b).initEvents() -} - -// HijackRequests creates a new router instance for requests hijacking. -// When use Fetch domain outside the router should be stopped. Enabling hijacking disables page caching, -// but such as 304 Not Modified will still work as expected. -// The entire process of hijacking one request: -// -// browser --req-> rod ---> server ---> rod --res-> browser -// -// The --req-> and --res-> are the parts that can be modified. -func (p *Page) HijackRequests() *HijackRouter { - return newHijackRouter(p.browser, p).initEvents() -} - -// HijackRouter context. -type HijackRouter struct { - run func() - stop func() - handlers []*hijackHandler - enable *proto.FetchEnable - client proto.Client - browser *Browser -} - -func newHijackRouter(browser *Browser, client proto.Client) *HijackRouter { - return &HijackRouter{ - enable: &proto.FetchEnable{}, - browser: browser, - client: client, - handlers: []*hijackHandler{}, - } -} - -func (r *HijackRouter) initEvents() *HijackRouter { //nolint: gocognit - ctx := r.browser.ctx - if cta, ok := r.client.(proto.Contextable); ok { - ctx = cta.GetContext() - } - - var sessionID proto.TargetSessionID - if tsa, ok := r.client.(proto.Sessionable); ok { - sessionID = tsa.GetSessionID() - } - - eventCtx, cancel := context.WithCancel(ctx) - r.stop = cancel - - _ = r.enable.Call(r.client) - - r.run = r.browser.Context(eventCtx).eachEvent(sessionID, func(e *proto.FetchRequestPaused) bool { - go func() { - ctx := r.new(eventCtx, e) - for _, h := range r.handlers { - if !h.regexp.MatchString(e.Request.URL) { - continue - } - - h.handler(ctx) - - if ctx.continueRequest != nil { - ctx.continueRequest.RequestID = e.RequestID - err := ctx.continueRequest.Call(r.client) - if err != nil { - ctx.OnError(err) - } - return - } - - if ctx.Skip { - continue - } - - if ctx.Response.fail.ErrorReason != "" { - err := ctx.Response.fail.Call(r.client) - if err != nil { - ctx.OnError(err) - } - return - } - - err := ctx.Response.payload.Call(r.client) - if err != nil { - ctx.OnError(err) - return - } - } - }() - - return false - }) - return r -} - -// Add a hijack handler to router, the doc of the pattern is the same as "proto.FetchRequestPattern.URLPattern". -func (r *HijackRouter) Add(pattern string, resourceType proto.NetworkResourceType, handler func(*Hijack)) error { - r.enable.Patterns = append(r.enable.Patterns, &proto.FetchRequestPattern{ - URLPattern: pattern, - ResourceType: resourceType, - }) - - reg := regexp.MustCompile(proto.PatternToReg(pattern)) - - r.handlers = append(r.handlers, &hijackHandler{ - pattern: pattern, - regexp: reg, - handler: handler, - }) - - return r.enable.Call(r.client) -} - -// Remove handler via the pattern. -func (r *HijackRouter) Remove(pattern string) error { - patterns := []*proto.FetchRequestPattern{} - handlers := []*hijackHandler{} - for _, h := range r.handlers { - if h.pattern != pattern { - patterns = append(patterns, &proto.FetchRequestPattern{URLPattern: h.pattern}) - handlers = append(handlers, h) - } - } - r.enable.Patterns = patterns - r.handlers = handlers - - return r.enable.Call(r.client) -} - -// new context. -func (r *HijackRouter) new(ctx context.Context, e *proto.FetchRequestPaused) *Hijack { - headers := http.Header{} - for k, v := range e.Request.Headers { - headers[k] = []string{v.String()} - } - - u, _ := url.Parse(e.Request.URL) - - req := &http.Request{ - Method: e.Request.Method, - URL: u, - Body: io.NopCloser(strings.NewReader(e.Request.PostData)), - Header: headers, - } - - return &Hijack{ - Request: &HijackRequest{ - event: e, - req: req.WithContext(ctx), - }, - Response: &HijackResponse{ - payload: &proto.FetchFulfillRequest{ - ResponseCode: 200, - RequestID: e.RequestID, - }, - fail: &proto.FetchFailRequest{ - RequestID: e.RequestID, - }, - }, - OnError: func(_ error) {}, - - browser: r.browser, - } -} - -// Run the router, after you call it, you shouldn't add new handler to it. -func (r *HijackRouter) Run() { - r.run() -} - -// Stop the router. -func (r *HijackRouter) Stop() error { - r.stop() - return proto.FetchDisable{}.Call(r.client) -} - -// hijackHandler to handle each request that match the regexp. -type hijackHandler struct { - pattern string - regexp *regexp.Regexp - handler func(*Hijack) -} - -// Hijack context. -type Hijack struct { - Request *HijackRequest - Response *HijackResponse - OnError func(error) - - // Skip to next handler - Skip bool - - continueRequest *proto.FetchContinueRequest - - // CustomState is used to store things for this context - CustomState interface{} - - browser *Browser -} - -// ContinueRequest without hijacking. The RequestID will be set by the router, you don't have to set it. -func (h *Hijack) ContinueRequest(cq *proto.FetchContinueRequest) { - h.continueRequest = cq -} - -// LoadResponse will send request to the real destination and load the response as default response to override. -func (h *Hijack) LoadResponse(client *http.Client, loadBody bool) error { - res, err := client.Do(h.Request.req) - if err != nil { - return err - } - - defer func() { _ = res.Body.Close() }() - - h.Response.payload.ResponseCode = res.StatusCode - h.Response.RawResponse = res - - for k, vs := range res.Header { - for _, v := range vs { - h.Response.SetHeader(k, v) - } - } - - if loadBody { - b, err := io.ReadAll(res.Body) - if err != nil { - return err - } - h.Response.payload.Body = b - } - - return nil -} - -// HijackRequest context. -type HijackRequest struct { - event *proto.FetchRequestPaused - req *http.Request -} - -// Type of the resource. -func (ctx *HijackRequest) Type() proto.NetworkResourceType { - return ctx.event.ResourceType -} - -// Method of the request. -func (ctx *HijackRequest) Method() string { - return ctx.event.Request.Method -} - -// URL of the request. -func (ctx *HijackRequest) URL() *url.URL { - u, _ := url.Parse(ctx.event.Request.URL) - return u -} - -// Header via a key. -func (ctx *HijackRequest) Header(key string) string { - return ctx.event.Request.Headers[key].String() -} - -// Headers of request. -func (ctx *HijackRequest) Headers() proto.NetworkHeaders { - return ctx.event.Request.Headers -} - -// Body of the request, devtools API doesn't support binary data yet, only string can be captured. -func (ctx *HijackRequest) Body() string { - return ctx.event.Request.PostData -} - -// JSONBody of the request. -func (ctx *HijackRequest) JSONBody() gson.JSON { - return gson.NewFrom(ctx.Body()) -} - -// Req returns the underlying http.Request instance that will be used to send the request. -func (ctx *HijackRequest) Req() *http.Request { - return ctx.req -} - -// SetContext of the underlying http.Request instance. -func (ctx *HijackRequest) SetContext(c context.Context) *HijackRequest { - ctx.req = ctx.req.WithContext(c) - return ctx -} - -// SetBody of the request, if obj is []byte or string, raw body will be used, else it will be encoded as json. -func (ctx *HijackRequest) SetBody(obj interface{}) *HijackRequest { - var b []byte - - switch body := obj.(type) { - case []byte: - b = body - case string: - b = []byte(body) - default: - b = utils.MustToJSONBytes(body) - } - - ctx.req.Body = io.NopCloser(bytes.NewBuffer(b)) - - return ctx -} - -// IsNavigation determines whether the request is a navigation request. -func (ctx *HijackRequest) IsNavigation() bool { - return ctx.Type() == proto.NetworkResourceTypeDocument -} - -// HijackResponse context. -type HijackResponse struct { - payload *proto.FetchFulfillRequest - RawResponse *http.Response - fail *proto.FetchFailRequest -} - -// Payload to respond the request from the browser. -func (ctx *HijackResponse) Payload() *proto.FetchFulfillRequest { - return ctx.payload -} - -// Body of the payload. -func (ctx *HijackResponse) Body() string { - return string(ctx.payload.Body) -} - -// Headers returns the clone of response headers. -// If you want to modify the response headers use HijackResponse.SetHeader . -func (ctx *HijackResponse) Headers() http.Header { - header := http.Header{} - - for _, h := range ctx.payload.ResponseHeaders { - header.Add(h.Name, h.Value) - } - - return header -} - -// SetHeader of the payload via key-value pairs. -func (ctx *HijackResponse) SetHeader(pairs ...string) *HijackResponse { - for i := 0; i < len(pairs); i += 2 { - ctx.payload.ResponseHeaders = append(ctx.payload.ResponseHeaders, &proto.FetchHeaderEntry{ - Name: pairs[i], - Value: pairs[i+1], - }) - } - return ctx -} - -// SetBody of the payload, if obj is []byte or string, raw body will be used, else it will be encoded as json. -func (ctx *HijackResponse) SetBody(obj interface{}) *HijackResponse { - switch body := obj.(type) { - case []byte: - ctx.payload.Body = body - case string: - ctx.payload.Body = []byte(body) - default: - ctx.payload.Body = utils.MustToJSONBytes(body) - } - return ctx -} - -// Fail request. -func (ctx *HijackResponse) Fail(reason proto.NetworkErrorReason) *HijackResponse { - ctx.fail.ErrorReason = reason - return ctx -} - -// HandleAuth for the next basic HTTP authentication. -// It will prevent the popup that requires user to input user name and password. -// Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication -func (b *Browser) HandleAuth(username, password string) func() error { - enable := b.DisableDomain("", &proto.FetchEnable{}) - disable := b.EnableDomain("", &proto.FetchEnable{ - HandleAuthRequests: true, - }) - - paused := &proto.FetchRequestPaused{} - auth := &proto.FetchAuthRequired{} - - ctx, cancel := context.WithCancel(b.ctx) - waitPaused := b.Context(ctx).WaitEvent(paused) - waitAuth := b.Context(ctx).WaitEvent(auth) - - return func() (err error) { - defer enable() - defer disable() - defer cancel() - - waitPaused() - - err = proto.FetchContinueRequest{ - RequestID: paused.RequestID, - }.Call(b) - if err != nil { - return - } - - waitAuth() - - err = proto.FetchContinueWithAuth{ - RequestID: auth.RequestID, - AuthChallengeResponse: &proto.FetchAuthChallengeResponse{ - Response: proto.FetchAuthChallengeResponseResponseProvideCredentials, - Username: username, - Password: password, - }, - }.Call(b) - - return - } -} diff --git a/internal/rod/input.go b/internal/rod/input.go deleted file mode 100644 index c7d3f9f..0000000 --- a/internal/rod/input.go +++ /dev/null @@ -1,457 +0,0 @@ -package rod - -import ( - "fmt" - "sync" - - "github.com/go-rod/rod/lib/input" - "github.com/go-rod/rod/lib/proto" - "github.com/go-rod/rod/lib/utils" - "github.com/ysmood/gson" -) - -// Keyboard represents the keyboard on a page, it's always related the main frame. -type Keyboard struct { - sync.Mutex - - page *Page - - // pressed keys must be released before it can be pressed again - pressed map[input.Key]struct{} -} - -func (p *Page) newKeyboard() *Page { - p.Keyboard = &Keyboard{page: p, pressed: map[input.Key]struct{}{}} - return p -} - -func (k *Keyboard) getModifiers() int { - k.Lock() - defer k.Unlock() - return k.modifiers() -} - -func (k *Keyboard) modifiers() int { - ms := 0 - for key := range k.pressed { - ms |= key.Modifier() - } - return ms -} - -// Press the key down. -// To input characters that are not on the keyboard, such as Chinese or Japanese, you should -// use method like [Page.InsertText]. -func (k *Keyboard) Press(key input.Key) error { - defer k.page.tryTrace(TraceTypeInput, "press key: "+key.Info().Code)() - k.page.browser.trySlowMotion() - - k.Lock() - defer k.Unlock() - - k.pressed[key] = struct{}{} - - return key.Encode(proto.InputDispatchKeyEventTypeKeyDown, k.modifiers()).Call(k.page) -} - -// Release the key. -func (k *Keyboard) Release(key input.Key) error { - defer k.page.tryTrace(TraceTypeInput, "release key: "+key.Info().Code)() - - k.Lock() - defer k.Unlock() - - if _, has := k.pressed[key]; !has { - return nil - } - - delete(k.pressed, key) - - return key.Encode(proto.InputDispatchKeyEventTypeKeyUp, k.modifiers()).Call(k.page) -} - -// Type releases the key after the press. -func (k *Keyboard) Type(keys ...input.Key) (err error) { - for _, key := range keys { - err = k.Press(key) - if err != nil { - return - } - err = k.Release(key) - if err != nil { - return - } - } - return -} - -// KeyActionType enum. -type KeyActionType int - -// KeyActionTypes. -const ( - KeyActionPress KeyActionType = iota - KeyActionRelease - KeyActionTypeKey -) - -// KeyAction to perform. -type KeyAction struct { - Type KeyActionType - Key input.Key -} - -// KeyActions to simulate. -type KeyActions struct { - keyboard *Keyboard - - Actions []KeyAction -} - -// KeyActions simulates the type actions on a physical keyboard. -// Useful when input shortcuts like ctrl+enter . -func (p *Page) KeyActions() *KeyActions { - return &KeyActions{keyboard: p.Keyboard} -} - -// Press keys is guaranteed to have a release at the end of actions. -func (ka *KeyActions) Press(keys ...input.Key) *KeyActions { - for _, key := range keys { - ka.Actions = append(ka.Actions, KeyAction{KeyActionPress, key}) - } - return ka -} - -// Release keys. -func (ka *KeyActions) Release(keys ...input.Key) *KeyActions { - for _, key := range keys { - ka.Actions = append(ka.Actions, KeyAction{KeyActionRelease, key}) - } - return ka -} - -// Type will release the key immediately after the pressing. -func (ka *KeyActions) Type(keys ...input.Key) *KeyActions { - for _, key := range keys { - ka.Actions = append(ka.Actions, KeyAction{KeyActionTypeKey, key}) - } - return ka -} - -// Do the actions. -func (ka *KeyActions) Do() (err error) { - for _, a := range ka.balance() { - switch a.Type { - case KeyActionPress: - err = ka.keyboard.Press(a.Key) - case KeyActionRelease: - err = ka.keyboard.Release(a.Key) - case KeyActionTypeKey: - err = ka.keyboard.Type(a.Key) - } - if err != nil { - return - } - } - return -} - -// Make sure there's at least one release after the presses, such as: -// -// p1,p2,p1,r1 => p1,p2,p1,r1,r2 -func (ka *KeyActions) balance() []KeyAction { - actions := ka.Actions - - h := map[input.Key]bool{} - for _, a := range actions { - switch a.Type { - case KeyActionPress: - h[a.Key] = true - case KeyActionRelease, KeyActionTypeKey: - h[a.Key] = false - } - } - - for key, needRelease := range h { - if needRelease { - actions = append(actions, KeyAction{KeyActionRelease, key}) - } - } - - return actions -} - -// InsertText is like pasting text into the page. -func (p *Page) InsertText(text string) error { - defer p.tryTrace(TraceTypeInput, "insert text "+text)() - p.browser.trySlowMotion() - - err := proto.InputInsertText{Text: text}.Call(p) - return err -} - -// Mouse represents the mouse on a page, it's always related the main frame. -type Mouse struct { - sync.Mutex - - page *Page - - id string // mouse svg dom element id - - pos proto.Point - - // the buttons is currently being pressed, reflects the press order - buttons []proto.InputMouseButton -} - -func (p *Page) newMouse() *Page { - p.Mouse = &Mouse{page: p, id: utils.RandString(8)} - return p -} - -// Position of current cursor. -func (m *Mouse) Position() proto.Point { - m.Lock() - defer m.Unlock() - return m.pos -} - -// MoveTo the absolute position. -func (m *Mouse) MoveTo(p proto.Point) error { - m.Lock() - defer m.Unlock() - - button, buttons := input.EncodeMouseButton(m.buttons) - - m.page.browser.trySlowMotion() - - err := proto.InputDispatchMouseEvent{ - Type: proto.InputDispatchMouseEventTypeMouseMoved, - X: p.X, - Y: p.Y, - Button: button, - Buttons: gson.Int(buttons), - Modifiers: m.page.Keyboard.getModifiers(), - }.Call(m.page) - if err != nil { - return err - } - - // to make sure set only when call is successful - m.pos = p - - if m.page.browser.trace { - if !m.updateMouseTracer() { - m.initMouseTracer() - m.updateMouseTracer() - } - } - - return nil -} - -// MoveAlong the guide function. -// Every time the guide function is called it should return the next mouse position, return true to stop. -// Read the source code of [Mouse.MoveLinear] as an example to use this method. -func (m *Mouse) MoveAlong(guide func() (proto.Point, bool)) error { - for { - p, stop := guide() - if stop { - return m.MoveTo(p) - } - - err := m.MoveTo(p) - if err != nil { - return err - } - } -} - -// MoveLinear to the absolute position with the given steps. -// Such as move from (0,0) to (6,6) with 3 steps, the mouse will first move to (2,2) then (4,4) then (6,6). -func (m *Mouse) MoveLinear(to proto.Point, steps int) error { - p := m.Position() - step := to.Minus(p).Scale(1 / float64(steps)) - count := 0 - - return m.MoveAlong(func() (proto.Point, bool) { - count++ - if count == steps { - return to, true - } - - p = p.Add(step) - return p, false - }) -} - -// Scroll the relative offset with specified steps. -func (m *Mouse) Scroll(offsetX, offsetY float64, steps int) error { - m.Lock() - defer m.Unlock() - - defer m.page.tryTrace(TraceTypeInput, fmt.Sprintf("scroll (%.2f, %.2f)", offsetX, offsetY))() - m.page.browser.trySlowMotion() - - if steps < 1 { - steps = 1 - } - - button, buttons := input.EncodeMouseButton(m.buttons) - - stepX := offsetX / float64(steps) - stepY := offsetY / float64(steps) - - for i := 0; i < steps; i++ { - err := proto.InputDispatchMouseEvent{ - Type: proto.InputDispatchMouseEventTypeMouseWheel, - Button: button, - Buttons: gson.Int(buttons), - Modifiers: m.page.Keyboard.getModifiers(), - DeltaX: stepX, - DeltaY: stepY, - X: m.pos.X, - Y: m.pos.Y, - }.Call(m.page) - if err != nil { - return err - } - } - - return nil -} - -// Down holds the button down. -func (m *Mouse) Down(button proto.InputMouseButton, clickCount int) error { - m.Lock() - defer m.Unlock() - - toButtons := append(append([]proto.InputMouseButton{}, m.buttons...), button) - - _, buttons := input.EncodeMouseButton(toButtons) - - err := proto.InputDispatchMouseEvent{ - Type: proto.InputDispatchMouseEventTypeMousePressed, - Button: button, - Buttons: gson.Int(buttons), - ClickCount: clickCount, - Modifiers: m.page.Keyboard.getModifiers(), - X: m.pos.X, - Y: m.pos.Y, - }.Call(m.page) - if err != nil { - return err - } - m.buttons = toButtons - return nil -} - -// Up releases the button. -func (m *Mouse) Up(button proto.InputMouseButton, clickCount int) error { - m.Lock() - defer m.Unlock() - - toButtons := []proto.InputMouseButton{} - for _, btn := range m.buttons { - if btn == button { - continue - } - toButtons = append(toButtons, btn) - } - - _, buttons := input.EncodeMouseButton(toButtons) - - err := proto.InputDispatchMouseEvent{ - Type: proto.InputDispatchMouseEventTypeMouseReleased, - Button: button, - Buttons: gson.Int(buttons), - ClickCount: clickCount, - Modifiers: m.page.Keyboard.getModifiers(), - X: m.pos.X, - Y: m.pos.Y, - }.Call(m.page) - if err != nil { - return err - } - m.buttons = toButtons - return nil -} - -// Click the button. It's the combination of [Mouse.Down] and [Mouse.Up]. -func (m *Mouse) Click(button proto.InputMouseButton, clickCount int) error { - m.page.browser.trySlowMotion() - - err := m.Down(button, clickCount) - if err != nil { - return err - } - - return m.Up(button, clickCount) -} - -// Touch presents a touch device, such as a hand with fingers, each finger is a [proto.InputTouchPoint]. -// Touch events is stateless, we use the struct here only as a namespace to make the API style unified. -type Touch struct { - page *Page -} - -func (p *Page) newTouch() *Page { - p.Touch = &Touch{page: p} - return p -} - -// Start a touch action. -func (t *Touch) Start(points ...*proto.InputTouchPoint) error { - // TODO: https://crbug.com/613219 - _ = t.page.WaitRepaint() - _ = t.page.WaitRepaint() - - return proto.InputDispatchTouchEvent{ - Type: proto.InputDispatchTouchEventTypeTouchStart, - TouchPoints: points, - Modifiers: t.page.Keyboard.getModifiers(), - }.Call(t.page) -} - -// Move touch points. Use the [proto.InputTouchPoint.ID] (Touch.identifier) to track points. -// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Touch_events -func (t *Touch) Move(points ...*proto.InputTouchPoint) error { - return proto.InputDispatchTouchEvent{ - Type: proto.InputDispatchTouchEventTypeTouchMove, - TouchPoints: points, - Modifiers: t.page.Keyboard.getModifiers(), - }.Call(t.page) -} - -// End touch action. -func (t *Touch) End() error { - return proto.InputDispatchTouchEvent{ - Type: proto.InputDispatchTouchEventTypeTouchEnd, - TouchPoints: []*proto.InputTouchPoint{}, - Modifiers: t.page.Keyboard.getModifiers(), - }.Call(t.page) -} - -// Cancel touch action. -func (t *Touch) Cancel() error { - return proto.InputDispatchTouchEvent{ - Type: proto.InputDispatchTouchEventTypeTouchCancel, - TouchPoints: []*proto.InputTouchPoint{}, - Modifiers: t.page.Keyboard.getModifiers(), - }.Call(t.page) -} - -// Tap dispatches a touchstart and touchend event. -func (t *Touch) Tap(x, y float64) error { - defer t.page.tryTrace(TraceTypeInput, "touch")() - t.page.browser.trySlowMotion() - - p := &proto.InputTouchPoint{X: x, Y: y} - - err := t.Start(p) - if err != nil { - return err - } - - return t.End() -} diff --git a/internal/rod/must.go b/internal/rod/must.go deleted file mode 100644 index fa94021..0000000 --- a/internal/rod/must.go +++ /dev/null @@ -1,1172 +0,0 @@ -// This file contains the methods that panics when error return value is not nil. -// Their function names are all prefixed with Must. -// A function here is usually a wrapper for the error version with fixed default options to make it easier to use. -// -// For example the source code of [Element.Click] and [Element.MustClick]. MustClick has no argument. -// But `Click` has a `button` argument to decide which button to click. -// `MustClick` feels like a version of `Click` with some default behaviors. - -package rod - -import ( - "errors" - "io" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - "github.com/go-rod/rod/lib/devices" - "github.com/go-rod/rod/lib/input" - "github.com/go-rod/rod/lib/proto" - "github.com/go-rod/rod/lib/utils" - "github.com/ysmood/gson" -) - -// It must be generated by genE. -type eFunc func(args ...interface{}) - -// Generate a eFunc with the specified fail function. -// If the last arg of eFunc is error the fail will be called. -func genE(fail func(interface{})) eFunc { - return func(args ...interface{}) { - err, ok := args[len(args)-1].(error) - if ok { - fail(err) - } - } -} - -// WithPanic returns a browser clone with the specified panic function. -// The fail must stop the current goroutine's execution immediately, such as use [runtime.Goexit] or panic inside it. -func (b *Browser) WithPanic(fail func(interface{})) *Browser { - n := *b - n.e = genE(fail) - return &n -} - -// MustConnect is similar to [Browser.Connect]. -func (b *Browser) MustConnect() *Browser { - b.e(b.Connect()) - return b -} - -// MustClose is similar to [Browser.Close]. -func (b *Browser) MustClose() { - _ = b.Close() -} - -// MustIncognito is similar to [Browser.Incognito]. -func (b *Browser) MustIncognito() *Browser { - p, err := b.Incognito() - b.e(err) - return p -} - -// MustPage is similar to [Browser.Page]. -// The url list will be joined by "/". -func (b *Browser) MustPage(url ...string) *Page { - p, err := b.Page(proto.TargetCreateTarget{URL: strings.Join(url, "/")}) - b.e(err) - return p -} - -// MustPages is similar to [Browser.Pages]. -func (b *Browser) MustPages() Pages { - list, err := b.Pages() - b.e(err) - return list -} - -// MustPageFromTargetID is similar to [Browser.PageFromTargetID]. -func (b *Browser) MustPageFromTargetID(targetID proto.TargetTargetID) *Page { - p, err := b.PageFromTarget(targetID) - b.e(err) - return p -} - -// MustHandleAuth is similar to [Browser.HandleAuth]. -func (b *Browser) MustHandleAuth(username, password string) (wait func()) { - w := b.HandleAuth(username, password) - return func() { b.e(w()) } -} - -// MustIgnoreCertErrors is similar to [Browser.IgnoreCertErrors]. -func (b *Browser) MustIgnoreCertErrors(enable bool) *Browser { - b.e(b.IgnoreCertErrors(enable)) - return b -} - -// MustGetCookies is similar to [Browser.GetCookies]. -func (b *Browser) MustGetCookies() []*proto.NetworkCookie { - nc, err := b.GetCookies() - b.e(err) - return nc -} - -// MustSetCookies is similar to [Browser.SetCookies]. -// If the len(cookies) is 0 it will clear all the cookies. -func (b *Browser) MustSetCookies(cookies ...*proto.NetworkCookie) *Browser { - if len(cookies) == 0 { - b.e(b.SetCookies(nil)) - } else { - b.e(b.SetCookies(proto.CookiesToParams(cookies))) - } - return b -} - -// MustWaitDownload is similar to [Browser.WaitDownload]. -// It will read the file into bytes then remove the file. -func (b *Browser) MustWaitDownload() func() []byte { - tmpDir := filepath.Join(os.TempDir(), "rod", "downloads") - wait := b.WaitDownload(tmpDir) - - return func() []byte { - info := wait() - path := filepath.Join(tmpDir, info.GUID) - defer func() { _ = os.Remove(path) }() - data, err := os.ReadFile(path) - b.e(err) - return data - } -} - -// MustVersion is similar to [Browser.Version]. -func (b *Browser) MustVersion() *proto.BrowserGetVersionResult { - v, err := b.Version() - b.e(err) - return v -} - -// MustFind is similar to [Browser.Find]. -func (ps Pages) MustFind(selector string) *Page { - p, err := ps.Find(selector) - if err != nil { - if len(ps) > 0 { - ps[0].e(err) - } else { - // fallback to utils.E, because we don't have enough - // context to call the scope `.e`. - utils.E(err) - } - } - return p -} - -// MustFindByURL is similar to [Page.FindByURL]. -func (ps Pages) MustFindByURL(regex string) *Page { - p, err := ps.FindByURL(regex) - if err != nil { - if len(ps) > 0 { - ps[0].e(err) - } else { - // fallback to utils.E, because we don't have enough - // context to call the scope `.e`. - utils.E(err) - } - } - return p -} - -// WithPanic returns a page clone with the specified panic function. -// The fail must stop the current goroutine's execution immediately, such as use [runtime.Goexit] or panic inside it. -func (p *Page) WithPanic(fail func(interface{})) *Page { - n := *p - n.e = genE(fail) - return &n -} - -// MustInfo is similar to [Page.Info]. -func (p *Page) MustInfo() *proto.TargetTargetInfo { - info, err := p.Info() - p.e(err) - return info -} - -// MustHTML is similar to [Page.HTML]. -func (p *Page) MustHTML() string { - html, err := p.HTML() - p.e(err) - return html -} - -// MustCookies is similar to [Page.Cookies]. -func (p *Page) MustCookies(urls ...string) []*proto.NetworkCookie { - cookies, err := p.Cookies(urls) - p.e(err) - return cookies -} - -// MustSetCookies is similar to [Page.SetCookies]. -// If the len(cookies) is 0 it will clear all the cookies. -func (p *Page) MustSetCookies(cookies ...*proto.NetworkCookieParam) *Page { - if len(cookies) == 0 { - cookies = nil - } - p.e(p.SetCookies(cookies)) - return p -} - -// MustSetExtraHeaders is similar to [Page.SetExtraHeaders]. -func (p *Page) MustSetExtraHeaders(dict ...string) (cleanup func()) { - cleanup, err := p.SetExtraHeaders(dict) - p.e(err) - return -} - -// MustSetUserAgent is similar to [Page.SetUserAgent]. -func (p *Page) MustSetUserAgent(req *proto.NetworkSetUserAgentOverride) *Page { - p.e(p.SetUserAgent(req)) - return p -} - -// MustSetBlockedURLs is similar to [Page.SetBlockedURLs]. -func (p *Page) MustSetBlockedURLs(urls ...string) *Page { - p.e(p.SetBlockedURLs(urls)) - return p -} - -// MustNavigate is similar to [Page.Navigate]. -func (p *Page) MustNavigate(url string) *Page { - p.e(p.Navigate(url)) - return p -} - -// MustResetNavigationHistory is similar to [Page.ResetNavigationHistory]. -func (p *Page) MustResetNavigationHistory() *Page { - p.e(p.ResetNavigationHistory()) - return p -} - -// MustReload is similar to [Page.Reload]. -func (p *Page) MustReload() *Page { - p.e(p.Reload()) - return p -} - -// MustActivate is similar to [Page.Activate]. -func (p *Page) MustActivate() *Page { - p.e(p.Activate()) - return p -} - -// MustNavigateBack is similar to [Page.NavigateBack]. -func (p *Page) MustNavigateBack() *Page { - p.e(p.NavigateBack()) - return p -} - -// MustNavigateForward is similar to [Page.NavigateForward]. -func (p *Page) MustNavigateForward() *Page { - p.e(p.NavigateForward()) - return p -} - -// MustGetWindow is similar to [Page.GetWindow]. -func (p *Page) MustGetWindow() *proto.BrowserBounds { - bounds, err := p.GetWindow() - p.e(err) - return bounds -} - -// MustSetWindow is similar to [Page.SetWindow]. -func (p *Page) MustSetWindow(left, top, width, height int) *Page { - p.e(p.SetWindow(&proto.BrowserBounds{ - Left: gson.Int(left), - Top: gson.Int(top), - Width: gson.Int(width), - Height: gson.Int(height), - WindowState: proto.BrowserWindowStateNormal, - })) - return p -} - -// MustWindowMinimize is similar to [Page.WindowMinimize]. -func (p *Page) MustWindowMinimize() *Page { - p.e(p.SetWindow(&proto.BrowserBounds{ - WindowState: proto.BrowserWindowStateMinimized, - })) - return p -} - -// MustWindowMaximize is similar to [Page.WindowMaximize]. -func (p *Page) MustWindowMaximize() *Page { - p.e(p.SetWindow(&proto.BrowserBounds{ - WindowState: proto.BrowserWindowStateMaximized, - })) - return p -} - -// MustWindowFullscreen is similar to [Page.WindowFullscreen]. -func (p *Page) MustWindowFullscreen() *Page { - p.e(p.SetWindow(&proto.BrowserBounds{ - WindowState: proto.BrowserWindowStateFullscreen, - })) - return p -} - -// MustWindowNormal is similar to [Page.WindowNormal]. -func (p *Page) MustWindowNormal() *Page { - p.e(p.SetWindow(&proto.BrowserBounds{ - WindowState: proto.BrowserWindowStateNormal, - })) - return p -} - -// MustSetViewport is similar to [Page.SetViewport]. -func (p *Page) MustSetViewport(width, height int, deviceScaleFactor float64, mobile bool) *Page { - p.e(p.SetViewport(&proto.EmulationSetDeviceMetricsOverride{ - Width: width, - Height: height, - DeviceScaleFactor: deviceScaleFactor, - Mobile: mobile, - })) - return p -} - -// MustEmulate is similar to [Page.Emulate]. -func (p *Page) MustEmulate(device devices.Device) *Page { - p.e(p.Emulate(device)) - return p -} - -// MustStopLoading is similar to [Page.StopLoading]. -func (p *Page) MustStopLoading() *Page { - p.e(p.StopLoading()) - return p -} - -// MustClose is similar to [Page.Close]. -func (p *Page) MustClose() { - p.e(p.Close()) -} - -// MustHandleDialog is similar to [Page.HandleDialog]. -func (p *Page) MustHandleDialog() (wait func() *proto.PageJavascriptDialogOpening, handle func(bool, string)) { - w, h := p.HandleDialog() - return w, func(accept bool, promptText string) { - p.e(h(&proto.PageHandleJavaScriptDialog{ - Accept: accept, - PromptText: promptText, - })) - } -} - -// MustHandleFileDialog is similar to [Page.HandleFileDialog]. -func (p *Page) MustHandleFileDialog() func(...string) { - setFiles, err := p.HandleFileDialog() - p.e(err) - return func(paths ...string) { - p.e(setFiles(paths)) - } -} - -// MustScreenshot is similar to [Page.Screenshot]. -// If the toFile is "", it Page.will save output to "tmp/screenshots" folder, time as the file name. -func (p *Page) MustScreenshot(toFile ...string) []byte { - bin, err := p.Screenshot(false, nil) - p.e(err) - p.e(saveFile(saveFileTypeScreenshot, bin, toFile)) - return bin -} - -// MustCaptureDOMSnapshot is similar to [Page.CaptureDOMSnapshot]. -func (p *Page) MustCaptureDOMSnapshot() (domSnapshot *proto.DOMSnapshotCaptureSnapshotResult) { - domSnapshot, err := p.CaptureDOMSnapshot() - p.e(err) - return domSnapshot -} - -// MustTriggerFavicon is similar to [PageTriggerFavicon]. -func (p *Page) MustTriggerFavicon() *Page { - p.e(p.TriggerFavicon()) - return p -} - -// MustScreenshotFullPage is similar to [Page.ScreenshotFullPage]. -// If the toFile is "", it Page.will save output to "tmp/screenshots" folder, time as the file name. -func (p *Page) MustScreenshotFullPage(toFile ...string) []byte { - bin, err := p.Screenshot(true, nil) - p.e(err) - p.e(saveFile(saveFileTypeScreenshot, bin, toFile)) - return bin -} - -// MustScrollScreenshot is similar to [Page.ScrollScreenshot]. -// If the toFile is "", it Page.will save output to "tmp/screenshots" folder, time as the file name. -func (p *Page) MustScrollScreenshot(toFile ...string) []byte { - bin, err := p.ScrollScreenshot(nil) - p.e(err) - p.e(saveFile(saveFileTypeScreenshot, bin, toFile)) - return bin -} - -// MustPDF is similar to [Page.PDF]. -// If the toFile is "", it Page.will save output to "tmp/pdf" folder, time as the file name. -func (p *Page) MustPDF(toFile ...string) []byte { - r, err := p.PDF(&proto.PagePrintToPDF{}) - p.e(err) - bin, err := io.ReadAll(r) - p.e(err) - - p.e(saveFile(saveFileTypePDF, bin, toFile)) - return bin -} - -// MustWaitOpen is similar to [Page.WaitOpen]. -func (p *Page) MustWaitOpen() (wait func() (newPage *Page)) { - w := p.WaitOpen() - return func() *Page { - page, err := w() - p.e(err) - return page - } -} - -// MustWaitNavigation is similar to [Page.WaitNavigation]. -func (p *Page) MustWaitNavigation() func() { - return p.WaitNavigation(proto.PageLifecycleEventNameNetworkAlmostIdle) -} - -// MustWaitRequestIdle is similar to [Page.WaitRequestIdle]. -func (p *Page) MustWaitRequestIdle(excludes ...string) (wait func()) { - return p.WaitRequestIdle(300*time.Millisecond, nil, excludes, nil) -} - -// MustWaitIdle is similar to [Page.WaitIdle]. -func (p *Page) MustWaitIdle() *Page { - p.e(p.WaitIdle(time.Minute)) - return p -} - -// MustWaitDOMStable is similar to [Page.WaitDOMStable]. -func (p *Page) MustWaitDOMStable() *Page { - p.e(p.WaitDOMStable(time.Second, 0)) - return p -} - -// MustWaitStable is similar to [Page.WaitStable]. -func (p *Page) MustWaitStable() *Page { - p.e(p.WaitStable(time.Second)) - return p -} - -// MustWaitLoad is similar to [Page.WaitLoad]. -func (p *Page) MustWaitLoad() *Page { - p.e(p.WaitLoad()) - return p -} - -// MustAddScriptTag is similar to [Page.AddScriptTag]. -func (p *Page) MustAddScriptTag(url string) *Page { - p.e(p.AddScriptTag(url, "")) - return p -} - -// MustAddStyleTag is similar to [Page.AddStyleTag]. -func (p *Page) MustAddStyleTag(url string) *Page { - p.e(p.AddStyleTag(url, "")) - return p -} - -// MustEvalOnNewDocument is similar to [Page.EvalOnNewDocument]. -func (p *Page) MustEvalOnNewDocument(js string) { - _, err := p.EvalOnNewDocument(js) - p.e(err) -} - -// MustExpose is similar to [Page.Expose]. -func (p *Page) MustExpose(name string, fn func(gson.JSON) (interface{}, error)) (stop func()) { - s, err := p.Expose(name, fn) - p.e(err) - return func() { p.e(s()) } -} - -// MustEval is similar to [Page.Eval]. -func (p *Page) MustEval(js string, params ...interface{}) gson.JSON { - res, err := p.Eval(js, params...) - p.e(err) - return res.Value -} - -// MustEvaluate is similar to [Page.Evaluate]. -func (p *Page) MustEvaluate(opts *EvalOptions) *proto.RuntimeRemoteObject { - res, err := p.Evaluate(opts) - p.e(err) - return res -} - -// MustWait is similar to [Page.Wait]. -func (p *Page) MustWait(js string, params ...interface{}) *Page { - p.e(p.Wait(Eval(js, params...))) - return p -} - -// MustWaitElementsMoreThan is similar to [Page.WaitElementsMoreThan]. -func (p *Page) MustWaitElementsMoreThan(selector string, num int) *Page { - p.e(p.WaitElementsMoreThan(selector, num)) - return p -} - -// MustObjectToJSON is similar to [Page.ObjectToJSON]. -func (p *Page) MustObjectToJSON(obj *proto.RuntimeRemoteObject) gson.JSON { - j, err := p.ObjectToJSON(obj) - p.e(err) - return j -} - -// MustObjectsToJSON is similar to [Page.ObjectsToJSON]. -func (p *Page) MustObjectsToJSON(list []*proto.RuntimeRemoteObject) gson.JSON { - arr := []interface{}{} - for _, obj := range list { - j, err := p.ObjectToJSON(obj) - p.e(err) - arr = append(arr, j.Val()) - } - return gson.New(arr) -} - -// MustElementFromNode is similar to [Page.ElementFromNode]. -func (p *Page) MustElementFromNode(node *proto.DOMNode) *Element { - el, err := p.ElementFromNode(node) - p.e(err) - return el -} - -// MustElementFromPoint is similar to [Page.ElementFromPoint]. -func (p *Page) MustElementFromPoint(left, top int) *Element { - el, err := p.ElementFromPoint(left, top) - p.e(err) - return el -} - -// MustRelease is similar to [Page.Release]. -func (p *Page) MustRelease(obj *proto.RuntimeRemoteObject) *Page { - p.e(p.Release(obj)) - return p -} - -// MustHas is similar to [Page.Has]. -func (p *Page) MustHas(selector string) bool { - has, _, err := p.Has(selector) - p.e(err) - return has -} - -// MustHasX is similar to [Page.HasX]. -func (p *Page) MustHasX(selector string) bool { - has, _, err := p.HasX(selector) - p.e(err) - return has -} - -// MustHasR is similar to [Page.HasR]. -func (p *Page) MustHasR(selector, regex string) bool { - has, _, err := p.HasR(selector, regex) - p.e(err) - return has -} - -// MustSearch is similar to [Page.Search]. -// It only returns the first element in the search result. -func (p *Page) MustSearch(query string) *Element { - res, err := p.Search(query) - p.e(err) - res.Release() - return res.First -} - -// MustElement is similar to [Page.Element]. -func (p *Page) MustElement(selector string) *Element { - el, err := p.Element(selector) - p.e(err) - return el -} - -// MustElementR is similar to [Page.ElementR]. -func (p *Page) MustElementR(selector, jsRegex string) *Element { - el, err := p.ElementR(selector, jsRegex) - p.e(err) - return el -} - -// MustElementX is similar to [Page.ElementX]. -func (p *Page) MustElementX(xPath string) *Element { - el, err := p.ElementX(xPath) - p.e(err) - return el -} - -// MustElementByJS is similar to [Page.ElementByJS]. -func (p *Page) MustElementByJS(js string, params ...interface{}) *Element { - el, err := p.ElementByJS(Eval(js, params...)) - p.e(err) - return el -} - -// MustElements is similar to [Page.Elements]. -func (p *Page) MustElements(selector string) Elements { - list, err := p.Elements(selector) - p.e(err) - return list -} - -// MustElementsX is similar to [Page.ElementsX]. -func (p *Page) MustElementsX(xpath string) Elements { - list, err := p.ElementsX(xpath) - p.e(err) - return list -} - -// MustElementsByJS is similar to [Page.ElementsByJS]. -func (p *Page) MustElementsByJS(js string, params ...interface{}) Elements { - list, err := p.ElementsByJS(Eval(js, params...)) - p.e(err) - return list -} - -// MustElementByJS is similar to [RaceContext.ElementByJS]. -func (rc *RaceContext) MustElementByJS(js string, params []interface{}) *RaceContext { - return rc.ElementByJS(Eval(js, params...)) -} - -// MustHandle is similar to [RaceContext.Handle]. -func (rc *RaceContext) MustHandle(callback func(*Element)) *RaceContext { - return rc.Handle(func(e *Element) error { - callback(e) - return nil - }) -} - -// MustDo is similar to [RaceContext.Do]. -func (rc *RaceContext) MustDo() *Element { - el, err := rc.Do() - rc.page.e(err) - return el -} - -// MustMoveTo is similar to [Mouse.Move]. -func (m *Mouse) MustMoveTo(x, y float64) *Mouse { - m.page.e(m.MoveTo(proto.NewPoint(x, y))) - return m -} - -// MustScroll is similar to [Mouse.Scroll]. -func (m *Mouse) MustScroll(x, y float64) *Mouse { - m.page.e(m.Scroll(x, y, 0)) - return m -} - -// MustDown is similar to [Mouse.Down]. -func (m *Mouse) MustDown(button proto.InputMouseButton) *Mouse { - m.page.e(m.Down(button, 1)) - return m -} - -// MustUp is similar to [Mouse.Up]. -func (m *Mouse) MustUp(button proto.InputMouseButton) *Mouse { - m.page.e(m.Up(button, 1)) - return m -} - -// MustClick is similar to [Mouse.Click]. -func (m *Mouse) MustClick(button proto.InputMouseButton) *Mouse { - m.page.e(m.Click(button, 1)) - return m -} - -// MustType is similar to [Keyboard.Type]. -func (k *Keyboard) MustType(key ...input.Key) *Keyboard { - k.page.e(k.Type(key...)) - return k -} - -// MustDo is similar to [KeyActions.Do]. -func (ka *KeyActions) MustDo() { - ka.keyboard.page.e(ka.Do()) -} - -// MustInsertText is similar to [Page.InsertText]. -func (p *Page) MustInsertText(text string) *Page { - p.e(p.InsertText(text)) - return p -} - -// MustStart is similar to [Touch.Start]. -func (t *Touch) MustStart(points ...*proto.InputTouchPoint) *Touch { - t.page.e(t.Start(points...)) - return t -} - -// MustMove is similar to [Touch.Move]. -func (t *Touch) MustMove(points ...*proto.InputTouchPoint) *Touch { - t.page.e(t.Move(points...)) - return t -} - -// MustEnd is similar to [Touch.End]. -func (t *Touch) MustEnd() *Touch { - t.page.e(t.End()) - return t -} - -// MustCancel is similar to [Touch.Cancel]. -func (t *Touch) MustCancel() *Touch { - t.page.e(t.Cancel()) - return t -} - -// MustTap is similar to [Touch.Tap]. -func (t *Touch) MustTap(x, y float64) *Touch { - t.page.e(t.Tap(x, y)) - return t -} - -// WithPanic returns an element clone with the specified panic function. -// The fail must stop the current goroutine's execution immediately, such as use [runtime.Goexit] or panic inside it. -func (el *Element) WithPanic(fail func(interface{})) *Element { - n := *el - n.e = genE(fail) - return &n -} - -// MustDescribe is similar to [Element.Describe]. -func (el *Element) MustDescribe() *proto.DOMNode { - node, err := el.Describe(1, false) - el.e(err) - return node -} - -// MustShadowRoot is similar to [Element.ShadowRoot]. -func (el *Element) MustShadowRoot() *Element { - node, err := el.ShadowRoot() - el.e(err) - return node -} - -// MustFrame is similar to [Element.Frame]. -func (el *Element) MustFrame() *Page { - p, err := el.Frame() - el.e(err) - return p -} - -// MustFocus is similar to [Element.Focus]. -func (el *Element) MustFocus() *Element { - el.e(el.Focus()) - return el -} - -// MustScrollIntoView is similar to [Element.ScrollIntoView]. -func (el *Element) MustScrollIntoView() *Element { - el.e(el.ScrollIntoView()) - return el -} - -// MustHover is similar to [Element.Hover]. -func (el *Element) MustHover() *Element { - el.e(el.Hover()) - return el -} - -// MustClick is similar to [Element.Click]. -func (el *Element) MustClick() *Element { - el.e(el.Click(proto.InputMouseButtonLeft, 1)) - return el -} - -// MustDoubleClick is similar to [Element.Click]. -func (el *Element) MustDoubleClick() *Element { - el.e(el.Click(proto.InputMouseButtonLeft, 2)) - return el -} - -// MustTap is similar to [Element.Tap]. -func (el *Element) MustTap() *Element { - el.e(el.Tap()) - return el -} - -// MustInteractable is similar to [Element.Interactable]. -func (el *Element) MustInteractable() bool { - _, err := el.Interactable() - if errors.Is(err, &NotInteractableError{}) { - return false - } - el.e(err) - return true -} - -// MustWaitInteractable is similar to [Element.WaitInteractable]. -func (el *Element) MustWaitInteractable() *Element { - el.e(el.WaitInteractable()) - return el -} - -// MustType is similar to [Element.Type]. -func (el *Element) MustType(keys ...input.Key) *Element { - el.e(el.Type(keys...)) - return el -} - -// MustKeyActions is similar to [Element.KeyActions]. -func (el *Element) MustKeyActions() *KeyActions { - ka, err := el.KeyActions() - el.e(err) - return ka -} - -// MustSelectText is similar to [Element.SelectText]. -func (el *Element) MustSelectText(regex string) *Element { - el.e(el.SelectText(regex)) - return el -} - -// MustSelectAllText is similar to [Element.SelectAllText]. -func (el *Element) MustSelectAllText() *Element { - el.e(el.SelectAllText()) - return el -} - -// MustInput is similar to [Element.Input]. -func (el *Element) MustInput(text string) *Element { - el.e(el.Input(text)) - return el -} - -// MustInputTime is similar to [Element.Input]. -func (el *Element) MustInputTime(t time.Time) *Element { - el.e(el.InputTime(t)) - return el -} - -// MustInputColor is similar to [Element.InputColor]. -func (el *Element) MustInputColor(color string) *Element { - el.e(el.InputColor(color)) - return el -} - -// MustBlur is similar to [Element.Blur]. -func (el *Element) MustBlur() *Element { - el.e(el.Blur()) - return el -} - -// MustSelect is similar to [Element.Select]. -func (el *Element) MustSelect(selectors ...string) *Element { - el.e(el.Select(selectors, true, SelectorTypeText)) - return el -} - -// MustMatches is similar to [Element.Matches]. -func (el *Element) MustMatches(selector string) bool { - res, err := el.Matches(selector) - el.e(err) - return res -} - -// MustAttribute is similar to [Element.Attribute]. -func (el *Element) MustAttribute(name string) *string { - attr, err := el.Attribute(name) - el.e(err) - return attr -} - -// MustProperty is similar to [Element.Property]. -func (el *Element) MustProperty(name string) gson.JSON { - prop, err := el.Property(name) - el.e(err) - return prop -} - -// MustDisabled is similar to [Element.Disabled]. -func (el *Element) MustDisabled() bool { - disabled, err := el.Disabled() - el.e(err) - return disabled -} - -// MustContainsElement is similar to [Element.ContainsElement]. -func (el *Element) MustContainsElement(target *Element) bool { - contains, err := el.ContainsElement(target) - el.e(err) - return contains -} - -// MustSetFiles is similar to [Element.SetFiles]. -func (el *Element) MustSetFiles(paths ...string) *Element { - el.e(el.SetFiles(paths)) - return el -} - -// MustSetDocumentContent is similar to [Page.SetDocumentContent]. -func (p *Page) MustSetDocumentContent(html string) *Page { - p.e(p.SetDocumentContent(html)) - return p -} - -// MustText is similar to [Element.Text]. -func (el *Element) MustText() string { - s, err := el.Text() - el.e(err) - return s -} - -// MustHTML is similar to [Element.HTML]. -func (el *Element) MustHTML() string { - s, err := el.HTML() - el.e(err) - return s -} - -// MustVisible is similar to [Element.Visible]. -func (el *Element) MustVisible() bool { - v, err := el.Visible() - el.e(err) - return v -} - -// MustWaitLoad is similar to [Element.WaitLoad]. -func (el *Element) MustWaitLoad() *Element { - el.e(el.WaitLoad()) - return el -} - -// MustWaitStable is similar to [Element.WaitStable]. -func (el *Element) MustWaitStable() *Element { - el.e(el.WaitStable(300 * time.Millisecond)) - return el -} - -// MustWait is similar to [Element.Wait]. -func (el *Element) MustWait(js string, params ...interface{}) *Element { - el.e(el.Wait(Eval(js, params...))) - return el -} - -// MustWaitVisible is similar to [Element.WaitVisible]. -func (el *Element) MustWaitVisible() *Element { - el.e(el.WaitVisible()) - return el -} - -// MustWaitInvisible is similar to [Element.WaitInvisible].. -func (el *Element) MustWaitInvisible() *Element { - el.e(el.WaitInvisible()) - return el -} - -// MustWaitEnabled is similar to [Element.WaitEnabled]. -func (el *Element) MustWaitEnabled() *Element { - el.e(el.WaitEnabled()) - return el -} - -// MustWaitWritable is similar to [Element.WaitWritable]. -func (el *Element) MustWaitWritable() *Element { - el.e(el.WaitWritable()) - return el -} - -// MustShape is similar to [Element.Shape]. -func (el *Element) MustShape() *proto.DOMGetContentQuadsResult { - shape, err := el.Shape() - el.e(err) - return shape -} - -// MustCanvasToImage is similar to [Element.CanvasToImage]. -func (el *Element) MustCanvasToImage() []byte { - bin, err := el.CanvasToImage("", -1) - el.e(err) - return bin -} - -// MustResource is similar to [Element.Resource]. -func (el *Element) MustResource() []byte { - bin, err := el.Resource() - el.e(err) - return bin -} - -// MustBackgroundImage is similar to [Element.BackgroundImage]. -func (el *Element) MustBackgroundImage() []byte { - bin, err := el.BackgroundImage() - el.e(err) - return bin -} - -// MustScreenshot is similar to [Element.Screenshot]. -func (el *Element) MustScreenshot(toFile ...string) []byte { - bin, err := el.Screenshot(proto.PageCaptureScreenshotFormatPng, 0) - el.e(err) - el.e(saveFile(saveFileTypeScreenshot, bin, toFile)) - return bin -} - -// MustRelease is similar to [Element.Release]. -func (el *Element) MustRelease() { - el.e(el.Release()) -} - -// MustRemove is similar to [Element.Remove]. -func (el *Element) MustRemove() { - el.e(el.Remove()) -} - -// MustEval is similar to [Element.Eval]. -func (el *Element) MustEval(js string, params ...interface{}) gson.JSON { - res, err := el.Eval(js, params...) - el.e(err) - return res.Value -} - -// MustHas is similar to [Element.Has]. -func (el *Element) MustHas(selector string) bool { - has, _, err := el.Has(selector) - el.e(err) - return has -} - -// MustHasX is similar to [Element.HasX]. -func (el *Element) MustHasX(selector string) bool { - has, _, err := el.HasX(selector) - el.e(err) - return has -} - -// MustHasR is similar to [Element.HasR]. -func (el *Element) MustHasR(selector, regex string) bool { - has, _, err := el.HasR(selector, regex) - el.e(err) - return has -} - -// MustElement is similar to [Element.Element]. -func (el *Element) MustElement(selector string) *Element { - el, err := el.Element(selector) - el.e(err) - return el -} - -// MustElementX is similar to [Element.ElementX]. -func (el *Element) MustElementX(xpath string) *Element { - el, err := el.ElementX(xpath) - el.e(err) - return el -} - -// MustElementByJS is similar to [Element.ElementByJS]. -func (el *Element) MustElementByJS(js string, params ...interface{}) *Element { - el, err := el.ElementByJS(Eval(js, params...)) - el.e(err) - return el -} - -// MustParent is similar to [Element.Parent]. -func (el *Element) MustParent() *Element { - parent, err := el.Parent() - el.e(err) - return parent -} - -// MustParents is similar to [Element.Parents]. -func (el *Element) MustParents(selector string) Elements { - list, err := el.Parents(selector) - el.e(err) - return list -} - -// MustNext is similar to [Element.Next]. -func (el *Element) MustNext() *Element { - parent, err := el.Next() - el.e(err) - return parent -} - -// MustPrevious is similar to [Element.Previous]. -func (el *Element) MustPrevious() *Element { - parent, err := el.Previous() - el.e(err) - return parent -} - -// MustElementR is similar to [Element.ElementR]. -func (el *Element) MustElementR(selector, regex string) *Element { - sub, err := el.ElementR(selector, regex) - el.e(err) - return sub -} - -// MustElements is similar to [Element.Elements]. -func (el *Element) MustElements(selector string) Elements { - list, err := el.Elements(selector) - el.e(err) - return list -} - -// MustElementsX is similar to [Element.ElementsX]. -func (el *Element) MustElementsX(xpath string) Elements { - list, err := el.ElementsX(xpath) - el.e(err) - return list -} - -// MustElementsByJS is similar to [Element.ElementsByJS]. -func (el *Element) MustElementsByJS(js string, params ...interface{}) Elements { - list, err := el.ElementsByJS(Eval(js, params...)) - el.e(err) - return list -} - -// MustAdd is similar to [HijackRouter.Add]. -func (r *HijackRouter) MustAdd(pattern string, handler func(*Hijack)) *HijackRouter { - r.browser.e(r.Add(pattern, "", handler)) - return r -} - -// MustRemove is similar to [HijackRouter.Remove]. -func (r *HijackRouter) MustRemove(pattern string) *HijackRouter { - r.browser.e(r.Remove(pattern)) - return r -} - -// MustStop is similar to [HijackRouter.Stop]. -func (r *HijackRouter) MustStop() { - r.browser.e(r.Stop()) -} - -// MustLoadResponse is similar to [Hijack.LoadResponse]. -func (h *Hijack) MustLoadResponse() { - h.browser.e(h.LoadResponse(http.DefaultClient, true)) -} - -// MustEqual is similar to [Element.Equal]. -func (el *Element) MustEqual(elm *Element) bool { - res, err := el.Equal(elm) - el.e(err) - return res -} - -// MustMoveMouseOut is similar to [Element.MoveMouseOut]. -func (el *Element) MustMoveMouseOut() *Element { - el.e(el.MoveMouseOut()) - return el -} - -// MustGetXPath is similar to [Element.GetXPath]. -func (el *Element) MustGetXPath(optimized bool) string { - xpath, err := el.GetXPath(optimized) - el.e(err) - return xpath -} - -// MustGet an elem from the pool. Use the [Pool[T].Put] to make it reusable later. -func (p Pool[T]) MustGet(create func() *T) *T { - elem := <-p - if elem == nil { - elem = create() - } - return elem -} diff --git a/internal/rod/page.go b/internal/rod/page.go deleted file mode 100644 index f646ece..0000000 --- a/internal/rod/page.go +++ /dev/null @@ -1,1061 +0,0 @@ -package rod - -import ( - "context" - "crypto/md5" - "encoding/base64" - "encoding/hex" - "errors" - "fmt" - "sync" - "time" - - "github.com/go-rod/rod/lib/cdp" - "github.com/go-rod/rod/lib/devices" - "github.com/go-rod/rod/lib/js" - "github.com/go-rod/rod/lib/proto" - "github.com/go-rod/rod/lib/utils" - "github.com/ysmood/goob" - "github.com/ysmood/got/lib/lcs" - "github.com/ysmood/gson" -) - -// Page implements these interfaces. -var ( - _ proto.Client = &Page{} - _ proto.Contextable = &Page{} - _ proto.Sessionable = &Page{} -) - -// Page represents the webpage. -// We try to hold as less states as possible. -// When a page is closed by Rod or not all the ongoing operations an events on it will abort. -type Page struct { - // TargetID is a unique ID for a remote page. - // It's usually used in events sent from the browser to tell which page an event belongs to. - TargetID proto.TargetTargetID - - // FrameID is a unique ID for a browsing context. - // Usually, different FrameID means different javascript execution context. - // Such as an iframe and the page it belongs to will have the same TargetID but different FrameIDs. - FrameID proto.PageFrameID - - // SessionID is a unique ID for a page attachment to a controller. - // It's usually used in transport layer to tell which page to send the control signal. - // A page can attached to multiple controllers, the browser uses it distinguish controllers. - SessionID proto.TargetSessionID - - e eFunc - - ctx context.Context - - // Used to abort all ongoing actions when a page closes. - sessionCancel func() - - root *Page - - sleeper func() utils.Sleeper - - browser *Browser - event *goob.Observable - - // devices - Mouse *Mouse - Keyboard *Keyboard - Touch *Touch - - element *Element // iframe only - - jsCtxLock *sync.Mutex - jsCtxID *proto.RuntimeRemoteObjectID // use pointer so that page clones can share the change - helpersLock *sync.Mutex - helpers map[proto.RuntimeRemoteObjectID]map[string]proto.RuntimeRemoteObjectID -} - -// String interface. -func (p *Page) String() string { - id := p.TargetID - if len(id) > 8 { - id = id[:8] - } - return fmt.Sprintf("", id) -} - -// IsIframe tells if it's iframe. -func (p *Page) IsIframe() bool { - return p.element != nil -} - -// GetSessionID interface. -func (p *Page) GetSessionID() proto.TargetSessionID { - return p.SessionID -} - -// Browser of the page. -func (p *Page) Browser() *Browser { - return p.browser -} - -// Info of the page, such as the URL or title of the page. -func (p *Page) Info() (*proto.TargetTargetInfo, error) { - return p.browser.pageInfo(p.TargetID) -} - -// HTML of the page. -func (p *Page) HTML() (string, error) { - el, err := p.Element("html") - if err != nil { - return "", err - } - return el.HTML() -} - -// Cookies returns the page cookies. By default it will return the cookies for current page. -// The urls is the list of URLs for which applicable cookies will be fetched. -func (p *Page) Cookies(urls []string) ([]*proto.NetworkCookie, error) { - if len(urls) == 0 { - info, err := p.Info() - if err != nil { - return nil, err - } - urls = []string{info.URL} - } - - res, err := proto.NetworkGetCookies{Urls: urls}.Call(p) - if err != nil { - return nil, err - } - return res.Cookies, nil -} - -// SetCookies is similar to Browser.SetCookies . -func (p *Page) SetCookies(cookies []*proto.NetworkCookieParam) error { - if cookies == nil { - return proto.NetworkClearBrowserCookies{}.Call(p) - } - return proto.NetworkSetCookies{Cookies: cookies}.Call(p) -} - -// SetExtraHeaders whether to always send extra HTTP headers with the requests from this page. -func (p *Page) SetExtraHeaders(dict []string) (func(), error) { - headers := proto.NetworkHeaders{} - - for i := 0; i < len(dict); i += 2 { - headers[dict[i]] = gson.New(dict[i+1]) - } - - return p.EnableDomain(&proto.NetworkEnable{}), proto.NetworkSetExtraHTTPHeaders{Headers: headers}.Call(p) -} - -// SetUserAgent (browser brand, accept-language, etc) of the page. -// If req is nil, a default user agent will be used, a typical mac chrome. -func (p *Page) SetUserAgent(req *proto.NetworkSetUserAgentOverride) error { - if req == nil { - req = devices.LaptopWithMDPIScreen.UserAgentEmulation() - } - return req.Call(p) -} - -// SetBlockedURLs For some requests that do not want to be triggered, -// such as some dangerous operations, delete, quit logout, etc. -// Wildcards ('*') are allowed, such as ["*/api/logout/*","delete"]. -// NOTE: if you set empty pattern "", it will block all requests. -func (p *Page) SetBlockedURLs(urls []string) error { - if len(urls) == 0 { - return nil - } - return proto.NetworkSetBlockedURLs{Urls: urls}.Call(p) -} - -// Navigate to the url. If the url is empty, "about:blank" will be used. -// It will return immediately after the server responds the http header. -func (p *Page) Navigate(url string) error { - if url == "" { - url = "about:blank" - } - - // try to stop loading - _ = p.StopLoading() - - res, err := proto.PageNavigate{URL: url}.Call(p) - if err != nil { - return err - } - if res.ErrorText != "" { - return &NavigationError{res.ErrorText} - } - - p.root.unsetJSCtxID() - - return nil -} - -// NavigateBack history. -func (p *Page) NavigateBack() error { - // Not using cdp API because it doesn't work for iframe - _, err := p.Evaluate(Eval(`() => history.back()`).ByUser()) - return err -} - -// ResetNavigationHistory reset history. -func (p *Page) ResetNavigationHistory() error { - err := proto.PageResetNavigationHistory{}.Call(p) - return err -} - -// GetNavigationHistory get navigation history. -func (p *Page) GetNavigationHistory() (*proto.PageGetNavigationHistoryResult, error) { - return proto.PageGetNavigationHistory{}.Call(p) -} - -// NavigateForward history. -func (p *Page) NavigateForward() error { - // Not using cdp API because it doesn't work for iframe - _, err := p.Evaluate(Eval(`() => history.forward()`).ByUser()) - return err -} - -// Reload page. -func (p *Page) Reload() error { - p, cancel := p.WithCancel() - defer cancel() - - wait := p.EachEvent(func(e *proto.PageFrameNavigated) bool { - return e.Frame.ID == p.FrameID - }) - - // Not using cdp API because it doesn't work for iframe - _, err := p.Evaluate(Eval(`() => location.reload()`).ByUser()) - if err != nil { - return err - } - - wait() - - p.unsetJSCtxID() - - return nil -} - -// Activate (focuses) the page. -func (p *Page) Activate() (*Page, error) { - err := proto.TargetActivateTarget{TargetID: p.TargetID}.Call(p.browser) - return p, err -} - -func (p *Page) getWindowID() (proto.BrowserWindowID, error) { - res, err := proto.BrowserGetWindowForTarget{TargetID: p.TargetID}.Call(p) - if err != nil { - return 0, err - } - return res.WindowID, err -} - -// GetWindow position and size info. -func (p *Page) GetWindow() (*proto.BrowserBounds, error) { - id, err := p.getWindowID() - if err != nil { - return nil, err - } - - res, err := proto.BrowserGetWindowBounds{WindowID: id}.Call(p) - if err != nil { - return nil, err - } - - return res.Bounds, nil -} - -// SetWindow location and size. -func (p *Page) SetWindow(bounds *proto.BrowserBounds) error { - id, err := p.getWindowID() - if err != nil { - return err - } - - err = proto.BrowserSetWindowBounds{WindowID: id, Bounds: bounds}.Call(p) - return err -} - -// SetViewport overrides the values of device screen dimensions. -func (p *Page) SetViewport(params *proto.EmulationSetDeviceMetricsOverride) error { - if params == nil { - return proto.EmulationClearDeviceMetricsOverride{}.Call(p) - } - return params.Call(p) -} - -// SetDocumentContent sets the page document html content. -func (p *Page) SetDocumentContent(html string) error { - return proto.PageSetDocumentContent{ - FrameID: p.FrameID, - HTML: html, - }.Call(p) -} - -// Emulate the device, such as iPhone9. If device is devices.Clear, it will clear the override. -func (p *Page) Emulate(device devices.Device) error { - err := p.SetViewport(device.MetricsEmulation()) - if err != nil { - return err - } - - err = device.TouchEmulation().Call(p) - if err != nil { - return err - } - - return p.SetUserAgent(device.UserAgentEmulation()) -} - -// StopLoading forces the page stop navigation and pending resource fetches. -func (p *Page) StopLoading() error { - return proto.PageStopLoading{}.Call(p) -} - -// Close tries to close page, running its beforeunload hooks, if has any. -func (p *Page) Close() error { - p.browser.targetsLock.Lock() - defer p.browser.targetsLock.Unlock() - - success := true - ctx, cancel := context.WithCancel(p.ctx) - defer cancel() - messages := p.browser.Context(ctx).Event() - - for { - err := proto.PageClose{}.Call(p) - if errors.Is(err, cdp.ErrNotAttachedToActivePage) { - // TODO: I don't know why chromium doesn't allow us to close a page while it's navigating. - // Looks like a bug in chromium. - utils.Sleep(0.1) - continue - } else if err != nil { - return err - } - break - } - - for msg := range messages { - stop := false - - destroyed := proto.TargetTargetDestroyed{} - closed := proto.PageJavascriptDialogClosed{} - if msg.Load(&destroyed) { - stop = destroyed.TargetID == p.TargetID - } else if msg.SessionID == p.SessionID && msg.Load(&closed) { - success = closed.Result - stop = !success - } - - if stop { - break - } - } - - if success { - p.cleanupStates() - } else { - return &PageCloseCanceledError{} - } - - return nil -} - -// TriggerFavicon supports when browser in headless mode -// to trigger favicon's request. Pay attention to this -// function only supported when browser in headless mode, -// if you call it in no-headless mode, it will raise an error -// with the message "browser is no-headless". -func (p *Page) TriggerFavicon() error { - // check if browser whether in headless mode - // if not in headless mode then raise error - if !p.browser.isHeadless() { - return errors.New("browser is no-headless") - } - - _, err := p.Evaluate(evalHelper(js.TriggerFavicon).ByPromise()) - if err != nil { - return err - } - return nil -} - -// HandleDialog accepts or dismisses next JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload). -// Because modal dialog will block js, usually you have to trigger the dialog in another goroutine. -// For example: -// -// wait, handle := page.MustHandleDialog() -// go page.MustElement("button").MustClick() -// wait() -// handle(true, "") -func (p *Page) HandleDialog() ( - wait func() *proto.PageJavascriptDialogOpening, - handle func(*proto.PageHandleJavaScriptDialog) error, -) { - restore := p.EnableDomain(&proto.PageEnable{}) - - var e proto.PageJavascriptDialogOpening - w := p.WaitEvent(&e) - - return func() *proto.PageJavascriptDialogOpening { - w() - return &e - }, func(h *proto.PageHandleJavaScriptDialog) error { - defer restore() - return h.Call(p) - } -} - -// HandleFileDialog return a functions that waits for the next file chooser dialog pops up and returns the element -// for the event. -func (p *Page) HandleFileDialog() (func([]string) error, error) { - err := proto.PageSetInterceptFileChooserDialog{Enabled: true}.Call(p) - if err != nil { - return nil, err - } - - var e proto.PageFileChooserOpened - w := p.WaitEvent(&e) - - return func(paths []string) error { - w() - - err := proto.PageSetInterceptFileChooserDialog{Enabled: false}.Call(p) - if err != nil { - return err - } - - return proto.DOMSetFileInputFiles{ - Files: utils.AbsolutePaths(paths), - BackendNodeID: e.BackendNodeID, - }.Call(p) - }, nil -} - -// Screenshot captures the screenshot of current page. -func (p *Page) Screenshot(fullPage bool, req *proto.PageCaptureScreenshot) ([]byte, error) { - if req == nil { - req = &proto.PageCaptureScreenshot{} - } - if fullPage { - metrics, err := proto.PageGetLayoutMetrics{}.Call(p) - if err != nil { - return nil, err - } - - if metrics.CSSContentSize == nil { - return nil, errors.New("failed to get css content size") - } - - oldView := proto.EmulationSetDeviceMetricsOverride{} - set := p.LoadState(&oldView) - view := oldView - view.Width = int(metrics.CSSContentSize.Width) - view.Height = int(metrics.CSSContentSize.Height) - - err = p.SetViewport(&view) - if err != nil { - return nil, err - } - - defer func() { // try to recover the viewport - if !set { - _ = proto.EmulationClearDeviceMetricsOverride{}.Call(p) - return - } - - _ = p.SetViewport(&oldView) - }() - } - - shot, err := req.Call(p) - if err != nil { - return nil, err - } - return shot.Data, nil -} - -// ScrollScreenshotOptions is the options for the ScrollScreenshot. -type ScrollScreenshotOptions struct { - // Format (optional) Image compression format (defaults to png). - Format proto.PageCaptureScreenshotFormat `json:"format,omitempty"` - - // Quality (optional) Compression quality from range [0..100] (jpeg only). - Quality *int `json:"quality,omitempty"` - - // FixedTop (optional) The number of pixels to skip from the top. - // It is suitable for optimizing the screenshot effect when there is a fixed - // positioning element at the top of the page. - FixedTop float64 - - // FixedBottom (optional) The number of pixels to skip from the bottom. - FixedBottom float64 - - // WaitPerScroll until no animation (default is 300ms) - WaitPerScroll time.Duration -} - -// ScrollScreenshot Scroll screenshot does not adjust the size of the viewport, -// but achieves it by scrolling and capturing screenshots in a loop, and then stitching them together. -// Note that this method also has a flaw: when there are elements with fixed -// positioning on the page (usually header navigation components), -// these elements will appear repeatedly, you can set the FixedTop parameter to optimize it. -// -// Only support png and jpeg format yet, webP is not supported because no suitable processing -// library was found in golang. -func (p *Page) ScrollScreenshot(opt *ScrollScreenshotOptions) ([]byte, error) { - if opt == nil { - opt = &ScrollScreenshotOptions{} - } - if opt.WaitPerScroll == 0 { - opt.WaitPerScroll = time.Millisecond * 300 - } - - metrics, err := proto.PageGetLayoutMetrics{}.Call(p) - if err != nil { - return nil, err - } - - if metrics.CSSContentSize == nil || metrics.CSSVisualViewport == nil { - return nil, errors.New("failed to get css content size") - } - - viewpointHeight := metrics.CSSVisualViewport.ClientHeight - contentHeight := metrics.CSSContentSize.Height - - var scrollTop float64 - var images []utils.ImgWithBox - - for { - clip := &proto.PageViewport{ - X: 0, - Y: scrollTop, - Width: metrics.CSSVisualViewport.ClientWidth, - Scale: 1, - } - - scrollY := viewpointHeight - (opt.FixedTop + opt.FixedBottom) - if scrollTop+viewpointHeight > contentHeight { - clip.Height = contentHeight - scrollTop - } else { - clip.Height = scrollY - if scrollTop != 0 { - clip.Y += opt.FixedTop - } - } - - req := &proto.PageCaptureScreenshot{ - Format: opt.Format, - Quality: opt.Quality, - Clip: clip, - FromSurface: false, - CaptureBeyondViewport: false, - OptimizeForSpeed: false, - } - shot, err := req.Call(p) - if err != nil { - return nil, err - } - - images = append(images, utils.ImgWithBox{Img: shot.Data}) - - scrollTop += scrollY - if scrollTop >= contentHeight { - break - } - - err = p.Mouse.Scroll(0, scrollY, 1) - if err != nil { - return nil, fmt.Errorf("scroll error: %w", err) - } - - err = p.WaitDOMStable(opt.WaitPerScroll, 0) - if err != nil { - return nil, fmt.Errorf("WaitDOMStable error: %w", err) - } - } - - var imgOption *utils.ImgOption - if opt.Quality != nil { - imgOption = &utils.ImgOption{ - Quality: *opt.Quality, - } - } - bs, err := utils.SplicePngVertical(images, opt.Format, imgOption) - if err != nil { - return nil, err - } - - return bs, nil -} - -// CaptureDOMSnapshot Returns a document snapshot, including the full DOM tree of the root node -// (including iframes, template contents, and imported documents) in a flattened array, -// as well as layout and white-listed computed style information for the nodes. -// Shadow DOM in the returned DOM tree is flattened. -// `Documents` The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document. -// `Strings` Shared string table that all string properties refer to with indexes. -// Normally use `Strings` is enough. -func (p *Page) CaptureDOMSnapshot() (domSnapshot *proto.DOMSnapshotCaptureSnapshotResult, err error) { - _ = proto.DOMSnapshotEnable{}.Call(p) - - snapshot, err := proto.DOMSnapshotCaptureSnapshot{ - ComputedStyles: []string{}, - IncludePaintOrder: true, - IncludeDOMRects: true, - IncludeBlendedBackgroundColors: true, - IncludeTextColorOpacities: true, - }.Call(p) - if err != nil { - return nil, err - } - return snapshot, nil -} - -// PDF prints page as PDF. -func (p *Page) PDF(req *proto.PagePrintToPDF) (*StreamReader, error) { - req.TransferMode = proto.PagePrintToPDFTransferModeReturnAsStream - res, err := req.Call(p) - if err != nil { - return nil, err - } - - return NewStreamReader(p, res.Stream), nil -} - -// GetResource content by the url. Such as image, css, html, etc. -// Use the [proto.PageGetResourceTree] to list all the resources. -func (p *Page) GetResource(url string) ([]byte, error) { - res, err := proto.PageGetResourceContent{ - FrameID: p.FrameID, - URL: url, - }.Call(p) - if err != nil { - return nil, err - } - - data := res.Content - - var bin []byte - if res.Base64Encoded { - bin, err = base64.StdEncoding.DecodeString(data) - utils.E(err) - } else { - bin = []byte(data) - } - - return bin, nil -} - -// WaitOpen waits for the next new page opened by the current one. -func (p *Page) WaitOpen() func() (*Page, error) { - var targetID proto.TargetTargetID - - b := p.browser.Context(p.ctx) - wait := b.EachEvent(func(e *proto.TargetTargetCreated) bool { - targetID = e.TargetInfo.TargetID - return e.TargetInfo.OpenerID == p.TargetID - }) - - return func() (*Page, error) { - defer p.tryTrace(TraceTypeWait, "wait open")() - wait() - return b.PageFromTarget(targetID) - } -} - -// EachEvent of the specified event types, if any callback returns true the wait function will resolve, -// The type of each callback is (? means optional): -// -// func(proto.Event, proto.TargetSessionID?) bool? -// -// You can listen to multiple event types at the same time like: -// -// browser.EachEvent(func(a *proto.A) {}, func(b *proto.B) {}) -// -// Such as subscribe the events to know when the navigation is complete or when the page is rendered. -// Here's an example to dismiss all dialogs/alerts on the page: -// -// go page.EachEvent(func(e *proto.PageJavascriptDialogOpening) { -// _ = proto.PageHandleJavaScriptDialog{ Accept: false, PromptText: ""}.Call(page) -// })() -func (p *Page) EachEvent(callbacks ...interface{}) (wait func()) { - return p.browser.Context(p.ctx).eachEvent(p.SessionID, callbacks...) -} - -// WaitEvent waits for the next event for one time. It will also load the data into the event object. -func (p *Page) WaitEvent(e proto.Event) (wait func()) { - defer p.tryTrace(TraceTypeWait, "event", e.ProtoEvent())() - return p.browser.Context(p.ctx).waitEvent(p.SessionID, e) -} - -// WaitNavigation wait for a page lifecycle event when navigating. -// Usually you will wait for [proto.PageLifecycleEventNameNetworkAlmostIdle]. -func (p *Page) WaitNavigation(name proto.PageLifecycleEventName) func() { - _ = proto.PageSetLifecycleEventsEnabled{Enabled: true}.Call(p) - - wait := p.EachEvent(func(e *proto.PageLifecycleEvent) bool { - return e.Name == name - }) - - return func() { - defer p.tryTrace(TraceTypeWait, "navigation", name)() - wait() - _ = proto.PageSetLifecycleEventsEnabled{Enabled: false}.Call(p) - } -} - -// WaitRequestIdle returns a wait function that waits until no request for d duration. -// Be careful, d is not the max wait timeout, it's the least idle time. -// If you want to set a timeout you can use the [Page.Timeout] function. -// Use the includes and excludes regexp list to filter the requests by their url. -func (p *Page) WaitRequestIdle( - d time.Duration, - includes, excludes []string, - excludeTypes []proto.NetworkResourceType, -) func() { - defer p.tryTrace(TraceTypeWait, "request-idle")() - - if excludeTypes == nil { - excludeTypes = []proto.NetworkResourceType{ - proto.NetworkResourceTypeWebSocket, - proto.NetworkResourceTypeEventSource, - proto.NetworkResourceTypeMedia, - proto.NetworkResourceTypeImage, - proto.NetworkResourceTypeFont, - } - } - - if len(includes) == 0 { - includes = []string{""} - } - - p, cancel := p.WithCancel() - match := genRegMatcher(includes, excludes) - waitList := map[proto.NetworkRequestID]string{} - idleCounter := utils.NewIdleCounter(d) - update := p.tryTraceReq(includes, excludes) - update(nil) - - checkDone := func(id proto.NetworkRequestID) { - if _, has := waitList[id]; has { - delete(waitList, id) - update(waitList) - idleCounter.Done() - } - } - - wait := p.EachEvent(func(sent *proto.NetworkRequestWillBeSent) { - for _, t := range excludeTypes { - if sent.Type == t { - return - } - } - - if match(sent.Request.URL) { - // Redirect will send multiple NetworkRequestWillBeSent events with the same RequestID, - // we should filter them out. - if _, has := waitList[sent.RequestID]; !has { - waitList[sent.RequestID] = sent.Request.URL - update(waitList) - idleCounter.Add() - } - } - }, func(e *proto.NetworkLoadingFinished) { - checkDone(e.RequestID) - }, func(e *proto.NetworkLoadingFailed) { - checkDone(e.RequestID) - }) - - return func() { - go func() { - idleCounter.Wait(p.ctx) - cancel() - }() - wait() - } -} - -// WaitDOMStable waits until the change of the DOM tree is less or equal than diff percent for d duration. -// Be careful, d is not the max wait timeout, it's the least stable time. -// If you want to set a timeout you can use the [Page.Timeout] function. -func (p *Page) WaitDOMStable(d time.Duration, diff float64) error { - defer p.tryTrace(TraceTypeWait, "dom-stable")() - - domSnapshot, err := p.CaptureDOMSnapshot() - if err != nil { - return err - } - - t := time.NewTicker(d) - defer t.Stop() - - for { - select { - case <-t.C: - case <-p.ctx.Done(): - return p.ctx.Err() - } - - currentDomSnapshot, err := p.CaptureDOMSnapshot() - if err != nil { - return err - } - - xs := lcs.NewWords(domSnapshot.Strings) - ys := lcs.NewWords(currentDomSnapshot.Strings) - lcs := xs.YadLCS(p.ctx, ys) - - df := 1 - float64(len(lcs))/float64(len(ys)) - if df <= diff { - break - } - - domSnapshot = currentDomSnapshot - } - return nil -} - -// WaitStable waits until the page is stable for d duration. -func (p *Page) WaitStable(d time.Duration) error { - defer p.tryTrace(TraceTypeWait, "stable")() - - var err error - - setErr := sync.Once{} - - utils.All(func() { - e := p.WaitLoad() - setErr.Do(func() { err = e }) - }, func() { - p.WaitRequestIdle(d, nil, nil, nil)() - }, func() { - e := p.WaitDOMStable(d, 0) - setErr.Do(func() { err = e }) - })() - - return err -} - -// WaitIdle waits until the next window.requestIdleCallback is called. -func (p *Page) WaitIdle(timeout time.Duration) (err error) { - _, err = p.Evaluate(evalHelper(js.WaitIdle, timeout.Milliseconds()).ByPromise()) - return err -} - -// WaitRepaint waits until the next repaint. -// Doc: https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame -func (p *Page) WaitRepaint() error { - // we use root here because iframe doesn't trigger requestAnimationFrame - _, err := p.root.Eval(`() => new Promise(r => requestAnimationFrame(r))`) - return err -} - -// WaitLoad waits for the `window.onload` event, it returns immediately if the event is already fired. -func (p *Page) WaitLoad() error { - defer p.tryTrace(TraceTypeWait, "load")() - _, err := p.Evaluate(evalHelper(js.WaitLoad).ByPromise()) - return err -} - -// AddScriptTag to page. If url is empty, content will be used. -func (p *Page) AddScriptTag(url, content string) error { - hash := md5.Sum([]byte(url + content)) - id := hex.EncodeToString(hash[:]) - _, err := p.Evaluate(evalHelper(js.AddScriptTag, id, url, content).ByPromise()) - return err -} - -// AddStyleTag to page. If url is empty, content will be used. -func (p *Page) AddStyleTag(url, content string) error { - hash := md5.Sum([]byte(url + content)) - id := hex.EncodeToString(hash[:]) - _, err := p.Evaluate(evalHelper(js.AddStyleTag, id, url, content).ByPromise()) - return err -} - -// EvalOnNewDocument Evaluates given script in every frame upon creation (before loading frame's scripts). -func (p *Page) EvalOnNewDocument(js string) (remove func() error, err error) { - res, err := proto.PageAddScriptToEvaluateOnNewDocument{Source: js}.Call(p) - if err != nil { - return - } - - remove = func() error { - return proto.PageRemoveScriptToEvaluateOnNewDocument{ - Identifier: res.Identifier, - }.Call(p) - } - - return -} - -// Wait until the js returns true. -func (p *Page) Wait(opts *EvalOptions) error { - return utils.Retry(p.ctx, p.sleeper(), func() (bool, error) { - res, err := p.Evaluate(opts) - if err != nil { - return true, err - } - - return res.Value.Bool(), nil - }) -} - -// WaitElementsMoreThan waits until there are more than num elements that match the selector. -func (p *Page) WaitElementsMoreThan(selector string, num int) error { - return p.Wait(Eval(`(s, n) => document.querySelectorAll(s).length > n`, selector, num)) -} - -// ObjectToJSON by object id. -func (p *Page) ObjectToJSON(obj *proto.RuntimeRemoteObject) (gson.JSON, error) { - if obj.ObjectID == "" { - return obj.Value, nil - } - - res, err := proto.RuntimeCallFunctionOn{ - ObjectID: obj.ObjectID, - FunctionDeclaration: `function() { return this }`, - ReturnByValue: true, - }.Call(p) - if err != nil { - return gson.New(nil), err - } - return res.Result.Value, nil -} - -// ElementFromObject creates an Element from the remote object id. -func (p *Page) ElementFromObject(obj *proto.RuntimeRemoteObject) (*Element, error) { - // If the element is in an iframe, we need the jsCtxID to inject helper.js to the correct context. - id, err := p.jsCtxIDByObjectID(obj.ObjectID) - if err != nil { - return nil, err - } - - pid, err := p.getJSCtxID() - if err != nil { - return nil, err - } - - if id != pid { - clone := *p - clone.jsCtxID = &id - p = &clone - } - - return &Element{ - e: p.e, - ctx: p.ctx, - sleeper: p.sleeper, - page: p, - Object: obj, - }, nil -} - -// ElementFromNode creates an Element from the node, [proto.DOMNodeID] or [proto.DOMBackendNodeID] must be specified. -func (p *Page) ElementFromNode(node *proto.DOMNode) (*Element, error) { - res, err := proto.DOMResolveNode{ - NodeID: node.NodeID, - BackendNodeID: node.BackendNodeID, - }.Call(p) - if err != nil { - return nil, err - } - - el, err := p.ElementFromObject(res.Object) - if err != nil { - return nil, err - } - - // make sure always return an element node - desc, err := el.Describe(0, false) - if err != nil { - return nil, err - } - if desc.NodeName == "#text" { - el, err = el.Parent() - if err != nil { - return nil, err - } - } - - return el, nil -} - -// ElementFromPoint creates an Element from the absolute point on the page. -// The point should include the window scroll offset. -func (p *Page) ElementFromPoint(x, y int) (*Element, error) { - node, err := proto.DOMGetNodeForLocation{X: x, Y: y}.Call(p) - if err != nil { - return nil, err - } - - return p.ElementFromNode(&proto.DOMNode{ - BackendNodeID: node.BackendNodeID, - }) -} - -// Release the remote object. Usually, you don't need to call it. -// When a page is closed or reloaded, all remote objects will be released automatically. -// It's useful if the page never closes or reloads. -func (p *Page) Release(obj *proto.RuntimeRemoteObject) error { - err := proto.RuntimeReleaseObject{ObjectID: obj.ObjectID}.Call(p) - return err -} - -// Call implements the [proto.Client]. -func (p *Page) Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) { - return p.browser.Call(ctx, sessionID, methodName, params) -} - -// Event of the page. -func (p *Page) Event() <-chan *Message { - dst := make(chan *Message) - s := p.event.Subscribe(p.ctx) - - go func() { - defer close(dst) - for { - select { - case <-p.ctx.Done(): - return - case msg, ok := <-s: - if !ok { - return - } - select { - case <-p.ctx.Done(): - return - case dst <- msg.(*Message): //nolint: forcetypeassert - } - } - } - }() - - return dst -} - -func (p *Page) initEvents() { - p.event = goob.New(p.ctx) - event := p.browser.Context(p.ctx).Event() - - go func() { - for msg := range event { - detached := proto.TargetDetachedFromTarget{} - destroyed := proto.TargetTargetDestroyed{} - - if (msg.Load(&detached) && detached.SessionID == p.SessionID) || - (msg.Load(destroyed) && destroyed.TargetID == p.TargetID) { - p.sessionCancel() - return - } - - if msg.SessionID != p.SessionID { - continue - } - - p.event.Publish(msg) - } - }() -} diff --git a/internal/rod/page_eval.go b/internal/rod/page_eval.go deleted file mode 100644 index c57ca4c..0000000 --- a/internal/rod/page_eval.go +++ /dev/null @@ -1,380 +0,0 @@ -// This file serves for the Page.Evaluate. - -package rod - -import ( - "errors" - "fmt" - "strings" - "time" - - "github.com/go-rod/rod/lib/cdp" - "github.com/go-rod/rod/lib/js" - "github.com/go-rod/rod/lib/proto" - "github.com/go-rod/rod/lib/utils" - "github.com/ysmood/gson" -) - -// EvalOptions for Page.Evaluate. -type EvalOptions struct { - // If enabled the eval result will be a plain JSON value. - // If disabled the eval result will be a reference of a remote js object. - ByValue bool - - AwaitPromise bool - - // ThisObj represents the "this" object in the JS - ThisObj *proto.RuntimeRemoteObject - - // JS function definition to execute. - JS string - - // JSArgs represents the arguments that will be passed to JS. - // If an argument is [*proto.RuntimeRemoteObject] type, the corresponding remote object will be used. - // Or it will be passed as a plain JSON value. - // When an arg in the args is a *js.Function, the arg will be cached on the page's js context. - // When the arg.Name exists in the page's cache, it reuse the cache without sending - // the definition to the browser again. - // Useful when you need to eval a huge js expression many times. - JSArgs []interface{} - - // Whether execution should be treated as initiated by user in the UI. - UserGesture bool -} - -// Eval creates a [EvalOptions] with ByValue set to true. -func Eval(js string, args ...interface{}) *EvalOptions { - return &EvalOptions{ - ByValue: true, - AwaitPromise: false, - ThisObj: nil, - JS: js, - JSArgs: args, - UserGesture: false, - } -} - -func evalHelper(fn *js.Function, args ...interface{}) *EvalOptions { - return &EvalOptions{ - ByValue: true, - JSArgs: append([]interface{}{fn}, args...), - JS: fmt.Sprintf(`function (f /* %s */, ...args) { return f.apply(this, args) }`, fn.Name), - } -} - -// String interface. -func (e *EvalOptions) String() string { - fn := e.JS - args := e.JSArgs - - paramsStr := "" - thisStr := "" - - if e.ThisObj != nil { - thisStr = e.ThisObj.Description - } - if len(args) > 0 { - if f, ok := args[0].(*js.Function); ok { - fn = "rod." + f.Name - args = e.JSArgs[1:] - } - - paramsStr = strings.Trim(mustToJSONForDev(args), "[]\r\n") - } - - return fmt.Sprintf("%s(%s) %s", fn, paramsStr, thisStr) -} - -// This set the obj as ThisObj. -func (e *EvalOptions) This(obj *proto.RuntimeRemoteObject) *EvalOptions { - e.ThisObj = obj - return e -} - -// ByObject disables ByValue. -func (e *EvalOptions) ByObject() *EvalOptions { - e.ByValue = false - return e -} - -// ByUser enables UserGesture. -func (e *EvalOptions) ByUser() *EvalOptions { - e.UserGesture = true - return e -} - -// ByPromise enables AwaitPromise. -func (e *EvalOptions) ByPromise() *EvalOptions { - e.AwaitPromise = true - return e -} - -func (e *EvalOptions) formatToJSFunc() string { - js := strings.Trim(e.JS, "\t\n\v\f\r ;") - return fmt.Sprintf(`function() { return (%s).apply(this, arguments) }`, js) -} - -// Eval is a shortcut for [Page.Evaluate] with AwaitPromise, ByValue set to true. -func (p *Page) Eval(js string, args ...interface{}) (*proto.RuntimeRemoteObject, error) { - return p.Evaluate(Eval(js, args...).ByPromise()) -} - -// Evaluate js on the page. -func (p *Page) Evaluate(opts *EvalOptions) (res *proto.RuntimeRemoteObject, err error) { - var backoff utils.Sleeper - - // js context will be invalid if a frame is reloaded or not ready, then the isNilContextErr - // will be true, then we retry the eval again. - for { - res, err = p.evaluate(opts) - if err != nil && errors.Is(err, cdp.ErrCtxNotFound) { - if opts.ThisObj != nil { - return nil, &ObjectNotFoundError{opts.ThisObj} - } - - if backoff == nil { - backoff = utils.BackoffSleeper(30*time.Millisecond, 3*time.Second, nil) - } else { - _ = backoff(p.ctx) - } - - p.unsetJSCtxID() - - continue - } - return - } -} - -func (p *Page) evaluate(opts *EvalOptions) (*proto.RuntimeRemoteObject, error) { - args, err := p.formatArgs(opts) - if err != nil { - return nil, err - } - - req := proto.RuntimeCallFunctionOn{ - AwaitPromise: opts.AwaitPromise, - ReturnByValue: opts.ByValue, - UserGesture: opts.UserGesture, - FunctionDeclaration: opts.formatToJSFunc(), - Arguments: args, - } - - if opts.ThisObj == nil { - req.ObjectID, err = p.getJSCtxID() - if err != nil { - return nil, err - } - } else { - req.ObjectID = opts.ThisObj.ObjectID - } - - res, err := req.Call(p) - if err != nil { - return nil, err - } - - if res.ExceptionDetails != nil { - return nil, &EvalError{res.ExceptionDetails} - } - - return res.Result, nil -} - -// Expose fn to the page's window object with the name. The exposure survives reloads. -// Call stop to unbind the fn. -func (p *Page) Expose(name string, fn func(gson.JSON) (interface{}, error)) (stop func() error, err error) { - bind := "_" + utils.RandString(8) - - err = proto.RuntimeAddBinding{Name: bind}.Call(p) - if err != nil { - return - } - - _, err = p.Evaluate(Eval(js.ExposeFunc.Definition, name, bind)) - if err != nil { - return - } - - code := fmt.Sprintf(`(%s)("%s", "%s")`, js.ExposeFunc.Definition, name, bind) - remove, err := p.EvalOnNewDocument(code) - if err != nil { - return - } - - p, cancel := p.WithCancel() - - stop = func() error { - defer cancel() - err := remove() - if err != nil { - return err - } - return proto.RuntimeRemoveBinding{Name: bind}.Call(p) - } - - go p.EachEvent(func(e *proto.RuntimeBindingCalled) { - if e.Name == bind { - payload := gson.NewFrom(e.Payload) - res, err := fn(payload.Get("req")) - code := fmt.Sprintf("(res, err) => %s(res, err)", payload.Get("cb").Str()) - _, _ = p.Evaluate(Eval(code, res, err)) - } - })() - - return -} - -func (p *Page) formatArgs(opts *EvalOptions) ([]*proto.RuntimeCallArgument, error) { - formatted := []*proto.RuntimeCallArgument{} - for _, arg := range opts.JSArgs { - if obj, ok := arg.(*proto.RuntimeRemoteObject); ok { // remote object - formatted = append(formatted, &proto.RuntimeCallArgument{ObjectID: obj.ObjectID}) - } else if obj, ok := arg.(*js.Function); ok { // js helper - id, err := p.ensureJSHelper(obj) - if err != nil { - return nil, err - } - formatted = append(formatted, &proto.RuntimeCallArgument{ObjectID: id}) - } else { // plain json data - formatted = append(formatted, &proto.RuntimeCallArgument{Value: gson.New(arg)}) - } - } - - return formatted, nil -} - -// Check the doc of EvalHelper. -func (p *Page) ensureJSHelper(fn *js.Function) (proto.RuntimeRemoteObjectID, error) { - jsCtxID, err := p.getJSCtxID() - if err != nil { - return "", err - } - - fnID, has := p.getHelper(jsCtxID, js.Functions.Name) - if !has { - res, err := proto.RuntimeCallFunctionOn{ - ObjectID: jsCtxID, - FunctionDeclaration: js.Functions.Definition, - }.Call(p) - if err != nil { - return "", err - } - fnID = res.Result.ObjectID - p.setHelper(jsCtxID, js.Functions.Name, fnID) - } - - id, has := p.getHelper(jsCtxID, fn.Name) - if !has { - for _, dep := range fn.Dependencies { - _, err := p.ensureJSHelper(dep) - if err != nil { - return "", err - } - } - - res, err := proto.RuntimeCallFunctionOn{ - ObjectID: jsCtxID, - Arguments: []*proto.RuntimeCallArgument{{ObjectID: fnID}}, - - FunctionDeclaration: fmt.Sprintf( - // we only need the object id, but the cdp will return the whole function string. - // So we override the toString to reduce the overhead. - "functions => { const f = functions.%s = %s; f.toString = () => 'fn'; return f }", - fn.Name, fn.Definition, - ), - }.Call(p) - if err != nil { - return "", err - } - - id = res.Result.ObjectID - p.setHelper(jsCtxID, fn.Name, id) - } - - return id, nil -} - -func (p *Page) getHelper(jsCtxID proto.RuntimeRemoteObjectID, name string) (proto.RuntimeRemoteObjectID, bool) { - p.helpersLock.Lock() - defer p.helpersLock.Unlock() - - if p.helpers == nil { - p.helpers = map[proto.RuntimeRemoteObjectID]map[string]proto.RuntimeRemoteObjectID{} - } - - list, ok := p.helpers[jsCtxID] - if !ok { - list = map[string]proto.RuntimeRemoteObjectID{} - p.helpers[jsCtxID] = list - } - - id, ok := list[name] - return id, ok -} - -func (p *Page) setHelper(jsCtxID proto.RuntimeRemoteObjectID, name string, fnID proto.RuntimeRemoteObjectID) { - p.helpersLock.Lock() - defer p.helpersLock.Unlock() - - p.helpers[jsCtxID][name] = fnID -} - -// Returns the page's window object, the page can be an iframe. -func (p *Page) getJSCtxID() (proto.RuntimeRemoteObjectID, error) { - p.jsCtxLock.Lock() - defer p.jsCtxLock.Unlock() - - if *p.jsCtxID != "" { - return *p.jsCtxID, nil - } - - if !p.IsIframe() { - obj, err := proto.RuntimeEvaluate{Expression: "window"}.Call(p) - if err != nil { - return "", err - } - - *p.jsCtxID = obj.Result.ObjectID - p.helpersLock.Lock() - p.helpers = nil - p.helpersLock.Unlock() - return *p.jsCtxID, nil - } - - node, err := p.element.Describe(1, true) - if err != nil { - return "", err - } - - obj, err := proto.DOMResolveNode{BackendNodeID: node.ContentDocument.BackendNodeID}.Call(p) - if err != nil { - return "", err - } - - p.helpersLock.Lock() - delete(p.helpers, *p.jsCtxID) - p.helpersLock.Unlock() - id, err := p.jsCtxIDByObjectID(obj.Object.ObjectID) - *p.jsCtxID = id - return *p.jsCtxID, err -} - -func (p *Page) unsetJSCtxID() { - p.jsCtxLock.Lock() - defer p.jsCtxLock.Unlock() - - *p.jsCtxID = "" -} - -func (p *Page) jsCtxIDByObjectID(id proto.RuntimeRemoteObjectID) (proto.RuntimeRemoteObjectID, error) { - res, err := proto.RuntimeCallFunctionOn{ - ObjectID: id, - FunctionDeclaration: `() => window`, - }.Call(p) - if err != nil { - return "", err - } - - return res.Result.ObjectID, nil -} diff --git a/internal/rod/query.go b/internal/rod/query.go deleted file mode 100644 index 0020288..0000000 --- a/internal/rod/query.go +++ /dev/null @@ -1,543 +0,0 @@ -// This file contains all query related code for Page and Element to separate the concerns. - -package rod - -import ( - "errors" - "regexp" - - "github.com/go-rod/rod/lib/cdp" - "github.com/go-rod/rod/lib/js" - "github.com/go-rod/rod/lib/proto" - "github.com/go-rod/rod/lib/utils" -) - -// SelectorType enum. -type SelectorType string - -const ( - // SelectorTypeRegex type. - SelectorTypeRegex SelectorType = "regex" - // SelectorTypeCSSSector type. - SelectorTypeCSSSector SelectorType = "css-selector" - // SelectorTypeText type. - SelectorTypeText SelectorType = "text" -) - -// Elements provides some helpers to deal with element list. -type Elements []*Element - -// First returns the first element, if the list is empty returns nil. -func (els Elements) First() *Element { - if els.Empty() { - return nil - } - return els[0] -} - -// Last returns the last element, if the list is empty returns nil. -func (els Elements) Last() *Element { - if els.Empty() { - return nil - } - return els[len(els)-1] -} - -// Empty returns true if the list is empty. -func (els Elements) Empty() bool { - return len(els) == 0 -} - -// Pages provides some helpers to deal with page list. -type Pages []*Page - -// First returns the first page, if the list is empty returns nil. -func (ps Pages) First() *Page { - if ps.Empty() { - return nil - } - return ps[0] -} - -// Last returns the last page, if the list is empty returns nil. -func (ps Pages) Last() *Page { - if ps.Empty() { - return nil - } - return ps[len(ps)-1] -} - -// Empty returns true if the list is empty. -func (ps Pages) Empty() bool { - return len(ps) == 0 -} - -// Find the page that has the specified element with the css selector. -func (ps Pages) Find(selector string) (*Page, error) { - for _, page := range ps { - has, _, err := page.Has(selector) - if err != nil { - return nil, err - } - if has { - return page, nil - } - } - return nil, &PageNotFoundError{} -} - -// FindByURL returns the page that has the url that matches the jsRegex. -func (ps Pages) FindByURL(jsRegex string) (*Page, error) { - for _, page := range ps { - res, err := page.Eval(`() => location.href`) - if err != nil { - return nil, err - } - url := res.Value.String() - if regexp.MustCompile(jsRegex).MatchString(url) { - return page, nil - } - } - return nil, &PageNotFoundError{} -} - -// Has an element that matches the css selector. -func (p *Page) Has(selector string) (bool, *Element, error) { - el, err := p.Sleeper(NotFoundSleeper).Element(selector) - if errors.Is(err, &ElementNotFoundError{}) { - return false, nil, nil - } - if err != nil { - return false, nil, err - } - return true, el.Sleeper(p.sleeper), nil -} - -// HasX an element that matches the XPath selector. -func (p *Page) HasX(selector string) (bool, *Element, error) { - el, err := p.Sleeper(NotFoundSleeper).ElementX(selector) - if errors.Is(err, &ElementNotFoundError{}) { - return false, nil, nil - } - if err != nil { - return false, nil, err - } - return true, el.Sleeper(p.sleeper), nil -} - -// HasR an element that matches the css selector and its display text matches the jsRegex. -func (p *Page) HasR(selector, jsRegex string) (bool, *Element, error) { - el, err := p.Sleeper(NotFoundSleeper).ElementR(selector, jsRegex) - if errors.Is(err, &ElementNotFoundError{}) { - return false, nil, nil - } - if err != nil { - return false, nil, err - } - return true, el.Sleeper(p.sleeper), nil -} - -// Element retries until an element in the page that matches the CSS selector, then returns -// the matched element. -func (p *Page) Element(selector string) (*Element, error) { - return p.ElementByJS(evalHelper(js.Element, selector)) -} - -// ElementR retries until an element in the page that matches the css selector and it's text matches the jsRegex, -// then returns the matched element. -func (p *Page) ElementR(selector, jsRegex string) (*Element, error) { - return p.ElementByJS(evalHelper(js.ElementR, selector, jsRegex)) -} - -// ElementX retries until an element in the page that matches one of the XPath selectors, then returns -// the matched element. -func (p *Page) ElementX(xPath string) (*Element, error) { - return p.ElementByJS(evalHelper(js.ElementX, xPath)) -} - -// ElementByJS returns the element from the return value of the js function. -// If sleeper is nil, no retry will be performed. -// By default, it will retry until the js function doesn't return null. -// To customize the retry logic, check the examples of Page.Sleeper. -func (p *Page) ElementByJS(opts *EvalOptions) (*Element, error) { - var res *proto.RuntimeRemoteObject - var err error - - removeTrace := func() {} - err = utils.Retry(p.ctx, p.sleeper(), func() (bool, error) { - remove := p.tryTraceQuery(opts) - removeTrace() - removeTrace = remove - - res, err = p.Evaluate(opts.ByObject()) - if err != nil { - return true, err - } - - if res.Type == proto.RuntimeRemoteObjectTypeObject && res.Subtype == proto.RuntimeRemoteObjectSubtypeNull { - return false, nil - } - - return true, nil - }) - removeTrace() - if err != nil { - return nil, err - } - - if res.Subtype != proto.RuntimeRemoteObjectSubtypeNode { - return nil, &ExpectElementError{res} - } - - return p.ElementFromObject(res) -} - -// Elements returns all elements that match the css selector. -func (p *Page) Elements(selector string) (Elements, error) { - return p.ElementsByJS(evalHelper(js.Elements, selector)) -} - -// ElementsX returns all elements that match the XPath selector. -func (p *Page) ElementsX(xpath string) (Elements, error) { - return p.ElementsByJS(evalHelper(js.ElementsX, xpath)) -} - -// ElementsByJS returns the elements from the return value of the js. -func (p *Page) ElementsByJS(opts *EvalOptions) (Elements, error) { - res, err := p.Evaluate(opts.ByObject()) - if err != nil { - return nil, err - } - - if res.Subtype != proto.RuntimeRemoteObjectSubtypeArray { - return nil, &ExpectElementsError{res} - } - - defer func() { err = p.Release(res) }() - - list, err := proto.RuntimeGetProperties{ - ObjectID: res.ObjectID, - OwnProperties: true, - }.Call(p) - if err != nil { - return nil, err - } - - elemList := Elements{} - for _, obj := range list.Result { - if obj.Name == "__proto__" || obj.Name == "length" { - continue - } - val := obj.Value - - if val.Subtype != proto.RuntimeRemoteObjectSubtypeNode { - return nil, &ExpectElementsError{val} - } - - el, err := p.ElementFromObject(val) - if err != nil { - return nil, err - } - - elemList = append(elemList, el) - } - - return elemList, err -} - -// Search for the given query in the DOM tree until the result count is not zero, before that it will keep retrying. -// The query can be plain text or css selector or xpath. -// It will search nested iframes and shadow doms too. -func (p *Page) Search(query string) (*SearchResult, error) { - sr := &SearchResult{ - page: p, - restore: p.EnableDomain(proto.DOMEnable{}), - } - - err := utils.Retry(p.ctx, p.sleeper(), func() (bool, error) { - if sr.DOMPerformSearchResult != nil { - _ = proto.DOMDiscardSearchResults{SearchID: sr.SearchID}.Call(p) - } - - res, err := proto.DOMPerformSearch{ - Query: query, - IncludeUserAgentShadowDOM: true, - }.Call(p) - if err != nil { - return true, err - } - - sr.DOMPerformSearchResult = res - - if res.ResultCount == 0 { - return false, nil - } - - result, err := proto.DOMGetSearchResults{ - SearchID: res.SearchID, - FromIndex: 0, - ToIndex: 1, - }.Call(p) - if err != nil { - // when the page is still loading the search result is not ready - if errors.Is(err, cdp.ErrCtxNotFound) || - errors.Is(err, cdp.ErrSearchSessionNotFound) { - return false, nil - } - return true, err - } - - id := result.NodeIDs[0] - - // TODO: This is definitely a bad design of cdp, hope they can optimize it in the future. - // It's unnecessary to ask the user to explicitly call it. - // - // When the id is zero, it means the proto.DOMDocumentUpdated has fired which will - // invalidate all the existing NodeID. We have to call proto.DOMGetDocument - // to reset the remote browser's tracker. - if id == 0 { - _, _ = proto.DOMGetDocument{}.Call(p) - return false, nil - } - - el, err := p.ElementFromNode(&proto.DOMNode{NodeID: id}) - if err != nil { - return true, err - } - - sr.First = el - - return true, nil - }) - if err != nil { - return nil, err - } - - return sr, nil -} - -// SearchResult handler. -type SearchResult struct { - *proto.DOMPerformSearchResult - - page *Page - restore func() - - // First element in the search result - First *Element -} - -// Get l elements at the index of i from the remote search result. -func (s *SearchResult) Get(i, l int) (Elements, error) { - result, err := proto.DOMGetSearchResults{ - SearchID: s.SearchID, - FromIndex: i, - ToIndex: i + l, - }.Call(s.page) - if err != nil { - return nil, err - } - - list := Elements{} - - for _, id := range result.NodeIDs { - el, err := s.page.ElementFromNode(&proto.DOMNode{NodeID: id}) - if err != nil { - return nil, err - } - list = append(list, el) - } - - return list, nil -} - -// All returns all elements. -func (s *SearchResult) All() (Elements, error) { - return s.Get(0, s.ResultCount) -} - -// Release the remote search result. -func (s *SearchResult) Release() { - s.restore() - _ = proto.DOMDiscardSearchResults{SearchID: s.SearchID}.Call(s.page) -} - -type raceBranch struct { - condition func(*Page) (*Element, error) - callback func(*Element) error -} - -// RaceContext stores the branches to race. -type RaceContext struct { - page *Page - branches []*raceBranch -} - -// Race creates a context to race selectors. -func (p *Page) Race() *RaceContext { - return &RaceContext{page: p} -} - -// ElementFunc takes a custom function to determine race success. -func (rc *RaceContext) ElementFunc(fn func(*Page) (*Element, error)) *RaceContext { - rc.branches = append(rc.branches, &raceBranch{ - condition: fn, - }) - return rc -} - -// Element is similar to [Page.Element]. -func (rc *RaceContext) Element(selector string) *RaceContext { - return rc.ElementFunc(func(p *Page) (*Element, error) { - return p.Element(selector) - }) -} - -// ElementX is similar to [Page.ElementX]. -func (rc *RaceContext) ElementX(selector string) *RaceContext { - return rc.ElementFunc(func(p *Page) (*Element, error) { - return p.ElementX(selector) - }) -} - -// ElementR is similar to [Page.ElementR]. -func (rc *RaceContext) ElementR(selector, regex string) *RaceContext { - return rc.ElementFunc(func(p *Page) (*Element, error) { - return p.ElementR(selector, regex) - }) -} - -// ElementByJS is similar to [Page.ElementByJS]. -func (rc *RaceContext) ElementByJS(opts *EvalOptions) *RaceContext { - return rc.ElementFunc(func(p *Page) (*Element, error) { - return p.ElementByJS(opts) - }) -} - -// Search is similar to [Page.Search]. -func (rc *RaceContext) Search(query string) *RaceContext { - return rc.ElementFunc(func(p *Page) (*Element, error) { - res, err := p.Search(query) - if err != nil { - return nil, err - } - res.Release() - return res.First, nil - }) -} - -// Handle adds a callback function to the most recent chained selector. -// The callback function is run, if the corresponding selector is -// present first, in the Race condition. -func (rc *RaceContext) Handle(callback func(*Element) error) *RaceContext { - rc.branches[len(rc.branches)-1].callback = callback - return rc -} - -// Do the race. -func (rc *RaceContext) Do() (*Element, error) { - var el *Element - err := utils.Retry(rc.page.ctx, rc.page.sleeper(), func() (stop bool, err error) { - for _, branch := range rc.branches { - bEl, err := branch.condition(rc.page.Sleeper(NotFoundSleeper)) - if err == nil { - el = bEl.Sleeper(rc.page.sleeper) - - if branch.callback != nil { - err = branch.callback(el) - } - return true, err - } else if !errors.Is(err, &ElementNotFoundError{}) { - return true, err - } - } - return - }) - return el, err -} - -// Has an element that matches the css selector. -func (el *Element) Has(selector string) (bool, *Element, error) { - el, err := el.Element(selector) - if errors.Is(err, &ElementNotFoundError{}) { - return false, nil, nil - } - return err == nil, el, err -} - -// HasX an element that matches the XPath selector. -func (el *Element) HasX(selector string) (bool, *Element, error) { - el, err := el.ElementX(selector) - if errors.Is(err, &ElementNotFoundError{}) { - return false, nil, nil - } - return err == nil, el, err -} - -// HasR returns true if a child element that matches the css selector and its text matches the jsRegex. -func (el *Element) HasR(selector, jsRegex string) (bool, *Element, error) { - el, err := el.ElementR(selector, jsRegex) - if errors.Is(err, &ElementNotFoundError{}) { - return false, nil, nil - } - return err == nil, el, err -} - -// Element returns the first child that matches the css selector. -func (el *Element) Element(selector string) (*Element, error) { - return el.ElementByJS(evalHelper(js.Element, selector)) -} - -// ElementR returns the first child element that matches the css selector and its text matches the jsRegex. -func (el *Element) ElementR(selector, jsRegex string) (*Element, error) { - return el.ElementByJS(evalHelper(js.ElementR, selector, jsRegex)) -} - -// ElementX returns the first child that matches the XPath selector. -func (el *Element) ElementX(xPath string) (*Element, error) { - return el.ElementByJS(evalHelper(js.ElementX, xPath)) -} - -// ElementByJS returns the element from the return value of the js. -func (el *Element) ElementByJS(opts *EvalOptions) (*Element, error) { - e, err := el.page.Context(el.ctx).Sleeper(NotFoundSleeper).ElementByJS(opts.This(el.Object)) - if err != nil { - return nil, err - } - return e.Sleeper(el.sleeper), nil -} - -// Parent returns the parent element in the DOM tree. -func (el *Element) Parent() (*Element, error) { - return el.ElementByJS(Eval(`() => this.parentElement`)) -} - -// Parents that match the selector. -func (el *Element) Parents(selector string) (Elements, error) { - return el.ElementsByJS(evalHelper(js.Parents, selector)) -} - -// Next returns the next sibling element in the DOM tree. -func (el *Element) Next() (*Element, error) { - return el.ElementByJS(Eval(`() => this.nextElementSibling`)) -} - -// Previous returns the previous sibling element in the DOM tree. -func (el *Element) Previous() (*Element, error) { - return el.ElementByJS(Eval(`() => this.previousElementSibling`)) -} - -// Elements returns all elements that match the css selector. -func (el *Element) Elements(selector string) (Elements, error) { - return el.ElementsByJS(evalHelper(js.Elements, selector)) -} - -// ElementsX returns all elements that match the XPath selector. -func (el *Element) ElementsX(xpath string) (Elements, error) { - return el.ElementsByJS(evalHelper(js.ElementsX, xpath)) -} - -// ElementsByJS returns the elements from the return value of the js. -func (el *Element) ElementsByJS(opts *EvalOptions) (Elements, error) { - return el.page.Context(el.ctx).ElementsByJS(opts.This(el.Object)) -} diff --git a/internal/rod/states.go b/internal/rod/states.go deleted file mode 100644 index 271d366..0000000 --- a/internal/rod/states.go +++ /dev/null @@ -1,119 +0,0 @@ -package rod - -import ( - "reflect" - - "github.com/go-rod/rod/lib/proto" -) - -type stateKey struct { - browserContextID proto.BrowserBrowserContextID - sessionID proto.TargetSessionID - methodName string -} - -func (b *Browser) key(sessionID proto.TargetSessionID, methodName string) stateKey { - return stateKey{ - browserContextID: b.BrowserContextID, - sessionID: sessionID, - methodName: methodName, - } -} - -func (b *Browser) set(sessionID proto.TargetSessionID, methodName string, params interface{}) { - b.states.Store(b.key(sessionID, methodName), params) - - key := "" - switch methodName { - case (proto.EmulationClearDeviceMetricsOverride{}).ProtoReq(): - key = (proto.EmulationSetDeviceMetricsOverride{}).ProtoReq() - case (proto.EmulationClearGeolocationOverride{}).ProtoReq(): - key = (proto.EmulationSetGeolocationOverride{}).ProtoReq() - default: - domain, name := proto.ParseMethodName(methodName) - if name == "disable" { - key = domain + ".enable" - } - } - if key != "" { - b.states.Delete(b.key(sessionID, key)) - } -} - -// LoadState into the method, sessionID can be empty. -func (b *Browser) LoadState(sessionID proto.TargetSessionID, method proto.Request) (has bool) { - data, has := b.states.Load(b.key(sessionID, method.ProtoReq())) - if has { - reflect.Indirect(reflect.ValueOf(method)).Set( - reflect.Indirect(reflect.ValueOf(data)), - ) - } - return -} - -// RemoveState a state. -func (b *Browser) RemoveState(key interface{}) { - b.states.Delete(key) -} - -// EnableDomain and returns a restore function to restore previous state. -func (b *Browser) EnableDomain(sessionID proto.TargetSessionID, req proto.Request) (restore func()) { - _, enabled := b.states.Load(b.key(sessionID, req.ProtoReq())) - - if !enabled { - _, _ = b.Call(b.ctx, string(sessionID), req.ProtoReq(), req) - } - - return func() { - if !enabled { - domain, _ := proto.ParseMethodName(req.ProtoReq()) - _, _ = b.Call(b.ctx, string(sessionID), domain+".disable", nil) - } - } -} - -// DisableDomain and returns a restore function to restore previous state. -func (b *Browser) DisableDomain(sessionID proto.TargetSessionID, req proto.Request) (restore func()) { - _, enabled := b.states.Load(b.key(sessionID, req.ProtoReq())) - domain, _ := proto.ParseMethodName(req.ProtoReq()) - - if enabled { - _, _ = b.Call(b.ctx, string(sessionID), domain+".disable", nil) - } - - return func() { - if enabled { - _, _ = b.Call(b.ctx, string(sessionID), req.ProtoReq(), req) - } - } -} - -func (b *Browser) cachePage(page *Page) { - b.states.Store(page.TargetID, page) -} - -func (b *Browser) loadCachedPage(id proto.TargetTargetID) *Page { - if cache, ok := b.states.Load(id); ok { - return cache.(*Page) //nolint: forcetypeassert - } - return nil -} - -// LoadState into the method. -func (p *Page) LoadState(method proto.Request) (has bool) { - return p.browser.LoadState(p.SessionID, method) -} - -// EnableDomain and returns a restore function to restore previous state. -func (p *Page) EnableDomain(method proto.Request) (restore func()) { - return p.browser.Context(p.ctx).EnableDomain(p.SessionID, method) -} - -// DisableDomain and returns a restore function to restore previous state. -func (p *Page) DisableDomain(method proto.Request) (restore func()) { - return p.browser.Context(p.ctx).DisableDomain(p.SessionID, method) -} - -func (p *Page) cleanupStates() { - p.browser.RemoveState(p.TargetID) -} diff --git a/internal/rod/utils.go b/internal/rod/utils.go deleted file mode 100644 index e18e26a..0000000 --- a/internal/rod/utils.go +++ /dev/null @@ -1,271 +0,0 @@ -package rod - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "os" - "path/filepath" - "reflect" - "regexp" - "runtime/debug" - "sync" - "time" - - "github.com/go-rod/rod/lib/cdp" - "github.com/go-rod/rod/lib/proto" - "github.com/go-rod/rod/lib/utils" -) - -// CDPClient is usually used to make rod side-effect free. Such as proxy all IO of rod. -type CDPClient interface { - Event() <-chan *cdp.Event - Call(ctx context.Context, sessionID, method string, params interface{}) ([]byte, error) -} - -// Message represents a cdp.Event. -type Message struct { - SessionID proto.TargetSessionID - Method string - - lock *sync.Mutex - data json.RawMessage - event reflect.Value -} - -// Load data into e, returns true if e matches the event type. -func (msg *Message) Load(e proto.Event) bool { - if msg.Method != e.ProtoEvent() { - return false - } - - eVal := reflect.ValueOf(e) - if eVal.Kind() != reflect.Pointer { - return true - } - eVal = reflect.Indirect(eVal) - - msg.lock.Lock() - defer msg.lock.Unlock() - if msg.data == nil { - eVal.Set(msg.event) - return true - } - - utils.E(json.Unmarshal(msg.data, e)) - msg.event = eVal - msg.data = nil - return true -} - -// DefaultLogger for rod. -var DefaultLogger = log.New(os.Stdout, "[rod] ", log.LstdFlags) - -// DefaultSleeper generates the default sleeper for retry, it uses backoff to grow the interval. -// The growth looks like: -// -// A(0) = 100ms, A(n) = A(n-1) * random[1.9, 2.1), A(n) < 1s -// -// Why the default is not RequestAnimationFrame or DOM change events is because of if a retry never -// ends it can easily flood the program. But you can always easily config it into what you want. -var DefaultSleeper = func() utils.Sleeper { - return utils.BackoffSleeper(100*time.Millisecond, time.Second, nil) -} - -// NewPagePool instance. -func NewPagePool(limit int) Pool[Page] { - return NewPool[Page](limit) -} - -// NewBrowserPool instance. -func NewBrowserPool(limit int) Pool[Browser] { - return NewPool[Browser](limit) -} - -// Pool is used to thread-safely limit the number of elements at the same time. -// It's a common practice to use a channel to limit concurrency, it's not special for rod. -// This helper is more like an example to use Go Channel. -// Reference: https://golang.org/doc/effective_go#channels -type Pool[T any] chan *T - -// NewPool instance. -func NewPool[T any](limit int) Pool[T] { - p := make(chan *T, limit) - for i := 0; i < limit; i++ { - p <- nil - } - return p -} - -// Get a elem from the pool, allow error. Use the [Pool[T].Put] to make it reusable later. -func (p Pool[T]) Get(create func() (*T, error)) (elem *T, err error) { - elem = <-p - if elem == nil { - elem, err = create() - } - return -} - -// Put an elem back to the pool. -func (p Pool[T]) Put(elem *T) { - p <- elem -} - -// Cleanup helper. -func (p Pool[T]) Cleanup(iteratee func(*T)) { - for i := 0; i < cap(p); i++ { - select { - case elem := <-p: - if elem != nil { - iteratee(elem) - } - default: - } - } -} - -var _ io.ReadCloser = &StreamReader{} - -// StreamReader for browser data stream. -type StreamReader struct { - Offset *int - - c proto.Client - handle proto.IOStreamHandle - buf *bytes.Buffer -} - -// NewStreamReader instance. -func NewStreamReader(c proto.Client, h proto.IOStreamHandle) *StreamReader { - return &StreamReader{ - c: c, - handle: h, - buf: &bytes.Buffer{}, - } -} - -func (sr *StreamReader) Read(p []byte) (n int, err error) { - res, err := proto.IORead{ - Handle: sr.handle, - Offset: sr.Offset, - }.Call(sr.c) - if err != nil { - return 0, err - } - - if !res.EOF { - var bin []byte - if res.Base64Encoded { - bin, err = base64.StdEncoding.DecodeString(res.Data) - if err != nil { - return 0, err - } - } else { - bin = []byte(res.Data) - } - - _, _ = sr.buf.Write(bin) - } - - return sr.buf.Read(p) -} - -// Close the stream, discard any temporary backing storage. -func (sr *StreamReader) Close() error { - return proto.IOClose{Handle: sr.handle}.Call(sr.c) -} - -// Try try fn with recover, return the panic as rod.ErrTry. -func Try(fn func()) (err error) { - defer func() { - if val := recover(); val != nil { - err = &TryError{val, string(debug.Stack())} - } - }() - - fn() - - return err -} - -func genRegMatcher(includes, excludes []string) func(string) bool { - regIncludes := make([]*regexp.Regexp, len(includes)) - for i, p := range includes { - regIncludes[i] = regexp.MustCompile(p) - } - - regExcludes := make([]*regexp.Regexp, len(excludes)) - for i, p := range excludes { - regExcludes[i] = regexp.MustCompile(p) - } - - return func(s string) bool { - for _, include := range regIncludes { - if include.MatchString(s) { - for _, exclude := range regExcludes { - if exclude.MatchString(s) { - goto end - } - } - return true - } - } - end: - return false - } -} - -type saveFileType int - -const ( - saveFileTypeScreenshot saveFileType = iota - saveFileTypePDF -) - -func saveFile(fileType saveFileType, bin []byte, toFile []string) error { - if len(toFile) == 0 { - return nil - } - if toFile[0] == "" { - stamp := fmt.Sprintf("%d", time.Now().UnixNano()) - switch fileType { - case saveFileTypeScreenshot: - toFile = []string{"tmp", "screenshots", stamp + ".png"} - case saveFileTypePDF: - toFile = []string{"tmp", "pdf", stamp + ".pdf"} - } - } - return utils.OutputFile(filepath.Join(toFile...), bin) -} - -func httHTML(w http.ResponseWriter, body string) { - w.Header().Add("Content-Type", "text/html; charset=utf-8") - _, _ = w.Write([]byte(body)) -} - -func mustToJSONForDev(value interface{}) string { - buf := new(bytes.Buffer) - enc := json.NewEncoder(buf) - enc.SetEscapeHTML(false) - - utils.E(enc.Encode(value)) - - return buf.String() -} - -// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs -var regDataURI = regexp.MustCompile(`\Adata:(.+?)?(;base64)?,`) - -func parseDataURI(uri string) (string, []byte) { - matches := regDataURI.FindStringSubmatch(uri) - l := len(matches[0]) - contentType := matches[1] - - bin, _ := base64.StdEncoding.DecodeString(uri[l:]) - return contentType, bin -} diff --git a/internal/stealth/LICENSE b/internal/stealth/LICENSE deleted file mode 100644 index 5b3a8f0..0000000 --- a/internal/stealth/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Rod - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/internal/stealth/assets.go b/internal/stealth/assets.go deleted file mode 100644 index b9a0f0e..0000000 --- a/internal/stealth/assets.go +++ /dev/null @@ -1,12 +0,0 @@ -// generated by running "go generate" on project root - -package stealth - -// JSVersion for stealth -const JSVersion = "v2.7.3" - -// JS for stealth -const JS = `;(() => { - -(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(` + "`" + `at ` + "`" + `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n ` + "`" + `at Reflect.${trap} ` + "`" + `, // e.g. Reflect.get or Reflect.apply\n ` + "`" + `at Object.${trap} ` + "`" + `, // e.g. Object.get or Object.apply\n ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in ` + "`" + `makeNativeString` + "`" + `\n nativeToStringStr: Function.toString + '' // => ` + "`" + `function toString() { [native code] }` + "`" + `\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> ` + "`" + `HTMLMediaElement.prototype` + "`" + `\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> ` + "`" + `canPlayType` + "`" + `\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(` + "`" + `() => ${value}` + "`" + `)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple ` + "`" + `navigator` + "`" + ` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like ` + "`" + `navigator.__proto__.vendor` + "`" + ` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:'utils => {\n if (!window.chrome) {\n // Use the exact property descriptor found in headful Chrome\n // fetch it via ` + "`" + `Object.getOwnPropertyDescriptor(window, \'chrome\')` + "`" + `\n Object.defineProperty(window, \'chrome\', {\n writable: true,\n enumerable: true,\n configurable: false, // note!\n value: {} // We\'ll extend that later\n })\n }\n\n // That means we\'re running headful and don\'t need to mock anything\n if (\'app\' in window.chrome) {\n return // Nothing to do here\n }\n\n const makeError = {\n ErrorInInvocation: fn => {\n const err = new TypeError(` + "`" + `Error in invocation of app.${fn}()` + "`" + `)\n return utils.stripErrorWithAnchor(\n err,\n ` + "`" + `at ${fn} (eval at ` + "`" + `\n )\n }\n }\n\n // There\'s a some static data in that property which doesn\'t seem to change,\n // we should periodically check for updates: ` + "`" + `JSON.stringify(window.app, null, 2)` + "`" + `\n const STATIC_DATA = JSON.parse(\n ` + "`" + `\n{\n "isInstalled": false,\n "InstallState": {\n "DISABLED": "disabled",\n "INSTALLED": "installed",\n "NOT_INSTALLED": "not_installed"\n },\n "RunningState": {\n "CANNOT_RUN": "cannot_run",\n "READY_TO_RUN": "ready_to_run",\n "RUNNING": "running"\n }\n}\n ` + "`" + `.trim()\n )\n\n window.chrome.app = {\n ...STATIC_DATA,\n\n get isInstalled() {\n return false\n },\n\n getDetails: function getDetails() {\n if (arguments.length) {\n throw makeError.ErrorInInvocation(` + "`" + `getDetails` + "`" + `)\n }\n return null\n },\n getIsInstalled: function getDetails() {\n if (arguments.length) {\n throw makeError.ErrorInInvocation(` + "`" + `getIsInstalled` + "`" + `)\n }\n return false\n },\n runningState: function getDetails() {\n if (arguments.length) {\n throw makeError.ErrorInInvocation(` + "`" + `runningState` + "`" + `)\n }\n return \'cannot_run\'\n }\n }\n utils.patchToStringNested(window.chrome.app)\n }',_args:[]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(` + "`" + `at ` + "`" + `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n ` + "`" + `at Reflect.${trap} ` + "`" + `, // e.g. Reflect.get or Reflect.apply\n ` + "`" + `at Object.${trap} ` + "`" + `, // e.g. Object.get or Object.apply\n ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in ` + "`" + `makeNativeString` + "`" + `\n nativeToStringStr: Function.toString + '' // => ` + "`" + `function toString() { [native code] }` + "`" + `\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> ` + "`" + `HTMLMediaElement.prototype` + "`" + `\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> ` + "`" + `canPlayType` + "`" + `\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(` + "`" + `() => ${value}` + "`" + `)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple ` + "`" + `navigator` + "`" + ` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like ` + "`" + `navigator.__proto__.vendor` + "`" + ` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"utils => {\n if (!window.chrome) {\n // Use the exact property descriptor found in headful Chrome\n // fetch it via ` + "`" + `Object.getOwnPropertyDescriptor(window, 'chrome')` + "`" + `\n Object.defineProperty(window, 'chrome', {\n writable: true,\n enumerable: true,\n configurable: false, // note!\n value: {} // We'll extend that later\n })\n }\n\n // That means we're running headful and don't need to mock anything\n if ('csi' in window.chrome) {\n return // Nothing to do here\n }\n\n // Check that the Navigation Timing API v1 is available, we need that\n if (!window.performance || !window.performance.timing) {\n return\n }\n\n const { timing } = window.performance\n\n window.chrome.csi = function() {\n return {\n onloadT: timing.domContentLoadedEventEnd,\n startE: timing.navigationStart,\n pageT: Date.now() - timing.navigationStart,\n tran: 15 // Transition type or something\n }\n }\n utils.patchToString(window.chrome.csi)\n }",_args:[]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(` + "`" + `at ` + "`" + `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n ` + "`" + `at Reflect.${trap} ` + "`" + `, // e.g. Reflect.get or Reflect.apply\n ` + "`" + `at Object.${trap} ` + "`" + `, // e.g. Object.get or Object.apply\n ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in ` + "`" + `makeNativeString` + "`" + `\n nativeToStringStr: Function.toString + '' // => ` + "`" + `function toString() { [native code] }` + "`" + `\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> ` + "`" + `HTMLMediaElement.prototype` + "`" + `\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> ` + "`" + `canPlayType` + "`" + `\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(` + "`" + `() => ${value}` + "`" + `)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple ` + "`" + `navigator` + "`" + ` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like ` + "`" + `navigator.__proto__.vendor` + "`" + ` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, { opts }) => {\n if (!window.chrome) {\n // Use the exact property descriptor found in headful Chrome\n // fetch it via ` + "`" + `Object.getOwnPropertyDescriptor(window, 'chrome')` + "`" + `\n Object.defineProperty(window, 'chrome', {\n writable: true,\n enumerable: true,\n configurable: false, // note!\n value: {} // We'll extend that later\n })\n }\n\n // That means we're running headful and don't need to mock anything\n if ('loadTimes' in window.chrome) {\n return // Nothing to do here\n }\n\n // Check that the Navigation Timing API v1 + v2 is available, we need that\n if (\n !window.performance ||\n !window.performance.timing ||\n !window.PerformancePaintTiming\n ) {\n return\n }\n\n const { performance } = window\n\n // Some stuff is not available on about:blank as it requires a navigation to occur,\n // let's harden the code to not fail then:\n const ntEntryFallback = {\n nextHopProtocol: 'h2',\n type: 'other'\n }\n\n // The API exposes some funky info regarding the connection\n const protocolInfo = {\n get connectionInfo() {\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ntEntry.nextHopProtocol\n },\n get npnNegotiatedProtocol() {\n // NPN is deprecated in favor of ALPN, but this implementation returns the\n // HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN.\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)\n ? ntEntry.nextHopProtocol\n : 'unknown'\n },\n get navigationType() {\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ntEntry.type\n },\n get wasAlternateProtocolAvailable() {\n // The Alternate-Protocol header is deprecated in favor of Alt-Svc\n // (https://www.mnot.net/blog/2016/03/09/alt-svc), so technically this\n // should always return false.\n return false\n },\n get wasFetchedViaSpdy() {\n // SPDY is deprecated in favor of HTTP/2, but this implementation returns\n // true for HTTP/2 or HTTP2+QUIC/39 as well.\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)\n },\n get wasNpnNegotiated() {\n // NPN is deprecated in favor of ALPN, but this implementation returns true\n // for HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN.\n const ntEntry =\n performance.getEntriesByType('navigation')[0] || ntEntryFallback\n return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)\n }\n }\n\n const { timing } = window.performance\n\n // Truncate number to specific number of decimals, most of the ` + "`" + `loadTimes` + "`" + ` stuff has 3\n function toFixed(num, fixed) {\n var re = new RegExp('^-?\\\\d+(?:.\\\\d{0,' + (fixed || -1) + '})?')\n return num.toString().match(re)[0]\n }\n\n const timingInfo = {\n get firstPaintAfterLoadTime() {\n // This was never actually implemented and always returns 0.\n return 0\n },\n get requestTime() {\n return timing.navigationStart / 1000\n },\n get startLoadTime() {\n return timing.navigationStart / 1000\n },\n get commitLoadTime() {\n return timing.responseStart / 1000\n },\n get finishDocumentLoadTime() {\n return timing.domContentLoadedEventEnd / 1000\n },\n get finishLoadTime() {\n return timing.loadEventEnd / 1000\n },\n get firstPaintTime() {\n const fpEntry = performance.getEntriesByType('paint')[0] || {\n startTime: timing.loadEventEnd / 1000 // Fallback if no navigation occurred (` + "`" + `about:blank` + "`" + `)\n }\n return toFixed(\n (fpEntry.startTime + performance.timeOrigin) / 1000,\n 3\n )\n }\n }\n\n window.chrome.loadTimes = function() {\n return {\n ...protocolInfo,\n ...timingInfo\n }\n }\n utils.patchToString(window.chrome.loadTimes)\n }",_args:[{opts:{}}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(` + "`" + `at ` + "`" + `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n ` + "`" + `at Reflect.${trap} ` + "`" + `, // e.g. Reflect.get or Reflect.apply\n ` + "`" + `at Object.${trap} ` + "`" + `, // e.g. Object.get or Object.apply\n ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in ` + "`" + `makeNativeString` + "`" + `\n nativeToStringStr: Function.toString + '' // => ` + "`" + `function toString() { [native code] }` + "`" + `\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> ` + "`" + `HTMLMediaElement.prototype` + "`" + `\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> ` + "`" + `canPlayType` + "`" + `\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(` + "`" + `() => ${value}` + "`" + `)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple ` + "`" + `navigator` + "`" + ` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like ` + "`" + `navigator.__proto__.vendor` + "`" + ` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, { opts, STATIC_DATA }) => {\n if (!window.chrome) {\n // Use the exact property descriptor found in headful Chrome\n // fetch it via ` + "`" + `Object.getOwnPropertyDescriptor(window, 'chrome')` + "`" + `\n Object.defineProperty(window, 'chrome', {\n writable: true,\n enumerable: true,\n configurable: false, // note!\n value: {} // We'll extend that later\n })\n }\n\n // That means we're running headful and don't need to mock anything\n const existsAlready = 'runtime' in window.chrome\n // ` + "`" + `chrome.runtime` + "`" + ` is only exposed on secure origins\n const isNotSecure = !window.location.protocol.startsWith('https')\n if (existsAlready || (isNotSecure && !opts.runOnInsecureOrigins)) {\n return // Nothing to do here\n }\n\n window.chrome.runtime = {\n // There's a bunch of static data in that property which doesn't seem to change,\n // we should periodically check for updates: ` + "`" + `JSON.stringify(window.chrome.runtime, null, 2)` + "`" + `\n ...STATIC_DATA,\n // ` + "`" + `chrome.runtime.id` + "`" + ` is extension related and returns undefined in Chrome\n get id() {\n return undefined\n },\n // These two require more sophisticated mocks\n connect: null,\n sendMessage: null\n }\n\n const makeCustomRuntimeErrors = (preamble, method, extensionId) => ({\n NoMatchingSignature: new TypeError(\n preamble + ` + "`" + `No matching signature.` + "`" + `\n ),\n MustSpecifyExtensionID: new TypeError(\n preamble +\n ` + "`" + `${method} called from a webpage must specify an Extension ID (string) for its first argument.` + "`" + `\n ),\n InvalidExtensionID: new TypeError(\n preamble + ` + "`" + `Invalid extension id: '${extensionId}'` + "`" + `\n )\n })\n\n // Valid Extension IDs are 32 characters in length and use the letter ` + "`" + `a` + "`" + ` to ` + "`" + `p` + "`" + `:\n // https://source.chromium.org/chromium/chromium/src/+/master:components/crx_file/id_util.cc;drc=14a055ccb17e8c8d5d437fe080faba4c6f07beac;l=90\n const isValidExtensionID = str =>\n str.length === 32 && str.toLowerCase().match(/^[a-p]+$/)\n\n /** Mock ` + "`" + `chrome.runtime.sendMessage` + "`" + ` */\n const sendMessageHandler = {\n apply: function(target, ctx, args) {\n const [extensionId, options, responseCallback] = args || []\n\n // Define custom errors\n const errorPreamble = ` + "`" + `Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback): ` + "`" + `\n const Errors = makeCustomRuntimeErrors(\n errorPreamble,\n ` + "`" + `chrome.runtime.sendMessage()` + "`" + `,\n extensionId\n )\n\n // Check if the call signature looks ok\n const noArguments = args.length === 0\n const tooManyArguments = args.length > 4\n const incorrectOptions = options && typeof options !== 'object'\n const incorrectResponseCallback =\n responseCallback && typeof responseCallback !== 'function'\n if (\n noArguments ||\n tooManyArguments ||\n incorrectOptions ||\n incorrectResponseCallback\n ) {\n throw Errors.NoMatchingSignature\n }\n\n // At least 2 arguments are required before we even validate the extension ID\n if (args.length < 2) {\n throw Errors.MustSpecifyExtensionID\n }\n\n // Now let's make sure we got a string as extension ID\n if (typeof extensionId !== 'string') {\n throw Errors.NoMatchingSignature\n }\n\n if (!isValidExtensionID(extensionId)) {\n throw Errors.InvalidExtensionID\n }\n\n return undefined // Normal behavior\n }\n }\n utils.mockWithProxy(\n window.chrome.runtime,\n 'sendMessage',\n function sendMessage() {},\n sendMessageHandler\n )\n\n /**\n * Mock ` + "`" + `chrome.runtime.connect` + "`" + `\n *\n * @see https://developer.chrome.com/apps/runtime#method-connect\n */\n const connectHandler = {\n apply: function(target, ctx, args) {\n const [extensionId, connectInfo] = args || []\n\n // Define custom errors\n const errorPreamble = ` + "`" + `Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): ` + "`" + `\n const Errors = makeCustomRuntimeErrors(\n errorPreamble,\n ` + "`" + `chrome.runtime.connect()` + "`" + `,\n extensionId\n )\n\n // Behavior differs a bit from sendMessage:\n const noArguments = args.length === 0\n const emptyStringArgument = args.length === 1 && extensionId === ''\n if (noArguments || emptyStringArgument) {\n throw Errors.MustSpecifyExtensionID\n }\n\n const tooManyArguments = args.length > 2\n const incorrectConnectInfoType =\n connectInfo && typeof connectInfo !== 'object'\n\n if (tooManyArguments || incorrectConnectInfoType) {\n throw Errors.NoMatchingSignature\n }\n\n const extensionIdIsString = typeof extensionId === 'string'\n if (extensionIdIsString && extensionId === '') {\n throw Errors.MustSpecifyExtensionID\n }\n if (extensionIdIsString && !isValidExtensionID(extensionId)) {\n throw Errors.InvalidExtensionID\n }\n\n // There's another edge-case here: extensionId is optional so we might find a connectInfo object as first param, which we need to validate\n const validateConnectInfo = ci => {\n // More than a first param connectInfo as been provided\n if (args.length > 1) {\n throw Errors.NoMatchingSignature\n }\n // An empty connectInfo has been provided\n if (Object.keys(ci).length === 0) {\n throw Errors.MustSpecifyExtensionID\n }\n // Loop over all connectInfo props an check them\n Object.entries(ci).forEach(([k, v]) => {\n const isExpected = ['name', 'includeTlsChannelId'].includes(k)\n if (!isExpected) {\n throw new TypeError(\n errorPreamble + ` + "`" + `Unexpected property: '${k}'.` + "`" + `\n )\n }\n const MismatchError = (propName, expected, found) =>\n TypeError(\n errorPreamble +\n ` + "`" + `Error at property '${propName}': Invalid type: expected ${expected}, found ${found}.` + "`" + `\n )\n if (k === 'name' && typeof v !== 'string') {\n throw MismatchError(k, 'string', typeof v)\n }\n if (k === 'includeTlsChannelId' && typeof v !== 'boolean') {\n throw MismatchError(k, 'boolean', typeof v)\n }\n })\n }\n if (typeof extensionId === 'object') {\n validateConnectInfo(extensionId)\n throw Errors.MustSpecifyExtensionID\n }\n\n // Unfortunately even when the connect fails Chrome will return an object with methods we need to mock as well\n return utils.patchToStringNested(makeConnectResponse())\n }\n }\n utils.mockWithProxy(\n window.chrome.runtime,\n 'connect',\n function connect() {},\n connectHandler\n )\n\n function makeConnectResponse() {\n const onSomething = () => ({\n addListener: function addListener() {},\n dispatch: function dispatch() {},\n hasListener: function hasListener() {},\n hasListeners: function hasListeners() {\n return false\n },\n removeListener: function removeListener() {}\n })\n\n const response = {\n name: '',\n sender: undefined,\n disconnect: function disconnect() {},\n onDisconnect: onSomething(),\n onMessage: onSomething(),\n postMessage: function postMessage() {\n if (!arguments.length) {\n throw new TypeError(` + "`" + `Insufficient number of arguments.` + "`" + `)\n }\n throw new Error(` + "`" + `Attempting to use a disconnected port object` + "`" + `)\n }\n }\n return response\n }\n }",_args:[{opts:{runOnInsecureOrigins:!1},STATIC_DATA:{OnInstalledReason:{CHROME_UPDATE:"chrome_update",INSTALL:"install",SHARED_MODULE_UPDATE:"shared_module_update",UPDATE:"update"},OnRestartRequiredReason:{APP_UPDATE:"app_update",OS_UPDATE:"os_update",PERIODIC:"periodic"},PlatformArch:{ARM:"arm",ARM64:"arm64",MIPS:"mips",MIPS64:"mips64",X86_32:"x86-32",X86_64:"x86-64"},PlatformNaclArch:{ARM:"arm",MIPS:"mips",MIPS64:"mips64",X86_32:"x86-32",X86_64:"x86-64"},PlatformOs:{ANDROID:"android",CROS:"cros",LINUX:"linux",MAC:"mac",OPENBSD:"openbsd",WIN:"win"},RequestUpdateCheckStatus:{NO_UPDATE:"no_update",THROTTLED:"throttled",UPDATE_AVAILABLE:"update_available"}}}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(` + "`" + `at ` + "`" + `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n ` + "`" + `at Reflect.${trap} ` + "`" + `, // e.g. Reflect.get or Reflect.apply\n ` + "`" + `at Object.${trap} ` + "`" + `, // e.g. Object.get or Object.apply\n ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in ` + "`" + `makeNativeString` + "`" + `\n nativeToStringStr: Function.toString + '' // => ` + "`" + `function toString() { [native code] }` + "`" + `\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> ` + "`" + `HTMLMediaElement.prototype` + "`" + `\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> ` + "`" + `canPlayType` + "`" + `\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(` + "`" + `() => ${value}` + "`" + `)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple ` + "`" + `navigator` + "`" + ` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like ` + "`" + `navigator.__proto__.vendor` + "`" + ` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"utils => {\n /**\n * Input might look funky, we need to normalize it so e.g. whitespace isn't an issue for our spoofing.\n *\n * @example\n * video/webm; codecs=\"vp8, vorbis\"\n * video/mp4; codecs=\"avc1.42E01E\"\n * audio/x-m4a;\n * audio/ogg; codecs=\"vorbis\"\n * @param {String} arg\n */\n const parseInput = arg => {\n const [mime, codecStr] = arg.trim().split(';')\n let codecs = []\n if (codecStr && codecStr.includes('codecs=\"')) {\n codecs = codecStr\n .trim()\n .replace(` + "`" + `codecs=\"` + "`" + `, '')\n .replace(` + "`" + `\"` + "`" + `, '')\n .trim()\n .split(',')\n .filter(x => !!x)\n .map(x => x.trim())\n }\n return {\n mime,\n codecStr,\n codecs\n }\n }\n\n const canPlayType = {\n // Intercept certain requests\n apply: function(target, ctx, args) {\n if (!args || !args.length) {\n return target.apply(ctx, args)\n }\n const { mime, codecs } = parseInput(args[0])\n // This specific mp4 codec is missing in Chromium\n if (mime === 'video/mp4') {\n if (codecs.includes('avc1.42E01E')) {\n return 'probably'\n }\n }\n // This mimetype is only supported if no codecs are specified\n if (mime === 'audio/x-m4a' && !codecs.length) {\n return 'maybe'\n }\n\n // This mimetype is only supported if no codecs are specified\n if (mime === 'audio/aac' && !codecs.length) {\n return 'probably'\n }\n // Everything else as usual\n return target.apply(ctx, args)\n }\n }\n\n /* global HTMLMediaElement */\n utils.replaceWithProxy(\n HTMLMediaElement.prototype,\n 'canPlayType',\n canPlayType\n )\n }",_args:[]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(` + "`" + `at ` + "`" + `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n ` + "`" + `at Reflect.${trap} ` + "`" + `, // e.g. Reflect.get or Reflect.apply\n ` + "`" + `at Object.${trap} ` + "`" + `, // e.g. Object.get or Object.apply\n ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in ` + "`" + `makeNativeString` + "`" + `\n nativeToStringStr: Function.toString + '' // => ` + "`" + `function toString() { [native code] }` + "`" + `\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> ` + "`" + `HTMLMediaElement.prototype` + "`" + `\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> ` + "`" + `canPlayType` + "`" + `\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(` + "`" + `() => ${value}` + "`" + `)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple ` + "`" + `navigator` + "`" + ` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like ` + "`" + `navigator.__proto__.vendor` + "`" + ` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, { opts }) => {\n utils.replaceGetterWithProxy(\n Object.getPrototypeOf(navigator),\n 'hardwareConcurrency',\n utils.makeHandler().getterValue(opts.hardwareConcurrency)\n )\n }",_args:[{opts:{hardwareConcurrency:4}}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(` + "`" + `at ` + "`" + `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n ` + "`" + `at Reflect.${trap} ` + "`" + `, // e.g. Reflect.get or Reflect.apply\n ` + "`" + `at Object.${trap} ` + "`" + `, // e.g. Object.get or Object.apply\n ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in ` + "`" + `makeNativeString` + "`" + `\n nativeToStringStr: Function.toString + '' // => ` + "`" + `function toString() { [native code] }` + "`" + `\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> ` + "`" + `HTMLMediaElement.prototype` + "`" + `\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> ` + "`" + `canPlayType` + "`" + `\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(` + "`" + `() => ${value}` + "`" + `)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple ` + "`" + `navigator` + "`" + ` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like ` + "`" + `navigator.__proto__.vendor` + "`" + ` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, { opts }) => {\n const languages = opts.languages.length\n ? opts.languages\n : ['en-US', 'en']\n utils.replaceGetterWithProxy(\n Object.getPrototypeOf(navigator),\n 'languages',\n utils.makeHandler().getterValue(Object.freeze([...languages]))\n )\n }",_args:[{opts:{languages:[]}}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(` + "`" + `at ` + "`" + `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n ` + "`" + `at Reflect.${trap} ` + "`" + `, // e.g. Reflect.get or Reflect.apply\n ` + "`" + `at Object.${trap} ` + "`" + `, // e.g. Object.get or Object.apply\n ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in ` + "`" + `makeNativeString` + "`" + `\n nativeToStringStr: Function.toString + '' // => ` + "`" + `function toString() { [native code] }` + "`" + `\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> ` + "`" + `HTMLMediaElement.prototype` + "`" + `\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> ` + "`" + `canPlayType` + "`" + `\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(` + "`" + `() => ${value}` + "`" + `)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple ` + "`" + `navigator` + "`" + ` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like ` + "`" + `navigator.__proto__.vendor` + "`" + ` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, opts) => {\n const isSecure = document.location.protocol.startsWith('https')\n\n // In headful on secure origins the permission should be \"default\", not \"denied\"\n if (isSecure) {\n utils.replaceGetterWithProxy(Notification, 'permission', {\n apply() {\n return 'default'\n }\n })\n }\n\n // Another weird behavior:\n // On insecure origins in headful the state is \"denied\",\n // whereas in headless it's \"prompt\"\n if (!isSecure) {\n const handler = {\n apply(target, ctx, args) {\n const param = (args || [])[0]\n\n const isNotifications =\n param && param.name && param.name === 'notifications'\n if (!isNotifications) {\n return utils.cache.Reflect.apply(...arguments)\n }\n\n return Promise.resolve(\n Object.setPrototypeOf(\n {\n state: 'denied',\n onchange: null\n },\n PermissionStatus.prototype\n )\n )\n }\n }\n // Note: Don't use ` + "`" + `Object.getPrototypeOf` + "`" + ` here\n utils.replaceWithProxy(Permissions.prototype, 'query', handler)\n }\n }",_args:[{}]}),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(` + "`" + `at ` + "`" + `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n ` + "`" + `at Reflect.${trap} ` + "`" + `, // e.g. Reflect.get or Reflect.apply\n ` + "`" + `at Object.${trap} ` + "`" + `, // e.g. Object.get or Object.apply\n ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in ` + "`" + `makeNativeString` + "`" + `\n nativeToStringStr: Function.toString + '' // => ` + "`" + `function toString() { [native code] }` + "`" + `\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> ` + "`" + `HTMLMediaElement.prototype` + "`" + `\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> ` + "`" + `canPlayType` + "`" + `\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(` + "`" + `() => ${value}` + "`" + `)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple ` + "`" + `navigator` + "`" + ` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like ` + "`" + `navigator.__proto__.vendor` + "`" + ` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, { fns, data }) => {\n fns = utils.materializeFns(fns)\n\n // That means we're running headful\n const hasPlugins = 'plugins' in navigator && navigator.plugins.length\n if (hasPlugins) {\n return // nothing to do here\n }\n\n const mimeTypes = fns.generateMimeTypeArray(utils, fns)(data.mimeTypes)\n const plugins = fns.generatePluginArray(utils, fns)(data.plugins)\n\n // Plugin and MimeType cross-reference each other, let's do that now\n // Note: We're looping through ` + "`" + `data.plugins` + "`" + ` here, not the generated ` + "`" + `plugins` + "`" + `\n for (const pluginData of data.plugins) {\n pluginData.__mimeTypes.forEach((type, index) => {\n plugins[pluginData.name][index] = mimeTypes[type]\n\n Object.defineProperty(plugins[pluginData.name], type, {\n value: mimeTypes[type],\n writable: false,\n enumerable: false, // Not enumerable\n configurable: true\n })\n Object.defineProperty(mimeTypes[type], 'enabledPlugin', {\n value:\n type === 'application/x-pnacl'\n ? mimeTypes['application/x-nacl'].enabledPlugin // these reference the same plugin, so we need to re-use the Proxy in order to avoid leaks\n : new Proxy(plugins[pluginData.name], {}), // Prevent circular references\n writable: false,\n enumerable: false, // Important: ` + "`" + `JSON.stringify(navigator.plugins)` + "`" + `\n configurable: true\n })\n })\n }\n\n const patchNavigator = (name, value) =>\n utils.replaceProperty(Object.getPrototypeOf(navigator), name, {\n get() {\n return value\n }\n })\n\n patchNavigator('mimeTypes', mimeTypes)\n patchNavigator('plugins', plugins)\n\n // All done\n }",_args:[{fns:{generateMimeTypeArray:"(utils, fns) => mimeTypesData => {\n return fns.generateMagicArray(utils, fns)(\n mimeTypesData,\n MimeTypeArray.prototype,\n MimeType.prototype,\n 'type'\n )\n}",generatePluginArray:"(utils, fns) => pluginsData => {\n return fns.generateMagicArray(utils, fns)(\n pluginsData,\n PluginArray.prototype,\n Plugin.prototype,\n 'name'\n )\n}",generateMagicArray:"(utils, fns) =>\n function(\n dataArray = [],\n proto = MimeTypeArray.prototype,\n itemProto = MimeType.prototype,\n itemMainProp = 'type'\n ) {\n // Quick helper to set props with the same descriptors vanilla is using\n const defineProp = (obj, prop, value) =>\n Object.defineProperty(obj, prop, {\n value,\n writable: false,\n enumerable: false, // Important for mimeTypes & plugins: ` + "`" + `JSON.stringify(navigator.mimeTypes)` + "`" + `\n configurable: true\n })\n\n // Loop over our fake data and construct items\n const makeItem = data => {\n const item = {}\n for (const prop of Object.keys(data)) {\n if (prop.startsWith('__')) {\n continue\n }\n defineProp(item, prop, data[prop])\n }\n return patchItem(item, data)\n }\n\n const patchItem = (item, data) => {\n let descriptor = Object.getOwnPropertyDescriptors(item)\n\n // Special case: Plugins have a magic length property which is not enumerable\n // e.g. ` + "`" + `navigator.plugins[i].length` + "`" + ` should always be the length of the assigned mimeTypes\n if (itemProto === Plugin.prototype) {\n descriptor = {\n ...descriptor,\n length: {\n value: data.__mimeTypes.length,\n writable: false,\n enumerable: false,\n configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip ` + "`" + `length` + "`" + `\n }\n }\n }\n\n // We need to spoof a specific ` + "`" + `MimeType` + "`" + ` or ` + "`" + `Plugin` + "`" + ` object\n const obj = Object.create(itemProto, descriptor)\n\n // Virtually all property keys are not enumerable in vanilla\n const blacklist = [...Object.keys(data), 'length', 'enabledPlugin']\n return new Proxy(obj, {\n ownKeys(target) {\n return Reflect.ownKeys(target).filter(k => !blacklist.includes(k))\n },\n getOwnPropertyDescriptor(target, prop) {\n if (blacklist.includes(prop)) {\n return undefined\n }\n return Reflect.getOwnPropertyDescriptor(target, prop)\n }\n })\n }\n\n const magicArray = []\n\n // Loop through our fake data and use that to create convincing entities\n dataArray.forEach(data => {\n magicArray.push(makeItem(data))\n })\n\n // Add direct property access based on types (e.g. ` + "`" + `obj['application/pdf']` + "`" + `) afterwards\n magicArray.forEach(entry => {\n defineProp(magicArray, entry[itemMainProp], entry)\n })\n\n // This is the best way to fake the type to make sure this is false: ` + "`" + `Array.isArray(navigator.mimeTypes)` + "`" + `\n const magicArrayObj = Object.create(proto, {\n ...Object.getOwnPropertyDescriptors(magicArray),\n\n // There's one ugly quirk we unfortunately need to take care of:\n // The ` + "`" + `MimeTypeArray` + "`" + ` prototype has an enumerable ` + "`" + `length` + "`" + ` property,\n // but headful Chrome will still skip it when running ` + "`" + `Object.getOwnPropertyNames(navigator.mimeTypes)` + "`" + `.\n // To strip it we need to make it first ` + "`" + `configurable` + "`" + ` and can then overlay a Proxy with an ` + "`" + `ownKeys` + "`" + ` trap.\n length: {\n value: magicArray.length,\n writable: false,\n enumerable: false,\n configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip ` + "`" + `length` + "`" + `\n }\n })\n\n // Generate our functional function mocks :-)\n const functionMocks = fns.generateFunctionMocks(utils)(\n proto,\n itemMainProp,\n magicArray\n )\n\n // We need to overlay our custom object with a JS Proxy\n const magicArrayObjProxy = new Proxy(magicArrayObj, {\n get(target, key = '') {\n // Redirect function calls to our custom proxied versions mocking the vanilla behavior\n if (key === 'item') {\n return functionMocks.item\n }\n if (key === 'namedItem') {\n return functionMocks.namedItem\n }\n if (proto === PluginArray.prototype && key === 'refresh') {\n return functionMocks.refresh\n }\n // Everything else can pass through as normal\n return utils.cache.Reflect.get(...arguments)\n },\n ownKeys(target) {\n // There are a couple of quirks where the original property demonstrates \"magical\" behavior that makes no sense\n // This can be witnessed when calling ` + "`" + `Object.getOwnPropertyNames(navigator.mimeTypes)` + "`" + ` and the absence of ` + "`" + `length` + "`" + `\n // My guess is that it has to do with the recent change of not allowing data enumeration and this being implemented weirdly\n // For that reason we just completely fake the available property names based on our data to match what regular Chrome is doing\n // Specific issues when not patching this: ` + "`" + `length` + "`" + ` property is available, direct ` + "`" + `types` + "`" + ` props (e.g. ` + "`" + `obj['application/pdf']` + "`" + `) are missing\n const keys = []\n const typeProps = magicArray.map(mt => mt[itemMainProp])\n typeProps.forEach((_, i) => keys.push(` + "`" + `${i}` + "`" + `))\n typeProps.forEach(propName => keys.push(propName))\n return keys\n },\n getOwnPropertyDescriptor(target, prop) {\n if (prop === 'length') {\n return undefined\n }\n return Reflect.getOwnPropertyDescriptor(target, prop)\n }\n })\n\n return magicArrayObjProxy\n }",generateFunctionMocks:"utils => (\n proto,\n itemMainProp,\n dataArray\n) => ({\n /** Returns the MimeType object with the specified index. */\n item: utils.createProxy(proto.item, {\n apply(target, ctx, args) {\n if (!args.length) {\n throw new TypeError(\n ` + "`" + `Failed to execute 'item' on '${\n proto[Symbol.toStringTag]\n }': 1 argument required, but only 0 present.` + "`" + `\n )\n }\n // Special behavior alert:\n // - Vanilla tries to cast strings to Numbers (only integers!) and use them as property index lookup\n // - If anything else than an integer (including as string) is provided it will return the first entry\n const isInteger = args[0] && Number.isInteger(Number(args[0])) // Cast potential string to number first, then check for integer\n // Note: Vanilla never returns ` + "`" + `undefined` + "`" + `\n return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null\n }\n }),\n /** Returns the MimeType object with the specified name. */\n namedItem: utils.createProxy(proto.namedItem, {\n apply(target, ctx, args) {\n if (!args.length) {\n throw new TypeError(\n ` + "`" + `Failed to execute 'namedItem' on '${\n proto[Symbol.toStringTag]\n }': 1 argument required, but only 0 present.` + "`" + `\n )\n }\n return dataArray.find(mt => mt[itemMainProp] === args[0]) || null // Not ` + "`" + `undefined` + "`" + `!\n }\n }),\n /** Does nothing and shall return nothing */\n refresh: proto.refresh\n ? utils.createProxy(proto.refresh, {\n apply(target, ctx, args) {\n return undefined\n }\n })\n : undefined\n})"},data:{mimeTypes:[{type:"application/pdf",suffixes:"pdf",description:"",__pluginName:"Chrome PDF Viewer"},{type:"application/x-google-chrome-pdf",suffixes:"pdf",description:"Portable Document Format",__pluginName:"Chrome PDF Plugin"},{type:"application/x-nacl",suffixes:"",description:"Native Client Executable",__pluginName:"Native Client"},{type:"application/x-pnacl",suffixes:"",description:"Portable Native Client Executable",__pluginName:"Native Client"}],plugins:[{name:"Chrome PDF Plugin",filename:"internal-pdf-viewer",description:"Portable Document Format",__mimeTypes:["application/x-google-chrome-pdf"]},{name:"Chrome PDF Viewer",filename:"mhjfbmdgcfjbbpaeojofohoefgiehjai",description:"",__mimeTypes:["application/pdf"]},{name:"Native Client",filename:"internal-nacl-plugin",description:"",__mimeTypes:["application/x-nacl","application/x-pnacl"]}]}}]}),!1===navigator.webdriver||void 0===navigator.webdriver||delete Object.getPrototypeOf(navigator).webdriver,(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(` + "`" + `at ` + "`" + `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n ` + "`" + `at Reflect.${trap} ` + "`" + `, // e.g. Reflect.get or Reflect.apply\n ` + "`" + `at Object.${trap} ` + "`" + `, // e.g. Object.get or Object.apply\n ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in ` + "`" + `makeNativeString` + "`" + `\n nativeToStringStr: Function.toString + '' // => ` + "`" + `function toString() { [native code] }` + "`" + `\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> ` + "`" + `HTMLMediaElement.prototype` + "`" + `\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> ` + "`" + `canPlayType` + "`" + `\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(` + "`" + `() => ${value}` + "`" + `)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple ` + "`" + `navigator` + "`" + ` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like ` + "`" + `navigator.__proto__.vendor` + "`" + ` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, opts) => {\n const getParameterProxyHandler = {\n apply: function(target, ctx, args) {\n const param = (args || [])[0]\n const result = utils.cache.Reflect.apply(target, ctx, args)\n // UNMASKED_VENDOR_WEBGL\n if (param === 37445) {\n return opts.vendor || 'Intel Inc.' // default in headless: Google Inc.\n }\n // UNMASKED_RENDERER_WEBGL\n if (param === 37446) {\n return opts.renderer || 'Intel Iris OpenGL Engine' // default in headless: Google SwiftShader\n }\n return result\n }\n }\n\n // There's more than one WebGL rendering context\n // https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext#Browser_compatibility\n // To find out the original values here: Object.getOwnPropertyDescriptors(WebGLRenderingContext.prototype.getParameter)\n const addProxy = (obj, propName) => {\n utils.replaceWithProxy(obj, propName, getParameterProxyHandler)\n }\n // For whatever weird reason loops don't play nice with Object.defineProperty, here's the next best thing:\n addProxy(WebGLRenderingContext.prototype, 'getParameter')\n addProxy(WebGL2RenderingContext.prototype, 'getParameter')\n }",_args:[{}]}),(()=>{try{if(window.outerWidth&&window.outerHeight)return;const n=85;window.outerWidth=window.innerWidth,window.outerHeight=window.innerHeight+n}catch(n){}})(),(({_utilsFns:_utilsFns,_mainFunction:_mainFunction,_args:_args})=>{const utils=Object.fromEntries(Object.entries(_utilsFns).map((([key,value])=>[key,eval(value)])));utils.init(),eval(_mainFunction)(utils,..._args)})({_utilsFns:{init:"() => {\n utils.preloadCache()\n}",stripProxyFromErrors:"(handler = {}) => {\n const newHandler = {\n setPrototypeOf: function (target, proto) {\n if (proto === null)\n throw new TypeError('Cannot convert object to primitive value')\n if (Object.getPrototypeOf(target) === Object.getPrototypeOf(proto)) {\n throw new TypeError('Cyclic __proto__ value')\n }\n return Reflect.setPrototypeOf(target, proto)\n }\n }\n // We wrap each trap in the handler in a try/catch and modify the error stack if they throw\n const traps = Object.getOwnPropertyNames(handler)\n traps.forEach(trap => {\n newHandler[trap] = function () {\n try {\n // Forward the call to the defined proxy handler\n return handler[trap].apply(this, arguments || [])\n } catch (err) {\n // Stack traces differ per browser, we only support chromium based ones currently\n if (!err || !err.stack || !err.stack.includes(` + "`" + `at ` + "`" + `)) {\n throw err\n }\n\n // When something throws within one of our traps the Proxy will show up in error stacks\n // An earlier implementation of this code would simply strip lines with a blacklist,\n // but it makes sense to be more surgical here and only remove lines related to our Proxy.\n // We try to use a known \"anchor\" line for that and strip it with everything above it.\n // If the anchor line cannot be found for some reason we fall back to our blacklist approach.\n\n const stripWithBlacklist = (stack, stripFirstLine = true) => {\n const blacklist = [\n ` + "`" + `at Reflect.${trap} ` + "`" + `, // e.g. Reflect.get or Reflect.apply\n ` + "`" + `at Object.${trap} ` + "`" + `, // e.g. Object.get or Object.apply\n ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // caused by this very wrapper :-)\n ]\n return (\n err.stack\n .split('\\n')\n // Always remove the first (file) line in the stack (guaranteed to be our proxy)\n .filter((line, index) => !(index === 1 && stripFirstLine))\n // Check if the line starts with one of our blacklisted strings\n .filter(line => !blacklist.some(bl => line.trim().startsWith(bl)))\n .join('\\n')\n )\n }\n\n const stripWithAnchor = (stack, anchor) => {\n const stackArr = stack.split('\\n')\n anchor = anchor || ` + "`" + `at Object.newHandler. [as ${trap}] ` + "`" + ` // Known first Proxy line in chromium\n const anchorIndex = stackArr.findIndex(line =>\n line.trim().startsWith(anchor)\n )\n if (anchorIndex === -1) {\n return false // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n return stackArr.join('\\n')\n }\n\n // Special cases due to our nested toString proxies\n err.stack = err.stack.replace(\n 'at Object.toString (',\n 'at Function.toString ('\n )\n if ((err.stack || '').includes('at Function.toString (')) {\n err.stack = stripWithBlacklist(err.stack, false)\n throw err\n }\n\n // Try using the anchor method, fallback to blacklist if necessary\n err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack)\n\n throw err // Re-throw our now sanitized error\n }\n }\n })\n return newHandler\n}",stripErrorWithAnchor:"(err, anchor) => {\n const stackArr = err.stack.split('\\n')\n const anchorIndex = stackArr.findIndex(line => line.trim().startsWith(anchor))\n if (anchorIndex === -1) {\n return err // 404, anchor not found\n }\n // Strip everything from the top until we reach the anchor line (remove anchor line as well)\n // Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. ` + "`" + `TypeError` + "`" + `)\n stackArr.splice(1, anchorIndex)\n err.stack = stackArr.join('\\n')\n return err\n}",replaceProperty:"(obj, propName, descriptorOverrides = {}) => {\n return Object.defineProperty(obj, propName, {\n // Copy over the existing descriptors (writable, enumerable, configurable, etc)\n ...(Object.getOwnPropertyDescriptor(obj, propName) || {}),\n // Add our overrides (e.g. value, get())\n ...descriptorOverrides\n })\n}",preloadCache:"() => {\n if (utils.cache) {\n return\n }\n utils.cache = {\n // Used in our proxies\n Reflect: {\n get: Reflect.get.bind(Reflect),\n apply: Reflect.apply.bind(Reflect)\n },\n // Used in ` + "`" + `makeNativeString` + "`" + `\n nativeToStringStr: Function.toString + '' // => ` + "`" + `function toString() { [native code] }` + "`" + `\n }\n}",makeNativeString:"(name = '') => {\n return utils.cache.nativeToStringStr.replace('toString', name || '')\n}",patchToString:"(obj, str = '') => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === obj) {\n // We either return the optional string verbatim or derive the most desired result automatically\n return str || utils.makeNativeString(obj.name)\n }\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",patchToStringNested:"(obj = {}) => {\n return utils.execRecursively(obj, ['function'], utils.patchToString)\n}",redirectToString:"(proxyObj, originalObj) => {\n const handler = {\n apply: function (target, ctx) {\n // This fixes e.g. ` + "`" + `HTMLMediaElement.prototype.canPlayType.toString + \"\"` + "`" + `\n if (ctx === Function.prototype.toString) {\n return utils.makeNativeString('toString')\n }\n\n // ` + "`" + `toString` + "`" + ` targeted at our proxied Object detected\n if (ctx === proxyObj) {\n const fallback = () =>\n originalObj && originalObj.name\n ? utils.makeNativeString(originalObj.name)\n : utils.makeNativeString(proxyObj.name)\n\n // Return the toString representation of our original object if possible\n return originalObj + '' || fallback()\n }\n\n if (typeof ctx === 'undefined' || ctx === null) {\n return target.call(ctx)\n }\n\n // Check if the toString protype of the context is the same as the global prototype,\n // if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` + "`" + ` test case\n const hasSameProto = Object.getPrototypeOf(\n Function.prototype.toString\n ).isPrototypeOf(ctx.toString) // eslint-disable-line no-prototype-builtins\n if (!hasSameProto) {\n // Pass the call on to the local Function.prototype.toString instead\n return ctx.toString()\n }\n\n return target.call(ctx)\n }\n }\n\n const toStringProxy = new Proxy(\n Function.prototype.toString,\n utils.stripProxyFromErrors(handler)\n )\n utils.replaceProperty(Function.prototype, 'toString', {\n value: toStringProxy\n })\n}",replaceWithProxy:"(obj, propName, handler) => {\n const originalObj = obj[propName]\n const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.redirectToString(proxyObj, originalObj)\n\n return true\n}",replaceGetterWithProxy:"(obj, propName, handler) => {\n const fn = Object.getOwnPropertyDescriptor(obj, propName).get\n const fnStr = fn.toString() // special getter function string\n const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { get: proxyObj })\n utils.patchToString(proxyObj, fnStr)\n\n return true\n}",replaceGetterSetter:"(obj, propName, handlerGetterSetter) => {\n const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName)\n const handler = { ...ownPropertyDescriptor }\n\n if (handlerGetterSetter.get !== undefined) {\n const nativeFn = ownPropertyDescriptor.get\n handler.get = function() {\n return handlerGetterSetter.get.call(this, nativeFn.bind(this))\n }\n utils.redirectToString(handler.get, nativeFn)\n }\n\n if (handlerGetterSetter.set !== undefined) {\n const nativeFn = ownPropertyDescriptor.set\n handler.set = function(newValue) {\n handlerGetterSetter.set.call(this, newValue, nativeFn.bind(this))\n }\n utils.redirectToString(handler.set, nativeFn)\n }\n\n Object.defineProperty(obj, propName, handler)\n}",mockWithProxy:"(obj, propName, pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n\n utils.replaceProperty(obj, propName, { value: proxyObj })\n utils.patchToString(proxyObj)\n\n return true\n}",createProxy:"(pseudoTarget, handler) => {\n const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler))\n utils.patchToString(proxyObj)\n\n return proxyObj\n}",splitObjPath:"objPath => ({\n // Remove last dot entry (property) ==> ` + "`" + `HTMLMediaElement.prototype` + "`" + `\n objName: objPath.split('.').slice(0, -1).join('.'),\n // Extract last dot entry ==> ` + "`" + `canPlayType` + "`" + `\n propName: objPath.split('.').slice(-1)[0]\n})",replaceObjPathWithProxy:"(objPath, handler) => {\n const { objName, propName } = utils.splitObjPath(objPath)\n const obj = eval(objName) // eslint-disable-line no-eval\n return utils.replaceWithProxy(obj, propName, handler)\n}",execRecursively:"(obj = {}, typeFilter = [], fn) => {\n function recurse(obj) {\n for (const key in obj) {\n if (obj[key] === undefined) {\n continue\n }\n if (obj[key] && typeof obj[key] === 'object') {\n recurse(obj[key])\n } else {\n if (obj[key] && typeFilter.includes(typeof obj[key])) {\n fn.call(this, obj[key])\n }\n }\n }\n }\n recurse(obj)\n return obj\n}",stringifyFns:"(fnObj = { hello: () => 'world' }) => {\n // Object.fromEntries() ponyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine\n // https://github.com/feross/fromentries\n function fromEntries(iterable) {\n return [...iterable].reduce((obj, [key, val]) => {\n obj[key] = val\n return obj\n }, {})\n }\n return (Object.fromEntries || fromEntries)(\n Object.entries(fnObj)\n .filter(([key, value]) => typeof value === 'function')\n .map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval\n )\n}",materializeFns:"(fnStrObj = { hello: \"() => 'world'\" }) => {\n return Object.fromEntries(\n Object.entries(fnStrObj).map(([key, value]) => {\n if (value.startsWith('function')) {\n // some trickery is needed to make oldschool functions work :-)\n return [key, eval(` + "`" + `() => ${value}` + "`" + `)()] // eslint-disable-line no-eval\n } else {\n // arrow functions just work\n return [key, eval(value)] // eslint-disable-line no-eval\n }\n })\n )\n}",makeHandler:"() => ({\n // Used by simple ` + "`" + `navigator` + "`" + ` getter evasions\n getterValue: value => ({\n apply(target, ctx, args) {\n // Let's fetch the value first, to trigger and escalate potential errors\n // Illegal invocations like ` + "`" + `navigator.__proto__.vendor` + "`" + ` will throw here\n utils.cache.Reflect.apply(...arguments)\n return value\n }\n })\n})",arrayEquals:"(array1, array2) => {\n if (array1.length !== array2.length) {\n return false\n }\n for (let i = 0; i < array1.length; ++i) {\n if (array1[i] !== array2[i]) {\n return false\n }\n }\n return true\n}",memoize:"fn => {\n const cache = []\n return function(...args) {\n if (!cache.some(c => utils.arrayEquals(c.key, args))) {\n cache.push({ key: args, value: fn.apply(this, args) })\n }\n return cache.find(c => utils.arrayEquals(c.key, args)).value\n }\n}"},_mainFunction:"(utils, opts) => {\n try {\n // Adds a contentWindow proxy to the provided iframe element\n const addContentWindowProxy = iframe => {\n const contentWindowProxy = {\n get(target, key) {\n // Now to the interesting part:\n // We actually make this thing behave like a regular iframe window,\n // by intercepting calls to e.g. ` + "`" + `.self` + "`" + ` and redirect it to the correct thing. :)\n // That makes it possible for these assertions to be correct:\n // iframe.contentWindow.self === window.top // must be false\n if (key === 'self') {\n return this\n }\n // iframe.contentWindow.frameElement === iframe // must be true\n if (key === 'frameElement') {\n return iframe\n }\n // Intercept iframe.contentWindow[0] to hide the property 0 added by the proxy.\n if (key === '0') {\n return undefined\n }\n return Reflect.get(target, key)\n }\n }\n\n if (!iframe.contentWindow) {\n const proxy = new Proxy(window, contentWindowProxy)\n Object.defineProperty(iframe, 'contentWindow', {\n get() {\n return proxy\n },\n set(newValue) {\n return newValue // contentWindow is immutable\n },\n enumerable: true,\n configurable: false\n })\n }\n }\n\n // Handles iframe element creation, augments ` + "`" + `srcdoc` + "`" + ` property so we can intercept further\n const handleIframeCreation = (target, thisArg, args) => {\n const iframe = target.apply(thisArg, args)\n\n // We need to keep the originals around\n const _iframe = iframe\n const _srcdoc = _iframe.srcdoc\n\n // Add hook for the srcdoc property\n // We need to be very surgical here to not break other iframes by accident\n Object.defineProperty(iframe, 'srcdoc', {\n configurable: true, // Important, so we can reset this later\n get: function() {\n return _srcdoc\n },\n set: function(newValue) {\n addContentWindowProxy(this)\n // Reset property, the hook is only needed once\n Object.defineProperty(iframe, 'srcdoc', {\n configurable: false,\n writable: false,\n value: _srcdoc\n })\n _iframe.srcdoc = newValue\n }\n })\n return iframe\n }\n\n // Adds a hook to intercept iframe creation events\n const addIframeCreationSniffer = () => {\n /* global document */\n const createElementHandler = {\n // Make toString() native\n get(target, key) {\n return Reflect.get(target, key)\n },\n apply: function(target, thisArg, args) {\n const isIframe =\n args && args.length && ` + "`" + `${args[0]}` + "`" + `.toLowerCase() === 'iframe'\n if (!isIframe) {\n // Everything as usual\n return target.apply(thisArg, args)\n } else {\n return handleIframeCreation(target, thisArg, args)\n }\n }\n }\n // All this just due to iframes with srcdoc bug\n utils.replaceWithProxy(\n document,\n 'createElement',\n createElementHandler\n )\n }\n\n // Let's go\n addIframeCreationSniffer()\n } catch (err) {\n // console.warn(err)\n }\n }",_args:[]}); -})();` diff --git a/internal/stealth/main.go b/internal/stealth/main.go deleted file mode 100644 index 3ffbe64..0000000 --- a/internal/stealth/main.go +++ /dev/null @@ -1,21 +0,0 @@ -// Package stealth installs the same anti-detection script as -// github.com/go-rod/stealth without importing Rod's launcher-bearing root -// package. -package stealth - -import ( - "github.com/go-rod/rod/lib/proto" - "github.com/tamnd/kage/internal/rod" -) - -// Page creates a page and installs the stealth script before navigation. -func Page(browser *rod.Browser) (*rod.Page, error) { - page, err := browser.Page(proto.TargetCreateTarget{}) - if err != nil { - return nil, err - } - if _, err := page.EvalOnNewDocument(JS); err != nil { - return nil, err - } - return page, nil -}