Files
phishingclub/backend/embedded/remotebrowser_prelude.js
2026-09-12 20:17:05 +02:00

144 lines
5.7 KiB
JavaScript

// Remote browser script prelude.
//
// Adds a small state machine on top of the session so a script declares how to
// recognize each page once, then runs a loop that acts on the current page.
// Loaded into the script VM before the user script, so these helpers are ready
// when newSession() is called. Built only on the public session methods.
//
// Matchers, actions and hooks read and drive the page through the session s in
// their own scope; there is no separate page object.
(function () {
if (typeof newSession !== "function") {
return;
}
var baseNewSession = newSession;
// firstMatch checks each rule in insertion order and returns the first state
// name whose matcher is truthy, or null when none match. A matcher that throws
// counts as no match.
function firstMatch(rules) {
var names = Object.keys(rules);
for (var i = 0; i < names.length; i++) {
var name = names[i];
var hit = false;
try { hit = !!rules[name](); } catch (e) { hit = false; }
if (hit) { return name; }
}
return null;
}
// waitForState polls the rules until one matches or the timeout runs out.
// Returns the matching state name, or "timeout".
function waitForState(s, rules, timeoutMs) {
if (!timeoutMs) { timeoutMs = 10000; }
var deadline = Date.now() + timeoutMs;
while (true) {
var name = firstMatch(rules);
if (name) { return name; }
if (Date.now() >= deadline) { return "timeout"; }
s.wait(250);
}
}
// detectNext polls until a state other than prev matches, or the timeout runs
// out. prev is null on the first cycle, so any state counts. Waiting for a
// different state is what stops run from firing the same action twice while a
// page is still submitting or waiting for approval.
function detectNext(s, rules, prev, timeoutMs) {
if (!timeoutMs) { timeoutMs = 10000; }
var deadline = Date.now() + timeoutMs;
while (true) {
var name = firstMatch(rules);
if (name && name !== prev) { return name; }
if (Date.now() >= deadline) { return "timeout"; }
s.wait(250);
}
}
newSession = function (opts) {
// Default to {} so a no-argument newSession() call does not forward
// undefined, which the native binding would reject when it parses options.
var s = baseNewSession(opts || {});
// present is true when the selector matches at least one node.
s.present = function (sel) { return s.getNodeCount(sel) > 0; };
// visible is true when the first match is rendered and not hidden. This is
// an instant check, unlike waitVisible which blocks.
s.visible = function (sel) {
return s.evaluate(
"(function(){var e=document.querySelector(" + JSON.stringify(sel) + ");" +
"if(!e){return false;}var r=e.getBoundingClientRect();var st=getComputedStyle(e);" +
"return (r.width>0||r.height>0)&&st.visibility!=='hidden'&&st.display!=='none';})()"
) === true;
};
// query returns a URL query parameter value from the current page, decoded,
// or null when the parameter is absent.
s.query = function (name) {
var key = String(name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
var m = s.location().match(new RegExp("[?&]" + key + "=([^&]*)"));
return m ? decodeURIComponent(m[1]) : null;
};
// waitForState returns the current state name for the given rules, or
// "timeout" if none match within timeoutMs (default 10000). Use it directly
// to run your own loop instead of states().run().
s.waitForState = function (rules, timeoutMs) {
return waitForState(s, rules || {}, timeoutMs);
};
// states builds a state machine from the detection rules (name -> matcher).
// The returned machine carries optional before() and after() hooks and a
// run() loop, so a script reads as: s.states({...}).run({...}).
s.states = function (rules) {
var beforeFn = null;
var afterFn = null;
var machine = {
// before registers a callback run just before each detected step's
// action. It receives the state name.
before: function (fn) { beforeFn = fn; return machine; },
// after registers a callback run just after each step's action. It
// receives the state name.
after: function (fn) { afterFn = fn; return machine; },
// run drives the loop: detect the state, run its action, then wait for
// the state to change and run the next action. An action ends the loop
// by returning false or calling loop.stop(). The built in "timeout"
// state fires when the state does not change within detectTimeout.
// opts: { detectTimeout, timeout } in milliseconds.
run: function (actions, opts) {
opts = opts || {};
var detectTimeout = opts.detectTimeout || 10000;
var overall = opts.timeout || 0;
var startedAt = Date.now();
var stopped = false;
var loop = { state: null, stop: function () { stopped = true; } };
var prev = null;
while (true) {
var state;
if (overall && Date.now() - startedAt > overall) {
state = "timeout";
} else {
state = detectNext(s, rules, prev, detectTimeout);
}
loop.state = state;
var action = actions[state];
if (!action) {
if (typeof log === "function") { log("[run] no action for state", { state: state }); }
return state;
}
if (beforeFn) { beforeFn(state); }
var result = action(loop);
if (afterFn) { afterFn(state); }
if (result === false || stopped) { return state; }
prev = state;
}
}
};
return machine;
};
return s;
};
})();