EV-WebKit
view release on metacpan or search on metacpan
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.
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
"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 )