remoteview
Version:
Remote isolated browser platform by Dosyago
769 lines (756 loc) • 19.8 kB
JavaScript
import keys from './kbd.js';
export const FRAME_CONTROL = false;
export const WorldName = 'PlanetZanj';
const SHORT_TIMEOUT = 1000;
const MIN_DELTA = 40;
const MIN_PIX_DELTA = 8;
const THRESHOLD_DELTA = 1;
const DOM_DELTA_PIXEL = 0;
const DOM_DELTA_LINE = 1;
const DOM_DELTA_PAGE = 2;
const LINE_HEIGHT_GUESS = 32;
const SYNTHETIC_CTRL = e => keyEvent({key:'Control',originalType:e.originalType}, 2, true);
export default translator;
function translator(e, handled = {type:'case'}) {
handled.type = handled.type || 'case';
switch(e.type) {
case "touchcancel": {
return {
command : {
name: "Input.dispatchTouchEvent",
params: {
type: "touchCancel"
},
}
};
}
case "mousedown": case "mouseup": case "mousemove":
case "pointerdown": case "pointerup": case "pointermove": {
let button = "none";
if ( ! e.type.endsWith("move") ) {
if ( e.button == 0 ) {
button = "left";
} else {
button = "right";
}
}
return {
command : {
name: "Input.emulateTouchFromMouseEvent",
params: {
x: Math.round(e.bitmapX),
y: Math.round(e.bitmapY),
type: e.type.endsWith("down") ? "mousePressed" :
e.type.endsWith("up") ? "mouseReleased" : "mouseMoved",
button,
clickCount: !e.type.endsWith("move") ? 1 : 0,
modifiers: encodeModifiers(e.originalEvent)
},
requiresShot: ! e.originalEvent.noShot && e.type.endsWith("down")
}
};
}
case "wheel": {
// if we use emulateTouchFromMouseEvent we need a button value
const deltaMode = e.originalEvent.deltaMode;
const deltaX = adjustWheelDeltaByMode(e.originalEvent.deltaX, deltaMode);
const deltaY = adjustWheelDeltaByMode(e.originalEvent.deltaY, deltaMode);
const {contextId} = e;
const clientX = 0;
const clientY = 0
const deltas = {deltaX,deltaY,clientX,clientY};
let retVal;
if ( deltaX > MIN_DELTA || deltaY > MIN_DELTA ) {
const retVal1 = {
command: {
name: "Runtime.evaluate",
params: {
expression: `self.ensureScroll(${JSON.stringify(deltas)});`,
includeCommandLineAPI: false,
userGesture: true,
contextId,
timeout: SHORT_TIMEOUT
}
}
};
const retVal2 = mouseEvent(e, deltaX, deltaY);
retVal = [retVal1,retVal2];
} else {
retVal = mouseEvent(e, deltaX, deltaY);
}
return retVal;
}
case "auth-response": {
const {requestId, authResponse} = e;
return {
command: {
name: "Fetch.continueWithAuth",
params: {
requestId,
authChallengeResponse: authResponse
}
}
};
}
case "resample-imagery": {
const {down, up, averageBw} = e;
return {
command: {
isZombieLordCommand: true,
name: "Connection.resampleImagery",
params: {
averageBw, down , up
}
}
}
}
case "control-chars": {
return keyEvent(e);
}
case "keydown":
if ( !e.key || ! e.code ) {
return;
} else if ( e.key == "Unidentified" || e.code == "Unidentified" ) {
return;
} else {
return keyEvent(e);
}
case "keyup":
if ( !e.key || ! e.code ) {
return;
} else if ( e.key == "Unidentified" || e.code == "Unidentified" ) {
return;
} else {
return keyEvent(e);
}
case "keypress": {
if ( e.code == "Unidentified" ) {
if( e.key.length ) {
const text = e.key;
return {
command: {
name: "Input.insertText",
params: {
text
},
requiresShot: true,
ignoreHash: true
}
}
} else return;
} else if ( e.key == "Unidentified" ) {
if( e.code.length ) {
const text = e.code;
return {
command: {
name: "Input.insertText",
params: {
text
},
requiresShot: true,
ignoreHash: true
}
}
} else return;
} else return keyEvent(e);
};
case "typing": {
if ( e.isComposing || ! e.characters ) return;
else return {
command: {
name: "Input.insertText",
params: {
text: (e.characters || '')
},
requiresShot: true,
ignoreHash: true
}
}
}
case "typing-syncValue": {
return {
command: {
name: "Runtime.evaluate",
params: {
expression: `syncFocusedInputToValue("${e.encodedValue}");`,
includeCommandLineAPI: false,
userGesture: true,
contextId: e.contextId,
timeout: SHORT_TIMEOUT,
},
requiresShot: true,
ignoreHash: true
}
}
}
case "typing-deleteContentBackward": {
if ( ! e.encodedValueToDelete ) return;
else return {
command: {
name: "Runtime.evaluate",
params: {
expression: `fromFocusedInputDeleteLastOccurrenceOf("${e.encodedValueToDelete}");`,
includeCommandLineAPI: false,
userGesture: true,
contextId: e.contextId,
timeout: SHORT_TIMEOUT
},
requiresShot: true
}
}
}
case "url-address": {
return {
command: {
name: "Page.navigate",
params: {
url: e.address
},
requiresLoad: true,
requiresShot: true,
requiresTailShot: true
}
}
}
case "setDocument": {
const {frameId,sessionId,html} = e;
if ( frameId ) {
return {
command: {
name: "Page.setDocumentContent",
params: {
html, frameId, sessionId
},
requiresShot: true,
}
};
} else {
return {chain:[
{
command: {
name: "Page.getFrameTree",
params: {}
}
},
({frameTree:{frame:{id:frameId}}}) => {
return {
command: {
name: "Page.setDocumentContent",
params: {
html, frameId
},
requiresShot: true,
}
};
}
]};
}
}
case "history": {
switch(e.action) {
case "reload": case "stop": {
return {
command: {
requiresLoad: e.action == "reload",
requiresShot: e.action == "reload",
name: e.action == "reload" ? "Page.reload" : "Page.stopLoading",
params: {},
}
};
}
case "back": case "forward": {
return {chain:[
{
command: {
name: "Page.getNavigationHistory",
params: {}
}
},
({currentIndex, entries}) => {
const intendedEntry = entries[currentIndex + (e.action == "back" ? -1 : +1 )];
if ( intendedEntry ) {
return {
command: {
name: "Page.navigateToHistoryEntry",
params: {
entryId: intendedEntry.id
},
requiresLoad: true,
requiresShot: true,
requiresTailShot: true
}
};
}
}
]};
}
default: {
throw new TypeError(`Unkown history action ${e.action}`);
}
}
}
case "touchscroll": {
let {deltaX,deltaY,bitmapX:clientX,bitmapY:clientY,contextId} = e;
// only one scroll direction at a time
if ( Math.abs(deltaY) > Math.abs(deltaX) ) {
deltaX = 0;
if ( Math.abs(deltaY) > 0.2 * self.ViewportHeight ) {
deltaY = Math.round(5.718 * deltaY);
}
} else {
deltaY = 0;
if ( Math.abs(deltaX) > 0.3 * self.ViewportWidth ) {
deltaX = Math.round(5.718 * deltaX);
}
}
clientX = Math.round(clientX);
clientY = Math.round(clientY);
const deltas = {deltaX,deltaY,clientX,clientY};
const retVal1 = {
command: {
name: "Runtime.evaluate",
params: {
expression: `self.ensureScroll(${JSON.stringify(deltas)});`,
includeCommandLineAPI: false,
userGesture: true,
contextId,
timeout: SHORT_TIMEOUT
}
}
};
const retVal2 = mouseEvent(e, deltaX, deltaY);
const retVal = [retVal1,retVal2];
return retVal;
}
case "zoom": {
/** retval does not work. Expanding pinch is OK, but contracting seems to fail **/
/*
const retVal = {
command: {
name: "Input.synthesizePinchGesture",
params: {
relativeSpeed: 300,
scaleFactor: e.scale,
gestureSourceType: "touch",
x: Math.round(e.bitmapX),
y: Math.round(e.bitmapY)
},
requiresShot: true,
requiresExtraWait: true,
extraWait: 300
}
};
*/
/** so we are using emulation and multiplying the scale factor in the event listener **/
const retVal2 = {
command: {
name: "Emulation.setPageScaleFactor",
params: {
pageScaleFactor: e.scale
},
requiresShot: true,
requiresExtraWait: true,
extraWait: 300
}
}
return retVal2;
}
case "select": {
const retVal = {
command : {
name: "Runtime.evaluate",
params: {
expression: `self.setSelectValue("${e.value}");`,
includeCommandLineAPI: false,
userGesture: true,
contextId: e.executionContext,
timeout: SHORT_TIMEOUT
},
requiresShot: true,
requiresExtraWait: true,
extraWait: 300
}
};
return retVal;
}
case "window-bounds": {
let {width,height,targetId} = e;
width = parseInt(width);
height = parseInt(height);
const retVal = {chain:[
{
command: {
name: "Browser.getWindowForTarget",
params: {targetId},
}
},
({windowId, bounds}) => {
if ( bounds.width == width && bounds.height == height ) return;
const retVal = {
command: {
name: "Browser.setWindowBounds",
params: {
windowId,
bounds: {width, height}
},
requiresWindowId: true
}
};
return retVal;
}
]};
return retVal;
}
case "window-bounds-preImplementation": {
let {width,height,mobile} = e;
width = parseInt(width);
height = parseInt(height);
const retVal = {
command: {
name: "Emulation.setDeviceMetricsOverride",
params: {
width,
height,
mobile,
deviceScaleFactor:1,
screenOrientation: {
angle: 90,
type: 'landscapePrimary'
}
}
},
requiresShot: true,
};
return retVal;
}
case "user-agent": {
const {userAgent, platform, acceptLanguage} = e;
const retVal = {
command: {
name: "Network.setUserAgentOverride",
params: {
userAgent, platform,
acceptLanguage,
}
}
}
return retVal;
}
case "hide-scrollbars": {
const retVal = {
command: {
name: "Emulation.setScrollbarsHidden",
params: {
hidden: true
}
}
}
return retVal;
}
case "buffered-results-collection": {
return e;
}
case "doShot": {
return {
command: {
isZombieLordCommand: true,
name: "Connection.doShot",
params: {}
}
};
}
case "canKeysInput": {
return {chain:[
{
command: {
isZombieLordCommand: true,
name: "Connection.getContextIdsForActiveSession",
params: {
worldName: WorldName
}
}
},
({contextIds}) => contextIds.map(contextId => ({
command: {
name: "Runtime.evaluate",
params: {
expression: "canKeysInput();",
contextId: contextId,
timeout: SHORT_TIMEOUT
},
}
}))
]};
}
case "describeNode": {
const {backendNodeId} = e;
return {
command: {
name: "DOM.describeNode",
params: {
backendNodeId
}
}
};
}
case "getElementInfo": {
return {chain:[
{
command: {
isZombieLordCommand: true,
name: "Connection.getContextIdsForActiveSession",
params: {
worldName: WorldName
}
}
},
({contextIds}) => contextIds.map(contextId => ({
command: {
name: "Runtime.evaluate",
params: {
expression: `getElementInfo(${JSON.stringify(e.data)});`,
contextId: contextId,
timeout: SHORT_TIMEOUT
},
}
}))
]};
}
case "getFavicon": {
return {chain:[
{
command: {
isZombieLordCommand: true,
name: "Connection.getAllContextIds",
params: {
worldName: WorldName
}
}
},
({sessionContextIdPairs}) => sessionContextIdPairs.map(({sessionId,contextId}) => {
return {
command: {
name: "Runtime.evaluate",
params: {
sessionId,
contextId,
expression: "getFaviconElement();",
timeout: SHORT_TIMEOUT
},
}
};
})
]};
}
case "newIncognitoTab": {
return {chain:[
{
command: {
name: "Target.createBrowserContext",
params: {}
}
},
({browserContextId}) => {
return {
command: {
name: "Target.createTarget",
params: {
browserContextId,
url: "about:blank",
enableBeginFrameControl: FRAME_CONTROL
},
}
};
}
]};
}
case "isMobile": {
return {
command: {
isZombieLordCommand: true,
name: "Connection.setIsMobile",
params: {}
}
};
}
case "isSafari": {
return {
command: {
isZombieLordCommand: true,
name: "Connection.setIsSafari",
params: {}
}
};
}
case "isFirefox": {
return {
command: {
isZombieLordCommand: true,
name: "Connection.setIsFirefox",
params: {}
}
};
}
case "clearAllPageHistory": {
return {chain:[
{
command: {
isZombieLordCommand: true,
name: "Connection.getAllSessionIds",
params: {}
}
},
({sessionIds}) => sessionIds.map(sessionId => {
return {
command: {
name: "Page.resetNavigationHistory",
params: {
sessionId,
},
}
};
})
]};
}
case "clearCache": {
return {
command: {
name: "Network.clearBrowserCache",
params: {}
}
};
}
case "clearCookies": {
return {
command: {
name: "Network.clearBrowserCookies",
params: {}
}
};
}
case "respond-to-modal": {
let accept = false;
let {response, sessionId, promptText} = e;
if ( response == "ok" ) {
accept = true;
}
return {
command: {
name: "Page.handleJavaScriptDialog",
params: {
accept, promptText, sessionId
}
}
};
}
default: {
if ( ( !!e.command && !!e.command.name ) || Array.isArray(e) ) {
handled.type = 'default';
return e;
} else {
handled.type = 'unhandled';
return;
}
}
}
}
function mouseEvent(e, deltaX = 0, deltaY = 0) {
return {
command : {
name: "Input.dispatchMouseEvent",
params: {
x: Math.round(e.bitmapX),
y: Math.round(e.bitmapY),
type: "mouseWheel",
deltaX, deltaY
},
requiresShot: true,
}
};
}
function keyEvent(e, modifiers = 0, SYNTHETIC = false) {
const id = e.key && e.key.length > 1 ? e.key : e.code;
const def = keys[id];
const text = e.originalType == "keypress" ? String.fromCharCode(e.keyCode) : undefined;
modifiers = modifiers || encodeModifiers(e.originalEvent);
let type;
if ( e.originalType == "keydown" ) {
if ( text )
type = "keyDown";
else
type = "rawKeyDown";
} else if ( e.originalType == "keypress" ) {
type = "char";
} else {
type = "keyUp";
}
const retVal = {
command: {
name: "Input.dispatchKeyEvent",
params: {
type,
text,
unmodifiedText: text,
code: def.code,
key: def.key,
windowsVirtualKeyCode: e.keyCode,
modifiers,
},
requiresShot: e.key == "Enter" || e.key == "Tab" || e.key == "Delete",
ignoreHash: e.key == "Enter" || e.key == "Tab" || e.key == "Delete"
}
};
if ( ! SYNTHETIC && retVal.command.params.key == 'Meta' ) {
return [
retVal,
SYNTHETIC_CTRL(e)
];
}
return retVal;
}
function encodeModifiers(originalEvent) {
let modifiers = 0;
if (originalEvent.altKey ) {
modifiers += 1;
}
if (originalEvent.ctrlKey || originalEvent.metaKey) {
modifiers += 2;
}
if (originalEvent.metaKey ) {
modifiers += 4;
}
if (originalEvent.shiftKey ) {
modifiers += 8;
}
return modifiers;
}
function adjustWheelDeltaByMode(delta, mode) {
if ( delta == 0 ) return delta;
let threshold = Math.abs(delta) > THRESHOLD_DELTA;
if ( ! threshold ) {
delta = Math.sqrt(Math.abs(delta))*Math.sign(delta);
}
switch(mode) {
case DOM_DELTA_PIXEL:
//console.log("pix mode", delta);
if ( threshold && Math.abs(delta) < MIN_PIX_DELTA ) {
delta = Math.sign(delta)*MIN_PIX_DELTA;
}
break;
case DOM_DELTA_LINE:
//console.log("line mode", delta);
delta = delta * LINE_HEIGHT_GUESS;
if ( threshold && Math.abs(delta) < MIN_DELTA ) {
delta = Math.sign(delta)*MIN_DELTA;
}
break;
case DOM_DELTA_PAGE:
//console.log("page mode", delta);
delta = delta * self.ViewportHeight;
if ( threshold && Math.abs(delta) < MIN_DELTA ) {
delta = Math.sign(delta)*MIN_DELTA;
}
break;
}
return delta;
}