EV-WebKit

 view release on metacpan or  search on metacpan

README  view on Meta::CPAN

    loop. The caller starts and stops that loop ("EV::run", "EV::break") --
    no EV::WebKit method ever calls either for you. Methods that need to
    wait for a result take a trailing callback:

        sub { my ($result, $err) = @_; ... }

    On success, $err is "undef" and $result holds the method's result (shape
    documented per method below). On failure, $result is "undef" and $err is
    a short, human-readable string -- Perl's own " at FILE line N."
    diagnostic suffix is stripped where it would otherwise appear -- such as
    "timeout", "browser closed", or a cleaned JavaScript exception message.
    Methods never throw for ordinary runtime failures; always check $err.
    Some methods are plain synchronous accessors/mutators and take no
    callback at all: the state readers ("uri", "title", "is_loading",
    "status", "can_go_back", "can_go_forward"), "stop", the configuration
    setters ("settings", "set_user_agent"/"user_agent", "set_proxy", "zoom",
    "show_devtools", "mock_scheme"), the user-content methods
    ("add_user_script"/"add_user_style" and their removes), the fingerprint
    accessors, and "quit". Where it is not obvious from the usage line, the
    method's own entry says so.

    "EV::break" is safe to call directly from the trailing "($result, $err)"
    callbacks described above, and from "on_load", "on_error", "on_close"
    and "on_navigate", since all of those run on a clean EV tick. So do
    "on_request" and "on_response", which run in the proxy rather than in
    WebKit at all. "on_console", "on_dialog", "on_policy",
    "on_file_chooser", "on_download", "on_authenticate" and a "mock_scheme"
    producer, however, all fire synchronously inside WebKit's own dispatch
    frame -- do NOT call "EV::break" directly from those; schedule it
    instead, e.g. "EV::timer(0, 0, sub { EV::break })". (Calling "quit" from
    them is safe: it detects the frame and defers its own teardown.)

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

