/** * Write commands — navigate and interact with pages (side effects) * * goto, back, forward, reload, click, fill, select, hover, type, * press, scroll, wait, viewport, cookie, header, useragent */ import type { BrowserManager } from './browser-manager'; import { findInstalledBrowsers, importCookies, listSupportedBrowserNames } from './cookie-import-browser'; import { validateNavigationUrl } from './url-validation'; import * as fs from 'fs'; import * as path from 'path'; import { TEMP_DIR, isPathWithin } from './platform'; import { modifyStyle, undoModification, resetModifications, getModificationHistory } from './cdp-inspector'; // Security: Path validation for screenshot output const SAFE_DIRECTORIES = [TEMP_DIR, process.cwd()]; function validateOutputPath(filePath: string): void { const resolved = path.resolve(filePath); const isSafe = SAFE_DIRECTORIES.some(dir => isPathWithin(resolved, dir)); if (!isSafe) { throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`); } } /** Common selectors for page clutter removal */ const CLEANUP_SELECTORS = { ads: [ 'ins.adsbygoogle', '[id^="google_ads"]', '[id^="div-gpt-ad"]', 'iframe[src*="doubleclick"]', 'iframe[src*="googlesyndication"]', '[class*="ad-banner"]', '[class*="ad-wrapper"]', '[class*="ad-container"]', '[data-ad]', '[data-ad-slot]', '[class*="sponsored"]', '.ad', '.ads', '.advert', '.advertisement', ], cookies: [ '[class*="cookie-consent"]', '[class*="cookie-banner"]', '[class*="cookie-notice"]', '[id*="cookie-consent"]', '[id*="cookie-banner"]', '[id*="cookie-notice"]', '[class*="consent-banner"]', '[class*="consent-modal"]', '[class*="gdpr"]', '[id*="gdpr"]', '[class*="CookieConsent"]', '[id*="CookieConsent"]', '#onetrust-consent-sdk', '.onetrust-pc-dark-filter', '[class*="cc-banner"]', '[class*="cc-window"]', ], sticky: [ // Select fixed/sticky positioned elements (except navs and headers at top) // This is handled via JavaScript evaluation, not pure selectors ], social: [ '[class*="social-share"]', '[class*="share-buttons"]', '[class*="share-bar"]', '[class*="social-widget"]', '[class*="social-icons"]', 'iframe[src*="facebook.com/plugins"]', 'iframe[src*="platform.twitter"]', '[class*="fb-like"]', '[class*="tweet-button"]', '[class*="addthis"]', '[class*="sharethis"]', ], }; export async function handleWriteCommand( command: string, args: string[], bm: BrowserManager ): Promise { const page = bm.getPage(); // Frame-aware target for locator-based operations (click, fill, etc.) const target = bm.getActiveFrameOrPage(); const inFrame = bm.getFrame() !== null; switch (command) { case 'goto': { if (inFrame) throw new Error('Cannot use goto inside a frame. Run \'frame main\' first.'); const url = args[0]; if (!url) throw new Error('Usage: browse goto '); await validateNavigationUrl(url); const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 }); const status = response?.status() || 'unknown'; return `Navigated to ${url} (${status})`; } case 'back': { if (inFrame) throw new Error('Cannot use back inside a frame. Run \'frame main\' first.'); await page.goBack({ waitUntil: 'domcontentloaded', timeout: 15000 }); return `Back → ${page.url()}`; } case 'forward': { if (inFrame) throw new Error('Cannot use forward inside a frame. Run \'frame main\' first.'); await page.goForward({ waitUntil: 'domcontentloaded', timeout: 15000 }); return `Forward → ${page.url()}`; } case 'reload': { if (inFrame) throw new Error('Cannot use reload inside a frame. Run \'frame main\' first.'); await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 }); return `Reloaded ${page.url()}`; } case 'click': { const selector = args[0]; if (!selector) throw new Error('Usage: browse click '); // Auto-route: if ref points to a real