mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-10-03 14:06:51 +02:00
144 lines
5.7 KiB
JavaScript
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;
|
|
};
|
|
})();
|