EV-WebKit

 view release on metacpan or  search on metacpan

README  view on Meta::CPAN

CONSTRUCTOR
  available
        my $ok = EV::WebKit->available;

    Returns true if the required
    WebKit-6.0/Gtk-4.0/Gdk-4.0/JavaScriptCore-6.0 and Soup-3.0
    GObject-Introspection typelibs can be loaded, false otherwise. Safe to
    call before "new" to fail gracefully (e.g. to "plan skip_all" a test)
    instead of letting "new" die. Checking typelib availability does not
    require a display.

  new
        my $b = EV::WebKit->new(%options);

    Constructs a new browser: sets up (once per process) the GObject
    Introspection typelibs, initializes GTK4 (only once a display is known
    -- see "LIMITATIONS"), creates a WebKit network session, user content
    manager, web context and view, and shows a native GTK4 window containing
    it. Dies if the typelibs are unavailable or if no X display can be
    determined (see "display" below). %options:

    "window => [$width, $height]"
        Initial window size in pixels. Default "[1280, 1024]".

        A "fingerprint" profile overrides this where the two would
        contradict each other, since a window larger than the screen it
        claims to be on is itself a tell: a mobile profile sizes the window
        to the profile's own screen and ignores "window" outright, and a
        desktop profile caps each dimension at its screen's.

    "display => ':N'"
        Sets $ENV{DISPLAY} to this value before initializing GTK. If
        omitted, an already-exported $DISPLAY is used; if neither is
        available, "new" dies telling you to run under "xvfb-run" or pass
        this option -- EV::WebKit never starts an X server itself (see
        "LIMITATIONS").

        One display per process. GTK connects to a display once and cannot
        be moved to another, so every instance after the first shares the
        first one's display. Passing a "display" that disagrees with it
        croaks rather than being silently ignored.

    "timeout => $seconds"
        Default per-operation timeout, in seconds. Applies to every async
        operation that can block -- navigation
        ("go"/"load_html"/"back"/"forward"/ "reload"),
        "script"/"script_async", "find"/"find_all"/"find_js"/ "find_all_js"
        and the EV::WebKit::Element accessors, "frames" and everything
        routed through a frame, "resize", "html", "screenshot", "pdf", and
        the cookie operations
        ("set_cookie"/"cookies"/"clear_cookies"/"save_cookies"/"load_cookies
        ") -- and is the default for "wait_for"'s and "pdf"'s own "timeout"
        option, and "wait_for_navigation"'s. On expiry the operation's
        callback is resolved with "$err eq 'timeout'". Default 30.

        "download" is the deliberate exception: a large file legitimately
        takes longer than any per-operation timeout would allow, so a
        download is bounded only by the server and by "quit". Use
        "$dl->cancel" to end one early.

    "popups => 'follow' | 'block'"
        What to do with a navigation that asks for a new window. Documented
        in full under "EVENTS", with the rest of the options that shape how
        the browser reacts to the page.

    "user_agent => $string"
        Sets the initial User-Agent (equivalent to calling "set_user_agent"
        right after construction).

    "ephemeral => $bool"
        Use an ephemeral (in-memory, non-persistent) network session when
        true, or an on-disk/persistent one when false. Default 1. Forced to
        0 automatically when "cookie_jar" is given -- native cookie
        persistence requires a non-ephemeral session (see "cookie_jar"
        below).

    "devtools => 1"
        Enables the "enable-developer-extras" setting at construction time
        (required before the Web Inspector will do anything useful; see
        "show_devtools").

    "title => $string"
        Sets the native GTK4 window's title.

    "chrome => 1"
        Build a minimal browser chrome: a GNOME header bar with back,
        forward and reload buttons and an address entry, installed as the
        window title bar. Intended for visible use on a real display;
        harmless under xvfb-run. The reload button turns into a stop button
        while a page is loading. The address entry navigates on Enter
        (https:// is assumed when no scheme is given) and tracks the current
        page uri except while it has keyboard focus. The window title
        follows the page title. Automation methods keep working unchanged.

    "cookie_jar => $path"
        Configures $path as this instance's native, WebKit-managed
        persistent cookie store (forces a non-ephemeral session -- see
        "ephemeral" above). Cookies with a real expiry (a "max_age" greater
        than 0, or a "Set-Cookie: ...; Max-Age="/"Expires=" response header)
        are written to $path automatically and read back automatically by
        any later instance pointed at the same file -- no
        "save_cookies"/"load_cookies" call needed. SESSION cookies (no
        expiry) are *excluded* from this store by design (RFC 6265, same as
        every real browser); use "save_cookies"/"load_cookies" to snapshot
        those. See "Cookie Management" and "LIMITATIONS". Do not point
        save_cookies/load_cookies at the same file as cookie_jar: the native
        store and the JSON snapshot are different formats written by
        independent writers, and sharing a path will corrupt the file.

    "jar_format => 'sqlite' | 'text'"
        Storage format for the persistent cookie store. "sqlite" (default)
        is queryable with "sqlite3"; "text" is a human-readable
        Netscape-format cookie file. It applies to whichever store exists:
        "cookie_jar"'s file if you gave one, otherwise "data_dir"'s own
        ("cookies.txt" under "text", rather than "cookies.sqlite"). Ignored
        only when neither option is given.

    "data_dir => $path"
        Points this instance's entire session -- cookies, "localStorage",
        "IndexedDB", the HTTP cache, service-worker state -- at $path, and
        restores it whenever an instance is later built with the same $path.

README  view on Meta::CPAN

        samples -- so the same sample re-read through any API, rectangle or
        offset gives the same value. A fully opaque pixel gets an LSB flip.
        A partially transparent one is moved to an adjacent reachable value
        instead: "getImageData" returns un-premultiplied bytes, so only a
        lattice of values is producible at a given alpha and an LSB flip
        would land off it (the step is therefore larger than one LSB at low
        alpha). WebGL "readPixels" returns the premultiplied value directly,
        so there the step is applied to that value. Only engine-rendered
        audio buffers are touched, never one the page authored. The seed is
        reduced modulo 2**32, so seeds congruent mod 2**32 give identical
        noise. That makes the hardware-readback hash stable within a
        session, yet different from the automation host's real output
        (hiding llvmpipe/software GL) and different across seeds -- so the
        same profile can present distinct machines. Wrapped: "getImageData",
        "toDataURL"/"toBlob" (via an offscreen copy, so the encoded image
        carries the noise and WebGL-backed canvases are covered too),
        "AudioBuffer.getChannelData"/"copyFromChannel", the "AnalyserNode"
        frequency and time-domain readers, and "readPixels". Without "seed",
        none of this is installed and readback behaves exactly as before.
        See the Ceiling notes under "fingerprint" above for the residuals.

    "network_fingerprint => 1" or "network_fingerprint => 'chrome124'"
        Also match the connection fingerprint (TLS JA3/JA4 + HTTP/2 Akamai)
        to the "fingerprint" profile, so the origin sees one coherent device
        at the network layer too. Requires "fingerprint". It spins an
        in-process Proxy::Impersonate on this instance's EV loop and routes
        the browser through it: the proxy terminates WebKit's TLS locally
        and re-originates each request as the matching real browser via
        "libcurl-impersonate". The curl target is derived from the profile
        ("windows-chrome" -> "chrome150", "macos-safari" -> "safari26_0",
        "iphone-safari" -> "safari26_0_ios", "windows-firefox" ->
        "firefox147", "pixel-chrome" -> "chrome131_android"); pass a string
        to override it.

        "pixel-chrome" stays on Chrome 131 where the others track current
        stable, because "chrome131_android" is the newest Android target
        "libcurl-impersonate" ships and a profile must not claim a browser
        its TLS cannot back.

        The profile's identity headers (User-Agent + "Sec-CH-UA") are forced
        over the curl target's defaults, so even a Windows profile is
        coherent on the (macOS-built) "chrome131" target -- Windows and
        macOS Chrome share the same TLS/HTTP2, so only the header values
        differ. WebKit is told to accept the proxy's self-signed cert
        (set_tls_errors_policy('ignore')); this is safe because the
        browser-to-proxy hop is localhost and the proxy re-verifies the real
        origin upstream. WebKitGTK 6.0 exposes no custom-CA path (a spike
        confirmed it honors neither "SSL_CERT_FILE" nor a settable
        "GTlsDatabase"), which is why the "IGNORE" policy is used.

        Requires the optional Proxy::Impersonate toolchain (which builds
        "curl-impersonate" via Alien::curlimpersonate); croaks if it is
        unavailable. Mutually exclusive with an explicit "proxy". Out of
        scope: WebSockets, HTTP/3. See "network_fingerprint" and
        "proxy_port".

    "on_error", "on_load", "on_navigate", "on_close", "on_console",
    "on_dialog", "on_policy", "on_file_chooser", "on_download",
    "on_authenticate", "on_request", "on_response"
        Event callbacks -- see "EVENTS", which documents each one and what
        it is handed. ("popups", above, is documented there too: it is what
        happens when no "on_policy" is set.)

METHODS
  Navigation
    Load pages and read basic document state.

   go
        $b->go($uri, sub { my ($result, $err) = @_; ... });

    Loads $uri. On success $result is true; on failure (or timeout) $err is
    set. If a previous navigation on this instance was still in-flight, its
    callback is immediately invoked with "$err eq 'superseded'". The
    callback fires just after WebKit's own "load-changed:finished" signal --
    once the document "title" has crossed from the web process, which it
    does a fraction of a millisecond later ("uri" needs no such wait; it is
    set before "finished"). A page with no "<title>" never sends that
    notification, and settles on a 150ms deadline instead. "on_load" (if
    configured) fires right after the callback. Returns $b (chainable).

    Same-document navigation is not observable from here, and resolves with
    "$err eq 'timeout'". A fragment-only go("$here#section"), and
    "back"/"forward" across such a boundary, change the uri without loading
    anything -- and WebKitGTK emits no load event for them, so nothing tells
    this module they happened. The uri does move ("uri" reports it, and the
    page really did navigate); only the callback is left waiting.

    Predicting it instead of observing it was tried and abandoned: every
    rule for "this one will not reload" is falsifiable -- by the outgoing
    page touching its own hash while the new load is in flight, by
    "history.pushState" having moved the uri out from under the guess, by a
    web-process crash -- and each falsification reports success for a page
    that never loaded, which is worse than the wait it removes. So drive
    these from the page, where they are not a guess:

        $b->script('location.hash = "section"; return location.href;', $cb);

    and use "wait_for"/"wait_for_js" if the page reacts asynchronously.

   load_html
        $b->load_html($html, sub { my ($result, $err) = @_; ... });

    Loads a literal HTML string as the document, with the same completion
    semantics as "go" (no URI, so it does not count toward "save_cookies"'s
    default URI list). Returns $b.

  Navigation history
        $b->back(sub { my ($ok, $err) = @_; ... });     # optional callback
        $b->forward($cb);
        $b->reload($cb);
        $b->stop;
        $b->can_go_back;      # 1 or 0
        $b->can_go_forward;   # 1 or 0

    back, forward and reload behave like go: the optional trailing callback
    is invoked as ($ok, $err) when the resulting navigation finishes (or
    fails or times out). Calling back/forward when the history has no entry
    in that direction invokes the callback with the error 'cannot go back' /
    'cannot go forward', and reload on an instance that has never navigated
    with 'nothing to reload' (as go with no uri gives 'go: uri required').
    stop aborts the current load and returns the browser object; it takes no

README  view on Meta::CPAN

    "set_proxy", "mock_scheme", "show_devtools") become no-ops that just
    return $b.

    An operation already in flight at the moment "quit" is called is
    resolved deterministically, exactly once, rather than left dangling.
    Every pending
    "script"/"script_async"/"find"/"find_all"/"html"/"screenshot"/"pdf"
    call, cookie call, outstanding "wait_for", and navigation resolves with
    "$err eq 'browser closed'". Any call made *after* "quit" has returned
    likewise resolves immediately with 'browser closed'.

    "quit" never throws. It has to run your callbacks in order to resolve
    them, and one of them dying must not abort the teardown -- that would
    drop every callback still queued behind it and leak the window, view and
    session for the life of the process (nothing could retry: "quit" is
    already marked done). An exception from a callback is caught and
    reported with "warn".

    Calling "quit" from inside an event handler ("on_dialog", "on_policy",
    "on_console", "on_file_chooser", "on_download", "on_authenticate", or a
    "mock_scheme" producer) is safe. Those run inside WebKit's own dispatch
    frame, so "quit" defers the teardown -- and the callbacks it resolves --
    to the next clean tick of the loop rather than running them nested
    inside that frame, where an "EV::break" from one of them would wedge the
    loop (see "CALLBACK CONVENTION").

    'browser closed' reports how the callback was resolved, not whether the
    operation's effect took place. A cookie mutation already in flight when
    "quit" lands -- "set_cookie", "save_cookies" (which may still write its
    file), or "clear_cookies" -- can still complete its native effect even
    though its callback reports 'browser closed', because cancelling it
    mid-flight would risk a use-after-free during teardown. Treat 'browser
    closed' on an in-flight mutation as "outcome unknown", not "did not
    happen". (This does not apply to calls made *after* "quit", which never
    start any native work.)

  Handler accessors
        my $cb = $b->on_console;          # get
        $b->on_console(sub { ... });      # set, returns $b
        $b->on_console(undef);            # clear

    Ten of the twelve "on_*" handlers have a get/set accessor: "on_load",
    "on_error", "on_close", "on_navigate", "on_console", "on_dialog",
    "on_policy", "on_file_chooser", "on_download" and "on_authenticate". The
    other two, "on_request" and "on_response", are construct-time only --
    they need the in-process proxy built during "new" -- and have none.

    An accessor means code that did not construct the browser can still
    observe a handler, and can chain an existing one rather than clobbering
    it:

        my $prev = $b->on_console;
        $b->on_console(sub { $prev->(@_) if $prev; ...also mine... });

    Croaks on a non-coderef. Enabling "on_console" after a page has loaded
    takes effect from the next navigation: the console proxy is a user
    script, and those are injected at document start.

