super-pancake-automation
Version:
A lightweight DOM-based UI automation framework using Chrome DevTools Protocol
289 lines (245 loc) • 8 kB
JavaScript
// Secure execution utilities for Chrome DevTools Protocol operations
import { SuperPancakeError, SecurityError, validateSession } from './errors.js';
// Secure function execution with parameterized calls
export async function executeSecureFunction(session, nodeId, functionName, params = []) {
validateSession(session);
if (!nodeId) {
throw new SuperPancakeError('NodeId is required for function execution', 'INVALID_NODE_ID');
}
try {
const { object } = await session.send('DOM.resolveNode', { nodeId });
if (!object || !object.objectId) {
throw new SuperPancakeError('Failed to resolve node to object', 'NODE_RESOLUTION_FAILED');
}
return await session.send('Runtime.callFunctionOn', {
objectId: object.objectId,
functionDeclaration: getSecureFunction(functionName),
arguments: params.map(param => ({ value: param })),
returnByValue: true
});
} catch (error) {
throw new SuperPancakeError(
`Secure function execution failed: ${error.message}`,
'FUNCTION_EXECUTION_FAILED',
{ functionName, nodeId, params }
);
}
}
// Predefined secure functions (no string interpolation)
const SECURE_FUNCTIONS = {
click: 'function() { this.click(); }',
focus: 'function() { this.focus(); }',
setValue: `function(value) {
this.focus();
this.value = value;
this.dispatchEvent(new Event('input', { bubbles: true }));
}`,
getText: 'function() { return this.innerText || this.textContent; }',
getAttribute: 'function(attrName) { return this.getAttribute(attrName); }',
setAttribute: `function(attrName, value) {
this.setAttribute(attrName, value);
}`,
isVisible: `function() {
const style = window.getComputedStyle(this);
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
this.offsetParent !== null;
}`,
isEnabled: 'function() { return !this.disabled; }',
isChecked: 'function() { return this.checked || false; }',
setChecked: `function(checked) {
this.checked = checked;
this.dispatchEvent(new Event('change', { bubbles: true }));
}`,
selectOption: `function(values) {
if (this.tagName !== 'SELECT') return false;
const valueArray = Array.isArray(values) ? values : [values];
Array.from(this.options).forEach(option => {
option.selected = valueArray.includes(option.value);
});
this.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}`,
getSelectedOptions: `function() {
if (this.tagName !== 'SELECT') return [];
return Array.from(this.selectedOptions).map(option => ({
value: option.value,
text: option.textContent.trim(),
index: option.index
}));
}`,
getFormData: `function() {
const formData = {};
const elements = this.elements;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
const name = element.name;
if (!name) continue;
switch (element.type) {
case 'checkbox':
if (element.checked) {
formData[name] = formData[name] ?
(Array.isArray(formData[name]) ? [...formData[name], element.value] : [formData[name], element.value]) :
element.value;
}
break;
case 'radio':
if (element.checked) {
formData[name] = element.value;
}
break;
case 'select-multiple':
formData[name] = Array.from(element.selectedOptions).map(option => option.value);
break;
case 'file':
formData[name] = element.files ? Array.from(element.files).map(file => file.name) : [];
break;
default:
formData[name] = element.value;
}
}
return formData;
}`,
scrollIntoView: `function(options) {
const scrollOptions = options || { behavior: 'smooth', block: 'center' };
this.scrollIntoView(scrollOptions);
}`,
getBoundingRect: `function() {
const rect = this.getBoundingClientRect();
return {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
left: rect.left,
bottom: rect.bottom,
right: rect.right
};
}`,
getElementInfo: `function() {
return {
tagName: this.tagName.toLowerCase(),
className: this.className,
id: this.id,
name: this.name,
value: this.value,
textContent: this.textContent?.trim(),
checked: this.checked,
disabled: this.disabled,
selected: this.selected
};
}`,
doubleClick: `function() {
const event = new MouseEvent('dblclick', {
view: window,
bubbles: true,
cancelable: true
});
this.dispatchEvent(event);
}`,
rightClick: `function() {
const event = new MouseEvent('contextmenu', {
view: window,
bubbles: true,
cancelable: true
});
this.dispatchEvent(event);
}`,
pressKey: `function(key) {
const keyEvent = new KeyboardEvent('keydown', {
key: key,
bubbles: true,
cancelable: true
});
this.dispatchEvent(keyEvent);
}`,
submitForm: 'function() { this.submit(); }',
resetForm: 'function() { this.reset(); }',
clearValue: `function() {
this.value = '';
this.dispatchEvent(new Event('input', { bubbles: true }));
}`
};
function getSecureFunction(functionName) {
const func = SECURE_FUNCTIONS[functionName];
if (!func) {
throw new SecurityError(`Unknown secure function: ${functionName}`, { functionName });
}
return func;
}
// Secure evaluation for page-level operations
export async function executeSecurePageFunction(session, functionName, params = []) {
validateSession(session);
const pageFunction = getSecurePageFunction(functionName);
try {
return await session.send('Runtime.evaluate', {
expression: `(${pageFunction})(${params.map(p => JSON.stringify(p)).join(', ')})`,
returnByValue: true
});
} catch (error) {
throw new SuperPancakeError(
`Secure page function execution failed: ${error.message}`,
'PAGE_FUNCTION_EXECUTION_FAILED',
{ functionName, params }
);
}
}
const SECURE_PAGE_FUNCTIONS = {
scrollToTop: `function() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}`,
scrollToBottom: `function() {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}`,
getScrollPosition: `function() {
return { x: window.scrollX, y: window.scrollY };
}`,
getViewportSize: `function() {
return {
width: window.innerWidth,
height: window.innerHeight
};
}`,
getDocumentReady: `function() {
return document.readyState;
}`
};
function getSecurePageFunction(functionName) {
const func = SECURE_PAGE_FUNCTIONS[functionName];
if (!func) {
throw new SecurityError(`Unknown secure page function: ${functionName}`, { functionName });
}
return func;
}
// Safe text insertion with encoding
export function createSecureTextInsertion(text) {
// Encode text to prevent injection
const encodedText = JSON.stringify(String(text));
return `function(encodedText) {
const text = JSON.parse(encodedText);
this.focus();
this.value = text;
this.dispatchEvent(new Event('input', { bubbles: true }));
}`;
}
// Element query utilities with security checks
export async function resolveNodeSecurely(session, nodeId) {
validateSession(session);
if (!nodeId || typeof nodeId !== 'number') {
throw new SecurityError('Invalid nodeId provided', { nodeId });
}
try {
const resolved = await session.send('DOM.resolveNode', { nodeId });
if (!resolved || !resolved.object) {
throw new SuperPancakeError('Failed to resolve node', 'NODE_RESOLUTION_FAILED', { nodeId });
}
return resolved;
} catch (error) {
throw new SuperPancakeError(
`Node resolution failed: ${error.message}`,
'NODE_RESOLUTION_ERROR',
{ nodeId }
);
}
}