README  view on Meta::CPAN

        really is WebKit, so a Safari profile is a small lie and a Chrome
        one a larger one, but claiming Gecko means supplying a surface no
        WebKit build has: "productSub" (Gecko reports 20100101 where WebKit
        and Chromium report 20030107), "oscpu" and "buildID", which exist in
        no other engine and whose ABSENCE identifies the engine as surely as
        a wrong value, and "window.mozInnerScreenX"/"Y". Those are supplied.
        What is not, and cannot be without the engine, is CSS:
        "CSS.supports('-moz-appearance', 'none')" is false here and true in
        Firefox, and no property override fixes that without breaking the
        styling it claims to support. Use "windows-firefox" for the
        network-layer identity -- where it is exact -- and expect a
        determined JS-layer engine probe to see through it.

        A coherence layer fills the gaps a bare navigator/screen spoof would
        leave: a Chrome profile also gets "window.chrome" and a working
        "navigator.userAgentData" (brands/platform plus an async
        "getHighEntropyValues"); a mobile profile sizes the window to the
        profile's screen (so "window.innerWidth <= screen.width"), adds
        "ontouchstart", and overrides the "pointer"/"hover"/"resolution"
        media queries. Unlike the native navigator/screen getters, this
        layer -- and the WebGL "getParameter" override -- is installed as JS
        (a native replacement cannot delegate the non-spoofed cases: a JSC C
        function receives no "this"). The values are correct and consistent,
        but their getters/methods show JS source under a
        "Function.prototype.toString.call" (or a getter-"toString") check,
        so a determined script can still detect the
        "userAgentData"/"matchMedia"/WebGL wrappers.

        WebGL spoofs the full per-profile capability set, not only the
        UNMASKED vendor/renderer strings: the numeric parameters
        ("MAX_TEXTURE_SIZE" and friends), the supported-extension list, and
        "getShaderPrecisionFormat" all return the claimed GPU family's
        values on both WebGL1 and WebGL2, coherent with the renderer string.
        The advertised list is authoritative: "getExtension" returns "undef"
        for anything not on it, the real object when the host GL genuinely
        has it, and otherwise a minimal stub (carrying that extension's
        constants for the commonly probed ones, an empty object for the rest
        -- see the Ceiling notes below). Extension names are matched
        case-insensitively, as the spec requires, and an extension's own
        pnames (the UNMASKED pair, "MAX_TEXTURE_MAX_ANISOTROPY_EXT") are
        answered only once "getExtension" has enabled that extension on the
        context -- before that they report "null" and raise "INVALID_ENUM",
        exactly as a real context does.

        The capability tables are a curated subset covering the parameters
        fingerprinters actually read; a pname not in the table falls through
        to the real host value.

        The DOM interface set is aligned per profile too: a Chrome profile
        exposes "navigator.connection", "usb", "bluetooth", "getBattery",
        "scheduling" and "RTCPeerConnection" (the Android profile correctly
        omits "hid"/"serial"); a Safari profile exposes only "storage" and
        "RTCPeerConnection". Every stub is installed only when the build
        lacks the real API, so a WebKitGTK that ships one keeps it.

        PDF viewer presence follows the profile as well. The HTML
        specification hardcodes both states: a browser that displays PDFs
        inline reports "navigator.pdfViewerEnabled" true and five fixed
        plugin names, one that does not reports false and empty
        "plugins"/"mimeTypes" lists. WebKitGTK reports the viewer-present
        state, which is correct for desktop Chrome, desktop Safari and iOS
        Safari -- but not for "pixel-chrome": Chrome for Android had no
        inline PDF viewer at 131 (it shipped 2024-11, the Android viewer
        appeared behind a flag in 2024-12 and became default-on only in
        Chrome 135, 2025-04), so that profile reports the empty state.
        Override per instance with "pdf_viewer => 0|1". The empty lists are
        real "PluginArray"/ "MimeTypeArray" objects, cached like a real
        browser's, with "length" left on the prototype where it belongs.

        Ceiling: the spoof is thorough but not perfect, and these residuals
        remain. Workers are not covered at all. The extension hooks
        "window-object-cleared", which fires only for window globals, so a
        "Worker"/"SharedWorker"/"ServiceWorker" global keeps the real
        "navigator.platform", "languages" and hardware values and gets no
        readback noise -- while its "userAgent" is spoofed (that comes from
        the browser settings, not this extension). Reading
        "navigator.platform" on both sides of a "postMessage", or hashing an
        "OffscreenCanvas" inside a worker, defeats the whole layer; treat a
        page that uses workers as unprotected. The native navigator/screen
        getters are also still identifiable by the source text
        "Function.prototype.toString" reports for them: a real accessor
        renders as "function <prop>() { [native code] }" while these render
        as "function get() { [native code] }". The name and "[native code]"
        marker are correct, but the embedded identifier is not, and it
        cannot be corrected without replacing the getter with JavaScript --
        which costs far more than it saves. The JS-installed layers
        ("userAgentData"/"matchMedia"/WebGL/readback/feature stubs) show JS
        source under "Function.prototype.toString.call" and under a plain
        toString(), so a determined script can still detect them. They
        deliberately carry no own "toString" mask: such a mask defeats only
        the plain check -- "Function.prototype.toString.call" bypasses an
        own property and reveals the wrapper anyway -- while leaving an
        artifact no real function has, which "Object.keys" enumerates across
        the whole JS layer with no false positives. Trading a weak defence
        for a precise tell is a bad exchange, so the wrappers are left
        honest. Readback noise, when "seed" is set, is content-independent,
        so a script that renders a known image and reads it back can recover
        and undo it. It is also applied at read time rather than stored, so
        it does not survive a round trip: writing back what was just read
        ("putImageData"), or encoding and re-decoding through
        "toDataURL"/"toBlob", yields the un-noised pixels, and comparing the
        two detects that noise is active without knowing the content.
        Without "seed", canvas/AudioContext/WebGL-pixel readback reflects
        the real host output (often software/llvmpipe) and is not disguised
        at all. The "matchMedia" override answers JS queries (including
        compound and comma-separated ones), but CSS @media rules are
        evaluated by the engine and still reflect the real device, so a page
        that compares "getComputedStyle" against "matchMedia" sees a
        contradiction on a mobile or hi-DPI profile. The WebGL capability
        values are the canonical set for each GPU family, so a fingerprinter
        with a per-driver database could still find a mismatch, and any
        pname outside the curated tables still reports the host's real
        value. Stubbed extensions and "RTCPeerConnection" have no real
        runtime behaviour (no ICE, no devices), so a script that exercises
        their functionality -- rather than merely detecting their presence
        -- can spot the stub; an advertised extension the host GL lacks is
        an object with the right constants but no working methods.
        "navigator.languages" is a real array with the profile's tags, but
        not a "FrozenArray": a real browser caches one frozen array and
        returns it every time, so "navigator.languages ===
        navigator.languages" and "Object.isFrozen(navigator.languages)" are



( run in 0.734 second using v1.01-cache-2.11-cpan-b16cb0d3907 )