EVENTS
    Optional callbacks passed to "new", and the one option that shapes what
    the browser does when it has none ("popups"):

    "on_error => sub { my ($err) = @_ }"
        Called for a navigation failure that has no "go"/"load_html"
        callback waiting for it (e.g. a stray "load-failed" signal).
        Ordinary navigation failures go to that call's own callback instead,
        not here.

    "on_load => sub { }"
        Called with no arguments when a navigation started through this API
        ("go", "load_html", "back", "forward", or "reload") finishes
        successfully, right after that navigation's own callback (if any).
        It does NOT fire for user- or page-JS-initiated navigations (e.g.
        clicking a link, or a script-driven redirect) -- only for
        navigations this instance itself started through one of the methods
        above.

    "on_console => sub { my ($text) = @_ }"
        Called for each "console.log"/"warn"/"error"/"info" from page
        JavaScript. $text is a single string of the form "$level: $args",
        e.g. "log: hi". Implemented by monkey-patching "console" via an
        injected user script plus a script-message handler, not WebKit's
        native console-message signal.

    "on_dialog => sub { my ($dialog) = @_ }"
        Called for "window.alert"/"confirm"/"prompt" and the beforeunload
        confirmation. $dialog is an "EV::WebKit::Dialog" object, valid only
        for the duration of this call. If "on_dialog" is not given, every
        dialog is auto-dismissed so the page is never blocked.

    "on_navigate => sub { my ($uri) = @_ }"
        Called for every navigation that commits, whoever started it --
        including one the page starts itself, which is what a human clicking
        a link in a visible window looks like.

        "on_load" is not that. It fires only for a navigation this API
        started, so without "on_navigate" a browser you are also using by
        hand can change page and tell you nothing at all. An API navigation
        fires both.

        Delivered on a clean EV tick, so "EV::break" is safe from it.

    "on_close => sub { }"
        Called when the user closes the window (the titlebar close button,
        alt-F4, the window manager) -- not when you call "quit" yourself.
        Only reachable in the visible mode (a real $DISPLAY, usually with
        "chrome => 1").

        The instance is torn down first: every in-flight callback resolves
        with 'browser closed', the native window is destroyed, and only then
        is "on_close" called. So by the time it runs, $b is already closed
        -- it is a notification, not a veto.

        It does not stop your "EV::run" -- nothing in this module ever does;
        you own the loop. For a browser window whose closing should end the
        program, that is the whole handler:

            my $b = EV::WebKit->new(chrome => 1, on_close => sub { EV::break });
            ...
            EV::run;   # returns when the window is closed

        Unlike "on_console"/"on_dialog"/"on_policy", "on_close" is delivered
        on a clean EV tick, so calling "EV::break" directly from it is safe.

    "popups => 'follow' | 'block'"
        What to do with a navigation that asks for a new window -- a
        "target="_blank"" link, or "window.open". WebKit allows such a
        navigation and then asks for a window to put it in; a one-view
        browser has none to give, so the click would otherwise do nothing
        whatsoever: no navigation, no error, no event. The default 'follow'
        takes it in this view instead. 'block' keeps it dropped.

        The two arrive by different routes, which matters if you set
        "on_policy". A "target="_blank"" link is a policy decision, so that
        handler sees it first, with "type => 'new-window-action'", and can
        refuse it outright with "$p->block".

        "window.open" is not a window request WebKitGTK asks about, so no
        "new-window-action" ever arrives for it -- there is nothing to
        refuse at that stage. Under the default 'follow', though, the popup
        is re-issued in this view as an ordinary navigation, and that does
        reach "on_policy" as a "navigation-action" carrying the popup's own
        url. So selective filtering is possible for both mechanisms; only
        the decision "type" differs. (Under 'block' the popup is dropped
        before any navigation, so "on_policy" sees nothing at all.)

        One caveat if you are testing this: WebKit's own popup blocker drops
        a "window.open" made from an inline script with no user gesture
        behind it, and then nothing reaches "on_policy" either. Drive it
        from a real click -- "$el->click" counts -- as a page would.

        What "on_policy" does not decide is where an allowed one goes:
        WebKit asks for a window afterwards either way, and this option is
        what answers. So an "on_policy" that allows a "target="_blank"" link
        still lands it in this view under 'follow', and still drops it under
        'block'. If you want to route it yourself, "$p->block" and navigate
        from a clean tick -- starting a navigation inside the handler runs
        it in WebKit's own dispatch frame.

    "on_policy => sub { my ($info) = @_ }"
        Called for each navigation/new-window/response decision WebKit asks
        about. $info is an "EV::WebKit::Policy" object, valid only for the
        duration of this call. If "on_policy" is not given, WebKit's own
        default (allow) applies; if the handler doesn't call
        "allow"/"block", allow happens automatically once it returns.

        If the handler dies before deciding, the navigation is blocked and
        the exception reported with "warn". This handler is a gate, so it
        fails closed: a page that could provoke a die (a URI that breaks the
        handler's own parsing, say) would otherwise walk straight through
        it, since an exception escaping the handler leaves WebKit to apply
        its own default -- allow. A handler that already called "allow" or
        "block" keeps that decision even if it then dies.

    "on_download => sub { my ($download) = @_ }"
        Called when the page starts a download. The handler must name a
        destination with "save_to" or the download is cancelled -- see
        "on_download" under "Downloads and file upload" for the object's
        full interface and the reasoning.

    "on_file_chooser => sub { my ($chooser) = @_ }"
        Called when the page opens a file chooser, which is the only way to
        populate an "<input type=file>". Without this handler WebKit runs
        its own native chooser, unchanged. See "on_file_chooser" for the
        object it receives.

    "on_authenticate => sub { my ($auth) = @_ }"
        Answer an HTTP or proxy authentication challenge. Without a handler
        the challenge is cancelled and the navigation fails at once rather
        than waiting. See "on_authenticate".

    "on_request => sub { my ($req) = @_ }"
        Intercept, rewrite, mock or block every request the browser makes.
        Routes through the in-process proxy, so it does not see
        local-address traffic and it sets a connection fingerprint -- see
        "on_request" for both caveats.

    "on_response => sub { my ($res) = @_ }"
        Observe or rewrite each response's status and headers before the
        page sees them -- stripping "Content-Security-Policy" is the usual
        reason. Same proxy, same caveats. See "on_response".

EV::WebKit::Dialog
    Passed to "on_dialog". Valid only for the duration of that call.

    "type"
        Nick string: "alert", "confirm", "prompt", or



( run in 1.278 second using v1.01-cache-2.11-cpan-364913b4093 )