bbpro
Version:
BrowserBox - remote browser isolation tool
421 lines (381 loc) • 12.1 kB
JavaScript
class BBModal extends Base {
RESPONDABLE_MODALS = new Set([
'alert',
'confirm',
'prompt',
'paste',
'beforeunload',
'filechooser',
'infobox',
'auth',
'intentPrompt',
'settings'
]);
constructor() {
super();
this.unloadHandler = this.requestModalBeClosedFirst.bind(this);
this.prepareState();
}
prepareState(currentModal = {}) {
const state = this.state;
//super.prepareState();
// these are default values when there is no current Modal
let msg = '';
let type = '';
let title = '';
let currentModalEl = false;
let highlight = undefined;
let csrfToken = '';
let requestId = '';
let sessionId = '';
let mode = '';
let accept = '';
let multiple = false;
let submitText = '';
let cancelText = '';
let otherButton = null;
let link = null;
let working = false;
let url = '';
if ( currentModal ) {
// the defaults here are defaults when there *is* a current modal
({
msg:msg = 'Empty',
type,
highlight: highlight = false,
csrfToken:csrfToken = '',
url:url = '',
title:title = 'Untitled',
el:currentModalEl,
requestId:requestId = '',
mode:mode = '',
sessionId:sessionId = '',
accept: accept = '',
submitText:submitText = 'Submit',
cancelText:cancelText = 'Cancel',
otherButton:otherButton = null,
link:link = null,
working:working = false,
} = currentModal);
window.addEventListener('beforeunload', this.unloadHandler);
} else {
window.removeEventListener('beforeunload', this.unloadHandler);
}
if ( type == 'intentPrompt' ) {
if ( ! url ) {
throw new TypeError(`IntentPrompt modal requires a url`);
} else {
const Url = new URL(url);
if ( Url.protocol == 'intent:' ) {
if ( ( Url + '').includes('google.com/maps') ) {
Url.protocol = 'https:';
}
url = Url;
}
}
}
if ( type == 'auth' && ! requestId ) {
throw new TypeError(`Auth modal requires a requestId to send the response to`);
}
if ( type == 'filechooser' && !(mode && sessionId && csrfToken) ) {
DEBUG.debugModal && console.log(currentModal);
throw new TypeError(`File chooser modal requires all of: sessionId, mode and csrfToken`);
}
// we are getting this now via async fetch inside the HTML template ahahah
/*
if ( type == 'settings' && !csrfToken ) {
DEBUG.debugModal && console.log(currentModal);
throw new TypeError(`Settings modal requires a csrfToken`);
}
*/
if ( mode == 'selectMultiple' ) {
multiple = true;
}
currentModal = {
type, csrfToken, mode, requestId, msg,
highlight,
el: state.viewState.ModalRef[type],
sessionId, otherButton, title, url, multiple, accept,
link,
working, submitText, cancelText,
};
DEBUG.debugModal && console.log('after prepare state', {currentModal}, '(also "others" state mixin)');
this.others = currentModal;
}
async openModal({modal} = {}) {
if ( this.opening ) return;
this.opening = true;
const state = this.state;
const {ModalRef} = state.viewState;
const {
sessionId, mode, requestId, title, type, message:msg, defaultPrompt, url,
link,
highlight,
csrfToken,
} = modal;
let {
otherButton,
} = modal;
if ( !ModalRef[type] ) {
DEBUG.debugModal && console.warn(`Waiting until modal type ${type} has its element loaded...`);
}
await state._top.untilTrue(() => !!ModalRef[type], 100, 1000);
const currentModal = {
type, csrfToken, mode,
highlight,
requestId,
msg,
el:ModalRef[type],
sessionId,
otherButton,
link,
title, url
};
state.viewState.currentModal = currentModal;
this.prepareState(currentModal);
localStorage.setItem('lastModal', JSON.stringify(modal));
DEBUG.debugModal && console.log(state.viewState.currentModal);
const modalDebug = {
defaultPrompt, url, highlight, currentModal, ModalRef, state, title, type, otherButton, csrfToken,
link,
};
(DEBUG.debugModal || (DEBUG.val >= DEBUG.med)) && Object.assign(self, {modalDebug});
(DEBUG.debugModal || (DEBUG.val >= DEBUG.med)) && console.log(`Will display modal ${type} with ${msg} on el:`, state.viewState.currentModal.el);
this.state = state;
setTimeout(async () => {
if ( type == 'copy' ) {
await state._top.untilTrue(() => this.copyBoxTextarea.value == msg, 300, 20);
this.copyBoxTextarea.select();
let secondTitle = '';
try {
await navigator.clipboard.writeText(this.copyBoxTextarea.value);
DEBUG.debugClipboard && console.info(`Copied to clipboard`);
secondTitle = ' - Copied to Clipboard!';
} catch(e) {
DEBUG.debugClipboard && console.warn(`Could not copy to clipboard`, title);
this.latestCopyValue = this.copyBoxTextarea.value;
otherButton = `<button onclick="copyToClipboard">Copy</button>`
}
const currentModal = {
type, csrfToken, mode,
highlight,
requestId,
msg,
el:ModalRef[type],
sessionId,
otherButton,
link,
title: title + secondTitle,
url
};
state.viewState.currentModal = currentModal;
this.prepareState(currentModal);
this.state = state;
// weird hack don't know why we need this
this.copyBoxTitle.innerText = title + secondTitle;
}
}, 0);
}
copyToClipboard(event) {
// stop the modal from being closed by clicking this
event.stopPropagation();
// try to copy to clipboard
navigator.clipboard.writeText(this.latestCopyValue).then(
() => this.copyBoxTitle.innerText = 'Copied to Clipboard!'
).catch(
() => this.copyBoxTitle.innerText = 'Copy failed. Please manually copy.'
);
}
closeModal(click) {
const state = this.state;
if ( ! click.target.matches('button') || ! state.viewState.currentModal ) return;
const response = click.target.value || 'close';
const data = click.target.closest('form')?.response?.value || '';
const {sessionId, type} = state.viewState.currentModal;
state.viewState.lastModal = state.viewState.currentModal;
state.viewState.lastModal.modalResponse = response;
const {type:modalType} = state.viewState.lastModal;
if ( this.RESPONDABLE_MODALS.has(modalType) ) {
const modalResponse = {
synthetic: true,
modalType,
type: "respond-to-modal",
response,
sessionId,
[modalType === 'prompt' ? 'promptText': 'data']: data
};
DEBUG.debugModal && console.log({modalResponse});
state.H(modalResponse);
}
this.onlyCloseModal();
}
onlyCloseModal() {
const {state} = this;
state.viewState.currentModal = null;
this.opening = false;
setTimeout(() => this.state = state, 50);
}
respondWithAuth(click) {
const {state} = this;
click.preventDefault();
click.stopPropagation();
const form = click.target.closest('form');
const data = new FormData(form);
const requestId = data.get('requestid').slice(0,140);
const username = data.get('username').slice(0,140);
const password = data.get('password').slice(0,140);
const authResponse = {
username,
password,
response: "ProvideCredentials"
};
state.H({
synthetic: true,
type: 'auth-response',
requestId,
authResponse
});
DEBUG.debugAuth && console.log({authResponse});
this.closeModal(click);
}
sendPaste(click) {
const {state} = this;
click.preventDefault();
click.stopPropagation();
const modals = this;
const form = click.target.closest('form');
const data = new FormData(form);
const pasteText = data.get('response').slice(0,32000);
sendPasteData(pasteText);
function sendPasteData(pasteData) {
const input = state.viewState.shouldHaveFocus;
if ( ! input ) return;
const value = input.value;
const newValue = value.slice(0, input.selectionStart) + pasteData + value.slice(input.selectionEnd);
input.value = newValue;
input.selectionStart = input.selectionEnd;
if ( document.deepActiveElement !== input ) {
input.focus();
}
state.H({
type: 'typing',
data: pasteData,
synthetic: true,
event: {paste: true, simulated: true}
});
setTimeout(() => {
state.checkResults();
}, 300);
modals.closeModal(click);
}
}
respondWithCancel(click) {
const {state} = this;
click.preventDefault();
click.stopPropagation();
const form = click.target.closest('form');
const data = new FormData(form);
const requestId = data.get('requestid').slice(0,140);
const authResponse = {
response: "CancelAuth"
};
state.H({
synthetic: true,
type: 'auth-response',
modalType: 'auth',
requestId,
authResponse
});
this.closeModal(click);
}
async chooseFile(click) {
const {state} = this;
click.preventDefault();
click.stopPropagation();
const form = click.target.closest('form');
const body = new FormData(form);
const request = {
method: form.method,
body
};
Object.assign(state.viewState.currentModal, {
submitText: 'Uploading...',
working: true
});
this.prepareState(state.viewState.currentModal);
this.state = state;
try {
const resp = await fetch(form.action, request).then(r => r.json());
if ( resp.error ) {
alert(resp.error);
} else {
DEBUG.val && console.log(`Success attached files`, resp);
DEBUG.val && console.log({resp});
}
} catch(e) {
console.warn("Error on file upload", e);
}
this.onlyCloseModal(click);
}
async cancelFileChooser(click) {
const {state} = this;
click.preventDefault();
click.stopPropagation();
const form = click.target.closest('form');
form.reset();
const body = new FormData(form);
body.delete('files');
const request = {
method: form.method,
body
};
Object.assign(state.viewState.currentModal, {
cancelText: 'Canceling...',
working: true
});
this.prepareState(state.viewState.currentModal);
this.state = state;
const resp = await fetch(form.action, request).then(r => r.json());
if ( resp.error ) {
alert(`An error occurred`);
console.log({resp});
} else {
DEBUG.val && console.log(`Success cancelling file attachment`, resp);
console.log({resp});
}
this.onlyCloseModal(click);
}
requestModalBeClosedFirst(unload) {
const {state} = this;
if ( state.viewState.currentModal ) {
const message = "Please close the modal first";
const obj = (unload || window.event);
obj?.preventDefault?.();
if ( obj ) obj.returnValue = message;
return message;
}
}
oldsetup() {
// Auxilliary view functions
const ModalRef = {
alert: null, confirm: null, prompt: null, beforeunload: null,
infobox: null, notice: null, auth: null, filechooser: null,
paste: null,
intentPrompt: null,
settings: null,
};
// Permission request
function PermissionRequest({
permission, request, page
}) {
return R`
<article class="permission-request hidden">
<h1>${permission}</h1>
<p class=request>${page} is requesting ${permission} permission. The details are: ${request}</p>
<button class=grant>Grant</button>
<button class=deny>Deny</button>
</article>
`;
}
}
}