bbpro
Version:
BrowserBox - remote browser isolation tool
497 lines (462 loc) • 14.8 kB
JavaScript
class BBContextMenu extends Base {
static CLOSE_DELAY = 222;
static SHORT_CUT = 'Ctrl+Shift+J';
CTX_MENU_THRESHOLD = 375;
MENU_ITEMS(state) {
const {_top} = state;
return {
'page': [
...(_top.deviceIsMobile() ? this.MobileOptions(_top) : []),
{
title: 'Open Link in New Tab',
shortCut: this.constructor.SHORT_CUT,
func: this.openInNewTab,
},
// This is blocked (apparently) on: https://bugs.chromium.org/p/chromium/issues/detail?id=1015260
{
title: 'Open Link in Incognito Tab',
shortCut: this.constructor.SHORT_CUT,
func: this.newBrowserContextAndTab,
},
{
hr: true,
title: 'Resize Screen',
shortCut: this.constructor.SHORT_CUT,
func: this.resizeViewport
},
{
title: (
document.fullscreenElement ||
document.webkitFullscreenElement
) ?
'Exit Full Screen' :
'Full Screen'
,
shortCut: this.constructor.SHORT_CUT,
func: this.fullScreen,
},
{
title: 'Save Screenshot',
shortCut: this.constructor.SHORT_CUT,
func: this.download
},
{
title: 'Toggle Controls',
shortCut: this.constructor.SHORT_CUT,
func: this.toggleChromeUI
},
{
title: 'Copy Text from Here',
shortCut: this.constructor.SHORT_CUT,
func: this.copy,
hr: true
},
{
title: 'Copy Link Address from Here',
shortCut: this.constructor.SHORT_CUT,
func: this.copyLink,
},
{
title: 'Paste Text',
shortCut: this.constructor.SHORT_CUT,
func: this.paste
},
{
hr: true,
title: 'Reload',
shortCut: this.constructor.SHORT_CUT,
func: this.reload
},
{
title: 'Clear History',
shortCut: this.constructor.SHORT_CUT,
func: this.clearHistoryAndCacheLeaveCookies,
},
{
title: 'Wipe All Data',
shortCut: this.constructor.SHORT_CUT,
func: this.clearBrowsingData,
},
{
title: 'Inspect in DevTools',
hr: true,
shortCut: this.constructor.SHORT_CUT,
func: this.inspectCurrentTabInDevTools,
},
],
};
}
MobileOptions(state) {
return (DEBUG.showKeyboardToggleInContextMenu ? [
{
title: state.viewState.shouldHaveFocus ? 'Hide keyboard' : 'Show keyboard',
shortCut: this.constructor.SHORT_CUT,
func: (click) => {
state.toggleVirtualKeyboard(click);
this.close(state);
}
},
] : []);
}
constructor() {
super();
}
// custom untilLoaded to ensure styles are loaded
// before running bond to ensure that the correct sizes are used
// in calculation of the menu position, otherwise the menu will
// be positioned based on how big it is before the styles are applied
async untilLoaded() {
await super.untilLoaded();
await becomesTrue(() => getComputedStyle(this?.shadowRoot?.querySelector('#load-test'))?.display === "contents");
return true;
}
get menuItems() {
return this.MENU_ITEMS(this.state._top).page;
}
/* core functions */
async inspectCurrentTabInDevTools(click) {
const state = this.state._top;
const currentTab = state.activeTarget;
DEBUG.debugInspect && console.log(`Opening DevTools window to inspect tab ${currentTab}`, state.active);
const devtoolsWindow = window.open("about:blank");
const url = state.CONFIG.isOnion ? new URL(
`${location.protocol}//${localStorage.getItem(state.CONFIG.devtoolsServiceFileName)}`
)
:
new URL(location)
;
url.port = state.CONFIG.isOnion ? 443 : parseInt(location.port) + 1;
url.pathname = "login";
const params = new URLSearchParams();
params.set('token', state.sessionToken);
url.search = params;
DEBUG.debugInspect && console.log("Login url", url.href);
// we don't use cookie auth with Tor as Tor browser will block this "3rd-party request"
if ( !state.CONFIG.isOnion ) {
try {
await fetch(url, {mode: 'no-cors', credentials: 'include'});
} catch(e) {
console.warn(`Issue when attempting to login via token for cookie to devtools service`);
}
}
const dtUrl = `${url.host}/devtools/page/${currentTab}`;
DEBUG.debugDevTools && console.log({dtUrl});
const inspectParams = new URLSearchParams();
inspectParams.set(
location.protocol.endsWith('https:') ? 'wss' : 'ws',
dtUrl
);
inspectParams.set('remoteFrontend', 'true');
url.search = inspectParams;
if ( state.CONFIG.isOnion ) {
const locationUrl = new URL(url);
locationUrl.pathname = `/devtools/inspector.html`
const nextUri = locationUrl.href;
url.pathname = '/login';
url.searchParams.set('token', localStorage.getItem(state.CONFIG.sessionTokenFileName));
url.searchParams.set('nextUri', nextUri);
devtoolsWindow.location = url;
} else {
url.pathname = `/devtools/inspector.html`
DEBUG.debugInspect && console.log("Inspect url", url.href);
devtoolsWindow.location = url;
}
}
close(_, delay = true) {
DEBUG.debugContextMenu && console.log((new Error(`Tracking close event`)).stack);
const state = this.state;
const {_top} = this.state;
if ( delay ) {
setTimeout(() => {
if ( _top.viewState.contextMenu ) {
_top.viewState.contextMenu = null;
_top.contextMenuActive = false;
//_top.contextMenuEvent = null;
this.state = state;
setState('bbpro', _top);
}
}, this.constructor.CLOSE_DELAY);
} else {
if ( _top.viewState.contextMenu ) {
_top.viewState.contextMenu = null;
_top.contextMenuActive = false;
//_top.contextMenuEvent = null;
}
}
this.state = state;
setState('bbpro', _top);
}
copy(click) {
DEBUG.debugCopyPaste && console.log(`Received copy request`);
state = this.state._top;
const contextClick = state.contextMenuEvent;
let pageX, pageY, clientX, clientY;
if ( contextClick?.detail?.pageX ) {
({pageX,pageY,clientX,clientY} = contextClick.detail);
} else {
({pageX,pageY,clientX,clientY} = contextClick);
}
DEBUG.debugCopyPaste && console.log({clientX,clientY});
const target = state.viewState.canvasEl;
const {H} = state;
this.close(state);
state.elementInfoContinuation = ({innerText, noSuchElement}) => {
if ( ! noSuchElement ) {
state.elementInfoContinuation = null;
state.viewState.modalComponent.openModal({modal:{
type:'copy',
highlight: true,
message: innerText,
title: `Text from Page`
}}, state);
}
};
DEBUG.debugCopyPaste && console.log({clientX,clientY});
H({
type: 'getElementInfo',
synthetic: true,
data: {
innerText: true,
target,
clientX, clientY
}
});
setTimeout(() => {
state.checkResults();
}, 300);
DEBUG.debugCopyPaste && console.log({clientX,clientY});
}
copyLink(click) {
state = this.state._top;
const contextClick = state.contextMenuEvent;
const target = state.viewState.canvasEl;
let pageX, pageY, clientX, clientY;
if ( contextClick?.detail?.pageX ) {
({pageX,pageY,clientX,clientY} = contextClick.detail);
} else {
({pageX,pageY,clientX,clientY} = contextClick);
}
const {H} = state;
this.close(state);
state.elementInfoContinuation = ({attributes, noSuchElement}) => {
if ( ! noSuchElement ) {
state.elementInfoContinuation = null;
state.viewState.modalComponent.openModal({modal:{
type: 'copy',
message: attributes.href,
highlight: true,
title: 'Link from Page'
}}, state);
}
};
H({
type: 'getElementInfo',
synthetic: true,
data: {
closest: 'a[href]',
attributes: ['href'],
target,
clientX, clientY
}
});
setTimeout(() => {
state.checkResults();
}, 300);
}
paste(click) {
state = this.state._top;
this.close(state);
state.viewState.modalComponent.openModal({modal:{
type:'paste',
message: "Enter text to paste",
title: `Paste into Page`
}}, state);
}
download(click) {
state = this.state._top;
this.close(state);
const timeNow = new Date();
const stringTime = timeNow.toJSON();
const fileName = stringTime.replace(/[-:.]/g, "_");
const imageData = state.viewState.canvasEl.toDataURL();
const downloader = document.createElement('a');
downloader.href = imageData;
Object.assign(downloader.style, {
position: 'absolute',
top: '0px',
left: '0px',
opacity: 0
});
downloader.download = `${fileName}.png`;
document.body.appendChild(downloader);
downloader.click();
downloader.remove();
}
reload(click) {
state = this.state._top;
const goButton = state?.viewState?.omniBoxInput?.closest('form.url')?.querySelector('button.go');
if ( goButton ) {
goButton.click();
} else {
state.go({
target: {
address: {
value: state.activeTab().url || BLANK
}
}
}, state);
}
this.close(state);
}
openInNewTab(click) {
state = this.state._top;
const contextClick = state.contextMenuEvent;
const target = state.viewState.canvasEl;
let pageX, pageY, clientX, clientY;
if ( contextClick?.detail?.pageX ) {
({pageX,pageY,clientX,clientY} = contextClick.detail);
} else if ( contextClick ) {
({pageX,pageY,clientX,clientY} = contextClick);
} else {
console.warn(`NO data for context menu event`);
}
const {H} = state;
// we need to get the URL of the target link
// then use
// state.createTab(click, url);
state.viewState.killNextMouseReleased = true;
state.elementInfoContinuation = ({attributes, noSuchElement}) => {
DEBUG.debugTabs && console.log(`Received continuation`);
if ( ! noSuchElement ) {
state.elementInfoContinuation = null;
state.createTab(click, attributes.href);
setTimeout(() => state.runUpdateTabs(), 0);
}
};
H({
type: 'getElementInfo',
synthetic: true,
data: {
closest: 'a[href]',
attributes: ['href'],
target,
clientX, clientY
}
});
setTimeout(() => {
state.checkResults();
}, 300);
DEBUG.debugTabs && console.log(`Check sent element info request`);
this.close(state);
}
newBrowserContextAndTab(click) {
state = this.state._top;
const {H} = state;
const contextClick = state.contextMenuEvent;
const target = state.viewState.canvasEl;
let pageX, pageY, clientX, clientY;
if ( contextClick?.detail?.pageX ) {
({pageX,pageY,clientX,clientY} = contextClick.detail);
} else {
({pageX,pageY,clientX,clientY} = contextClick);
}
state.viewState.killNextMouseReleased = true;
state.elementInfoContinuation = ({attributes, noSuchElement}) => {
DEBUG.debugTabs && console.log(`Received continuation`);
if ( ! noSuchElement ) {
state.elementInfoContinuation = null;
H({
synthetic: true,
immediate: true,
type: 'newIncognitoTab',
url: attributes.href
});
setTimeout(() => {
state.checkResults();
setTimeout(() => state.runUpdateTabs(), 300);
}, 300);
}
};
H({
type: 'getElementInfo',
synthetic: true,
data: {
closest: 'a[href]',
attributes: ['href'],
target,
clientX, clientY
}
});
setTimeout(() => {
state.checkResults();
}, 300);
this.close(state);
}
clearHistoryAndCacheLeaveCookies(click) {
state = this.state._top;
const doIt = confirm("You'll stay signed in to most sites, but wipe browsing history and cached files. Are you sure?");
if ( doIt ) {
const {H} = state;
H({
synthetic: true,
type: "clearAllPageHistory"
});
H({
synthetic: true,
type: "clearCache"
});
alert("Cleared all caches and history.");
}
this.close(state);
}
clearBrowsingData(click) {
state = this.state._top;
const doIt = confirm("This will sign you out of most sites, and wipe all history and caches. Really wipe everything?");
if ( doIt ) {
const {H} = state;
H({
synthetic: true,
type: "clearAllPageHistory"
});
H({
synthetic: true,
type: "clearCache"
});
H({
synthetic: true,
type: "clearCookies"
});
alert("Cleared all history, caches and cookies.");
}
this.close(state);
}
resizeViewport(click) {
state = this.state._top;
window._voodoo_asyncSizeTab({resetRequested:true})
this.close(state);
}
toggleChromeUI(click) {
state = this.state._top;
state.chromeUI ^= true;
state.hideIMEUI();
this.close(state);
window._voodoo_asyncSizeTab({resetRequested:true})
}
async fullScreen(click) {
state = this.state._top;
if ( document.fullscreenElement || document.webkitFullscreenElement ) {
if ( document.webkitCancelFullscreen ) {
document.webkitCancelFullscreen();
} else {
await document.exitFullscreen();
}
} else {
if ( document.body.webkitRequestFullscreen ) {
document.body.webkitRequestFullscreen({navigationUI:'hide'});
} else {
await document.body.requestFullscreen({navigationUI:'hide'});
}
}
this.resizeViewport(click);
}
}