mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-09-21 00:10:43 +02:00
add page editor auto complete for remote browser variable 'rb'
Signed-off-by: RonniSkansing <rskansing@gmail.com>
This commit is contained in:
@@ -1,3 +1,155 @@
|
||||
<script context="module">
|
||||
import * as monacoModule from 'monaco-editor';
|
||||
|
||||
// Registered once for the whole app. The providers are global to the html
|
||||
// language, so a module level guard keeps a second editor from adding a
|
||||
// duplicate. rb is the client that {{RemoteBrowserScript "name"}} injects into
|
||||
// a phishing page, so the features are only offered inside a <script> block.
|
||||
let rbFeaturesRegistered = false;
|
||||
|
||||
// One source of truth for both completion and hover.
|
||||
const RB_OBJECT_DETAIL = 'Remote Browser client (window.rb)';
|
||||
const RB_OBJECT_DOC =
|
||||
'`rb` (alias `window.rb`). Client injected by `{{RemoteBrowserScript "name"}}`. Connects the phishing page to the remote browser session over WebSocket.\n\nMethods: `on(event, handler)`, `send(event, data)`, `mountStream(name, element)`.';
|
||||
|
||||
const RB_MEMBERS = [
|
||||
{
|
||||
name: 'on',
|
||||
detail: '(event: string, handler: (data) => void): void',
|
||||
insertText: "on('${1:event}', function (data) {\n\t$0\n})",
|
||||
doc: 'Subscribe to an event the remote browser script sends with `emit()`.\n\nStream form: `rb.on("stream_start", name, handler)` and `rb.on("stream_stop", name, handler)` fire when a named stream starts or stops.'
|
||||
},
|
||||
{
|
||||
name: 'send',
|
||||
detail: '(event: string, data?: any): void',
|
||||
insertText: "send('${1:event}'${2:, data})",
|
||||
doc: 'Send an event and optional data to the running remote browser script. The script receives it through `waitForEvent()` or `s.on()`.'
|
||||
},
|
||||
{
|
||||
name: 'mountStream',
|
||||
detail: '(name: string, element: HTMLElement, opts?: object): void',
|
||||
insertText: "mountStream('${1:name}', ${2:element})",
|
||||
doc: 'Mount a named element stream the script started with `s.stream(selector, name)` into a page element, for example a captcha. Forwards mouse and keyboard input back to the streamed element.'
|
||||
}
|
||||
];
|
||||
|
||||
// beforeText is the source from the document start up to a point. True when
|
||||
// that point sits inside an open <script> block.
|
||||
function rbInScript(beforeText) {
|
||||
const openIdx = beforeText.lastIndexOf('<script');
|
||||
const closeIdx = beforeText.lastIndexOf('</script');
|
||||
return openIdx !== -1 && openIdx >= closeIdx;
|
||||
}
|
||||
|
||||
function ensureRbLanguageFeatures() {
|
||||
if (rbFeaturesRegistered) return;
|
||||
rbFeaturesRegistered = true;
|
||||
const Kind = monacoModule.languages.CompletionItemKind;
|
||||
const AsSnippet = monacoModule.languages.CompletionItemInsertTextRule.InsertAsSnippet;
|
||||
|
||||
monacoModule.languages.registerCompletionItemProvider('html', {
|
||||
triggerCharacters: ['.'],
|
||||
provideCompletionItems(model, position) {
|
||||
const before = model.getValueInRange({
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column
|
||||
});
|
||||
if (!rbInScript(before)) {
|
||||
return { suggestions: [] };
|
||||
}
|
||||
|
||||
const lineBefore = before.slice(before.lastIndexOf('\n') + 1);
|
||||
const word = model.getWordUntilPosition(position);
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: word.startColumn,
|
||||
endColumn: word.endColumn
|
||||
};
|
||||
|
||||
// member access on rb / window.rb / remoteBrowser
|
||||
if (/(?:^|[^\w$.])(?:window\s*\.\s*)?(?:rb|remoteBrowser)\s*\.\s*[\w$]*$/.test(lineBefore)) {
|
||||
return {
|
||||
suggestions: RB_MEMBERS.map((m) => ({
|
||||
label: m.name,
|
||||
kind: Kind.Method,
|
||||
insertText: m.insertText,
|
||||
insertTextRules: AsSnippet,
|
||||
range,
|
||||
detail: m.detail,
|
||||
documentation: { value: m.doc }
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
// bare identifier: offer the rb object, but not right after another dot
|
||||
const uptoWord = lineBefore.slice(0, lineBefore.length - word.word.length);
|
||||
if (/\.\s*$/.test(uptoWord)) {
|
||||
return { suggestions: [] };
|
||||
}
|
||||
return {
|
||||
suggestions: [
|
||||
{
|
||||
label: 'rb',
|
||||
kind: Kind.Variable,
|
||||
insertText: 'rb',
|
||||
range,
|
||||
detail: RB_OBJECT_DETAIL,
|
||||
documentation: { value: RB_OBJECT_DOC }
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
monacoModule.languages.registerHoverProvider('html', {
|
||||
provideHover(model, position) {
|
||||
const before = model.getValueInRange({
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column
|
||||
});
|
||||
if (!rbInScript(before)) return null;
|
||||
|
||||
const w = model.getWordAtPosition(position);
|
||||
if (!w) return null;
|
||||
const lineContent = model.getLineContent(position.lineNumber);
|
||||
const beforeWord = lineContent.slice(0, w.startColumn - 1);
|
||||
const range = new monacoModule.Range(
|
||||
position.lineNumber,
|
||||
w.startColumn,
|
||||
position.lineNumber,
|
||||
w.endColumn
|
||||
);
|
||||
|
||||
// rb / window.rb / remoteBrowser object itself
|
||||
if (w.word === 'rb' || w.word === 'remoteBrowser') {
|
||||
const endsWithDot = /\.\s*$/.test(beforeWord);
|
||||
const isWindowRb = /(?:^|[^\w$])window\s*\.\s*$/.test(beforeWord);
|
||||
if (endsWithDot && !isWindowRb) return null;
|
||||
return { range, contents: [{ value: RB_OBJECT_DOC }] };
|
||||
}
|
||||
|
||||
// a member accessed on rb
|
||||
const m = RB_MEMBERS.find((x) => x.name === w.word);
|
||||
if (
|
||||
m &&
|
||||
/(?:^|[^\w$.])(?:window\s*\.\s*)?(?:rb|remoteBrowser)\s*\.\s*$/.test(beforeWord)
|
||||
) {
|
||||
return {
|
||||
range,
|
||||
contents: [{ value: '```js\nrb.' + w.word + m.detail + '\n```' }, { value: m.doc }]
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { onMount, tick } from 'svelte';
|
||||
import * as monaco from 'monaco-editor';
|
||||
@@ -462,6 +614,10 @@
|
||||
return new editorWorker();
|
||||
}
|
||||
};
|
||||
// offer rb client completions in the phishing page editor
|
||||
if (contentType === 'page') {
|
||||
ensureRbLanguageFeatures();
|
||||
}
|
||||
const editorOptions = {
|
||||
value: value,
|
||||
language: 'html',
|
||||
|
||||
@@ -440,10 +440,17 @@ interface Session {
|
||||
/** Evaluate a JS expression in the page context and return the result */
|
||||
evaluate(expression: string): any;
|
||||
|
||||
// ── Screenshots ───────────────────────────────────────────────────────────
|
||||
/** Take a full-page screenshot, visible in the test runner log */
|
||||
// ── Screenshots (test/debug only) ─────────────────────────────────────────
|
||||
/** Test/debug only. Full page screenshot shown in the editor runner panel.
|
||||
* Discarded in live campaign sessions and never saved as a recipient event. */
|
||||
screenshot(name: string): void;
|
||||
/** Test/debug only. Screenshot of one element, shown in the editor runner
|
||||
* panel. Discarded in live campaign sessions. */
|
||||
screenshotElement(selector: string, name: string): void;
|
||||
/** Test/debug only. Captures the full page HTML and emits it as a named
|
||||
* dom_dump event in the editor runner panel. Discarded in live campaign
|
||||
* sessions and never saved as a recipient event. */
|
||||
domDump(name: string): void;
|
||||
|
||||
// ── Viewport & emulation ─────────────────────────────────────────────────
|
||||
setViewport(width: number, height: number): void;
|
||||
@@ -592,10 +599,16 @@ interface FrameSession {
|
||||
// ── JavaScript evaluation ─────────────────────────────────────────────────
|
||||
evaluate(expression: string): any;
|
||||
|
||||
// ── Screenshots & DOM capture ────────────────────────────────────────────
|
||||
// ── Screenshots & DOM capture (test/debug only) ───────────────────────────
|
||||
/** Test/debug only. Full page screenshot shown in the editor runner panel.
|
||||
* Discarded in live campaign sessions and never saved as a recipient event. */
|
||||
screenshot(name: string): void;
|
||||
/** Test/debug only. Screenshot of one element, shown in the editor runner
|
||||
* panel. Discarded in live campaign sessions. */
|
||||
screenshotElement(selector: string, name: string): void;
|
||||
/** Capture the full page HTML and emit it as a named dom_dump event for debugging */
|
||||
/** Test/debug only. Captures the full page HTML and emits it as a named
|
||||
* dom_dump event in the editor runner panel. Discarded in live campaign
|
||||
* sessions and never saved as a recipient event. */
|
||||
domDump(name: string): void;
|
||||
|
||||
// ── Viewport & emulation ─────────────────────────────────────────────────
|
||||
@@ -1439,6 +1452,7 @@ declare var Infinity: number;
|
||||
controlMode={streamControlMode}
|
||||
{runLog}
|
||||
{isRunning}
|
||||
showLog={true}
|
||||
on:inject={(e) => {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
const { event, data } = e.detail;
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
export let runLog = [];
|
||||
/** @type {boolean} Whether the script is currently running */
|
||||
export let isRunning = false;
|
||||
/** @type {boolean} Show the script log overlay. Only the editor test runner
|
||||
* feeds runLog; the live campaign view has no script log to show. */
|
||||
export let showLog = false;
|
||||
|
||||
let canvas;
|
||||
let ws = null;
|
||||
@@ -473,22 +476,24 @@
|
||||
{#if status === 'Connected'}
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">{fps} fps</span>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (logPanelOpen = !logPanelOpen)}
|
||||
class="ml-auto flex items-center gap-1.5 px-2 py-0.5 text-xs rounded border transition-colors {logPanelOpen
|
||||
? 'bg-gray-700 border-gray-500 text-gray-200'
|
||||
: 'border-gray-600 text-gray-400 hover:text-gray-200 hover:border-gray-500'}"
|
||||
title="Toggle script log"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3 h-3">
|
||||
<path fill-rule="evenodd" d="M2 4a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1ZM2 8a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1ZM3 11a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H3Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Logs
|
||||
{#if isRunning}
|
||||
<span class="inline-block w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse"></span>
|
||||
{/if}
|
||||
</button>
|
||||
{#if showLog}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (logPanelOpen = !logPanelOpen)}
|
||||
class="ml-auto flex items-center gap-1.5 px-2 py-0.5 text-xs rounded border transition-colors {logPanelOpen
|
||||
? 'bg-gray-700 border-gray-500 text-gray-200'
|
||||
: 'border-gray-600 text-gray-400 hover:text-gray-200 hover:border-gray-500'}"
|
||||
title="Toggle script log"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-3 h-3">
|
||||
<path fill-rule="evenodd" d="M2 4a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1ZM2 8a1 1 0 0 1 1-1h10a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1ZM3 11a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H3Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Logs
|
||||
{#if isRunning}
|
||||
<span class="inline-block w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse"></span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Tab bar (only shown when multiple tabs exist) -->
|
||||
{#if tabs.length > 1}
|
||||
@@ -607,7 +612,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if logPanelOpen}
|
||||
{#if showLog && logPanelOpen}
|
||||
<div
|
||||
class="absolute bottom-0 left-0 right-0 flex flex-col bg-gray-950/95 border-t border-gray-700 rounded-b"
|
||||
style="height: 13rem; max-height: 50%;"
|
||||
|
||||
Reference in New Issue
Block a user