Functions/GenXdev.Webbrowser/Invoke-WebbrowserEvaluation.ps1
############################################################################### <# .SYNOPSIS Executes JavaScript code in a selected web browser tab. .DESCRIPTION Executes JavaScript code in a selected browser tab with support for async/await, promises, and data synchronization between PowerShell and the browser context. Can execute code from strings, files, or URLs. This function provides comprehensive access to browser APIs including IndexedDB, localStorage, sessionStorage, and other web platform features. It includes built-in error handling, timeout management, and support for yielding multiple results from generator functions. The function uses Chrome DevTools Protocol (CDP) debugging connections, which provides privileged access that bypasses standard JavaScript security restrictions. This enables access to storage APIs, cross-origin resources (within the same tab), and other browser features that would normally be restricted in standard web contexts. Key capabilities: - Async/await and Promise support - Generator functions with yield support - Data synchronization via $Global:Data - Privileged access to browser storage APIs - Bypasses same-origin policy restrictions for current page storage - IndexedDB enumeration and data extraction - DOM manipulation and web API access - Error handling and timeout management .PARAMETER Scripts JavaScript code to execute. Can be string content, file paths, or URLs. Accepts pipeline input. .PARAMETER Inspect Adds debugger statement before executing to enable debugging. .PARAMETER NoAutoSelectTab Prevents automatic tab selection if no tab is currently selected. .PARAMETER Edge Selects Microsoft Edge browser for execution. .PARAMETER Chrome Selects Google Chrome browser for execution. .PARAMETER Page Browser page object for execution when using ByReference mode. .PARAMETER ByReference Session reference object when using ByReference mode. .EXAMPLE Execute simple JavaScript Invoke-WebbrowserEvaluation "document.title = 'hello world'" .EXAMPLE PS> Synchronizing data Select-WebbrowserTab -Force; $Global:Data = @{ files= (Get-ChildItem *.* -file | % FullName)}; [int] $number = Invoke-WebbrowserEvaluation " document.body.innerHTML = JSON.stringify(data.files); data.title = document.title; return 123; "; Write-Host " Document title : $($Global:Data.title) return value : $Number "; .EXAMPLE PS> Support for promises Select-WebbrowserTab -Force; Invoke-WebbrowserEvaluation " let myList = []; return new Promise((resolve) => { let i = 0; let a = setInterval(() => { myList.push(++i); if (i == 10) { clearInterval(a); resolve(myList); } }, 1000); }); " .EXAMPLE PS> Support for promises and more this function returns all rows of all tables/datastores of all databases of indexedDb in the selected tab beware, not all websites use indexedDb, it could return an empty set Select-WebbrowserTab -Force; Set-WebbrowserTabLocation "https://www.youtube.com/" Start-Sleep 3 $AllIndexedDbData = Invoke-WebbrowserEvaluation " // enumerate all indexedDB databases for (let db of await indexedDB.databases()) { // request to open database let openRequest = await indexedDB.open(db.name); // wait for eventhandlers to be called await new Promise((resolve,reject) => { openRequest.onsuccess = resolve; openRequest.onerror = reject }); // obtain reference let openedDb = openRequest.result; // initialize result let result = { DatabaseName: db.name, Version: db.version, Stores: [] } // itterate object store names for (let i = 0; i < openedDb.objectStoreNames.length; i++) { // reference let storeName = openedDb.objectStoreNames[i]; // start readonly transaction let tr = openedDb.transaction(storeName); // get objectstore handle let store = tr.objectStore(storeName); // request all data let getRequest = store.getAll(); // await result await new Promise((resolve,reject) => { getRequest.onsuccess = resolve; getRequest.onerror = reject; }); // add result result.Stores.push({ StoreName: storeName, Data: getRequest.result}); } // stream this database contents to the PowerShell pipeline, and continue yield result; } "; $AllIndexedDbData | Out-Host # SECURITY NOTE: This basic example works because the module uses Chrome DevTools # Protocol (CDP) debugging access, which bypasses normal JavaScript security # restrictions. Standard web pages cannot access IndexedDB from other origins, # but this debugging connection has the same privileges as the website itself. # See the enhanced example below for more details on security considerations. .EXAMPLE PS> Enhanced IndexedDB enumeration with metadata and error handling This enhanced approach provides more comprehensive IndexedDB data extraction including database counts, error handling, and metadata. Unlike the basic example above, this version handles security restrictions, provides detailed store information, and includes record counts without necessarily retrieving all data. Select-WebbrowserTab -Force; Set-WebbrowserTabLocation "https://www.youtube.com/" Start-Sleep 3 $EnhancedIndexedDbData = Invoke-WebbrowserEvaluation " // Enhanced IndexedDB enumeration with comprehensive error handling let results = []; for (let dbInfo of await indexedDB.databases()) { try { // Open database with timeout let db = await new Promise((resolve, reject) => { let req = indexedDB.open(dbInfo.name); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); setTimeout(() => reject(new Error('Database open timeout')), 5000); }); let dbResult = { DatabaseName: dbInfo.name, Version: dbInfo.version, ObjectStoreCount: db.objectStoreNames.length, Stores: [] }; // Process each object store for (let i = 0; i < db.objectStoreNames.length; i++) { let storeName = db.objectStoreNames[i]; try { let transaction = db.transaction(storeName, 'readonly'); let store = transaction.objectStore(storeName); // Get record count (faster than retrieving all data) let count = await new Promise((resolve, reject) => { let req = store.count(); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); setTimeout(() => reject(new Error('Count timeout')), 3000); }); dbResult.Stores.push({ StoreName: storeName, RecordCount: count, KeyPath: store.keyPath, AutoIncrement: store.autoIncrement, IndexNames: Array.from(store.indexNames) }); } catch (storeError) { dbResult.Stores.push({ StoreName: storeName, Error: storeError.message }); } } results.push(dbResult); db.close(); } catch (dbError) { results.push({ DatabaseName: dbInfo.name, Error: dbError.message }); } } yield results; "; $EnhancedIndexedDbData | ConvertTo-Json -Depth 10 # Key differences from the basic example: # 1. Includes error handling for database access issues # 2. Provides metadata (KeyPath, AutoIncrement, IndexNames) # 3. Gets record counts without retrieving all data (more efficient) # 4. Handles timeout scenarios # 5. Returns structured information about database schema # 6. More suitable for large databases where retrieving all data would be slow # SECURITY CONSIDERATIONS FOR INDEXEDDB ACCESS: # Both examples work because this module uses Chrome DevTools Protocol (CDP) through # the debugging port, which bypasses standard JavaScript security restrictions: # # Standard JavaScript Limitations: # - Same-origin policy restricts access to IndexedDB from other origins # - Some databases may be hidden or protected by browser security features # - Cross-origin database access is typically blocked # - Service worker databases may have additional protection # # How this example bypasses restrictions: # - Uses CDP debugging connection (--remote-debugging-port) for privileged access # - Executes in the context of the actual page, not a sandboxed environment # - Has the same permissions as the website itself for its own storage # - Can access all databases created by the current origin/domain # # Limitations Even With CDP: # - Cannot access databases from other origins/domains in the same browser # - Cannot access databases from other browser profiles or private browsing # - Some browser extensions may create isolated storage not accessible via JavaScript # # Alternative Approaches for Maximum Access: # - Use GenXdev.Webbrowser with multiple tabs from different origins # - Combine with file system access to browser profile directories (when possible) # - Use browser automation to navigate between different domains # - Consider using CDP Storage domain directly (advanced, not implemented in basic examples) .EXAMPLE PS> Support for yielded pipeline results Select-WebbrowserTab -Force; Invoke-WebbrowserEvaluation " for (let i = 0; i < 10; i++) { await (new Promise((resolve) => setTimeout(resolve, 1000))); yield i; } "; .EXAMPLE PS> Get-ChildItem *.js | Invoke-WebbrowserEvaluation -Edge .EXAMPLE PS> ls *.js | et -e .NOTES Requires the Windows 10+ Operating System #> function Invoke-WebbrowserEvaluation { [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] [Alias('Eval', 'et')] param( ############################################################################### [Parameter( Position = 0, Mandatory = $false, HelpMessage = 'JavaScript code, file path or URL to execute', ValueFromPipeline, ValueFromPipelineByPropertyName) ] [Alias('FullName')] [object[]] $Scripts, ############################################################################### [Parameter( Mandatory = $false, HelpMessage = 'Break in browser debugger before executing', ValueFromPipeline = $false) ] [switch] $Inspect, ############################################################################### [Parameter( Mandatory = $false, ValueFromPipeline = $false, HelpMessage = 'Prevent automatic tab selection' )] [switch] $NoAutoSelectTab, ############################################################################### [Alias('e')] [parameter( Mandatory = $false, HelpMessage = 'Use Microsoft Edge browser' )] [switch] $Edge, ############################################################################### [Alias('ch')] [parameter( Mandatory = $false, HelpMessage = 'Use Google Chrome browser' )] [switch] $Chrome, ############################################################################### [Parameter( HelpMessage = 'Browser page object reference', ValueFromPipeline = $false )] [object] $Page, ############################################################################### [Parameter( HelpMessage = 'Browser session reference object', ValueFromPipeline = $false )] [PSCustomObject] $ByReference ) Begin { # initialize reference tracking $reference = $null # handle reference initialization if (($null -eq $Page) -or ($null -eq $ByReference)) { try { $reference = GenXdev.Webbrowser\Get-ChromiumSessionReference $Page = $Global:chromeController } catch { if ($NoAutoSelectTab -eq $true) { throw $PSItem.Exception } # attempt auto-selection of browser tab try { Microsoft.PowerShell.Utility\Write-Verbose "Auto-selecting browser tab due to missing session reference..." GenXdev.Webbrowser\Select-WebbrowserTab -Chrome:$Chrome -Edge:$Edge | Microsoft.PowerShell.Core\Out-Null $Page = $Global:chromeController $reference = GenXdev.Webbrowser\Get-ChromiumSessionReference Microsoft.PowerShell.Utility\Write-Verbose "Auto-selection completed successfully" } catch { Microsoft.PowerShell.Utility\Write-Verbose "Auto-selection failed: $($PSItem.Exception.Message)" # Re-throw with more context about what failed throw "Failed to establish browser connection. Auto-selection error: $($PSItem.Exception.Message). Try running 'Select-WebbrowserTab -Force' manually first." } } } else { $reference = $ByReference } # validate browser context if (($null -eq $Page) -or ($null -eq $reference)) { throw 'No browser tab selected, use Select-WebbrowserTab to select a tab first.' } # Define the custom JavaScript for Visibility API events and CSS overrides $visibilityScript = @' document.addEventListener('visibilitychange', function() { console.log('Visibility changed to: ' + document.visibilityState); }); '@ $cssOverrideScript = @' document.documentElement.style.setProperty('--default-color-scheme', 'dark'); '@ # Subscribe to the FrameNavigated event to inject the custom JavaScript $null = Microsoft.PowerShell.Utility\Register-ObjectEvent -InputObject $page -EventName FrameNavigated -Action { $null = $page.EvaluateAsync($visibilityScript).Wait() $null = $page.EvaluateAsync($cssOverrideScript).Wait() } } process { Microsoft.PowerShell.Utility\Write-Verbose 'Processing JavaScript evaluation request...' # enumerate provided scripts foreach ($js in $Scripts) { try { if ($js -is [System.IO.FileInfo]) { # make it a string $js = $js.FullName; } Microsoft.PowerShell.Utility\Set-Variable -Name 'Data' -Value $reference.data -Scope Global # is it a file reference? if (($js -is [IO.FileInfo]) -or (($js -is [System.String]) -and [IO.File]::Exists($js))) { # comming from Get-ChildItem command? if ($js -is [IO.FileInfo]) { # make it a string $js = $js.FullName; } # it's a string with a path, load the content $js = [IO.File]::ReadAllText($js, [System.Text.Encoding]::UTF8) } else { # make it a string, if it isn't yet if ($js -isnot [System.String] -or [string]::IsNullOrWhiteSpace($js)) { $js = "$js"; } if ([string]::IsNullOrWhiteSpace($js) -eq $false) { [Uri] $uri = $null; $isUri = ( [Uri]::TryCreate("$js", 'absolute', [ref] $uri) -or ( $js.ToLowerInvariant().StartsWith('www.') -and [Uri]::TryCreate("http://$js", 'absolute', [ref] $uri) ) ) -and $uri.IsWellFormedOriginalString() -and $uri.Scheme -like 'http*'; if ($IsUri) { Microsoft.PowerShell.Utility\Write-Verbose 'is Uri' $httpResult = Microsoft.PowerShell.Utility\Invoke-WebRequest -Uri $Js if ($httpResult.StatusCode -eq 200) { $type = 'text/javascript'; if ($httpResult.Content -Match "[`r`n\s`t;,]import ") { $type = 'module'; } $ScriptHash = [GenXdev.Helpers.Hash]::FormatBytesAsHexString( [GenXdev.Helpers.Hash]::GetSha256BytesOfString($httpResult.Content)); $js = " let scripts = document.getElementsByTagName('script'); for (let i = 0; i < scripts.length; i++) { let script = scripts[i]; if (!!script && typeof script.getAttribute === 'function' && script.getAttribute('data-hash') === '$scriptHash') { return; } } let scriptTag = document.createElement('script'); let scriptLoaded = false; let loaded = () => { }; scriptTag.innerHTML = $(($httpResult.Content | Microsoft.PowerShell.Utility\ConvertTo-Json)); scriptTag.setAttribute('type', '$type'); scriptTag.setAttribute('data-hash', '$ScriptHash'); let head = document.getElementsByTagName('head')[0]; if (!head) { head = document.createElement('head'); document.appendChild(head); } head.appendChild(scriptTag); "; } else { throw "Downloading script '$js' resulted in http statuscode $($HttpResult.StatusCode) - $($HttpResult.StatusDescription)" } } } } # '-Inspect' parameter provided? if ($Inspect -eq $true) { # invoke a debug break-point $js = "debugger;`r`n$js" } Microsoft.PowerShell.Utility\Write-Verbose "Processing: `r`n$($js.Trim())" # convert data object to json, and then again to make it a json string $json = ($reference.data | Microsoft.PowerShell.Utility\ConvertTo-Json -Compress -Depth 100 | Microsoft.PowerShell.Utility\ConvertTo-Json -Compress -Depth 100); # init result $result = $null; $ScriptHash = [GenXdev.Helpers.Hash]::FormatBytesAsHexString( [GenXdev.Helpers.Hash]::GetSha256BytesOfString($js)); $js = "(function(data) { let resultData = window['iwae$ScriptHash'] || { started: false, done: false, success: true, data: data, returnValues: [] } window['iwae$ScriptHash'] = resultData; function catcher(e) { let resultData = window['iwae$ScriptHash']; resultData.success = false; resultData.done = true; try { resultData.returnValue = JSON.stringify(e); } catch (e2) { resultData.returnValue = e+''; } } if (!resultData.started) { resultData.started = true; try { eval($(" (async () => { let result; try { result = (async function*() { $js })(); let resultCount = 0; let resultValue; do { resultValue = await result.next(); if (resultValue.value instanceof Promise) { resultValue.value = await resultValue.value; } let resultData = window['iwae$ScriptHash'] if (resultCount++ === 0 && resultValue.done) { resultData.returnValue = resultValue.value; } else { if (!resultValue.done) { resultData.returnValues.push(resultValue.value); } } } while (!resultValue.done) let resultData = window['iwae$ScriptHash'] resultData.done = true; resultData.success = true; } catch (e) { catcher(e); } })() " | Microsoft.PowerShell.Utility\ConvertTo-Json -Compress -Depth 100)); } catch(e) { catcher(e); } } if (resultData.done) { delete window['iwae$ScriptHash']; } let clone = JSON.parse(JSON.stringify(resultData)); resultData.returnValues = []; return clone; })(JSON.parse($json)); "; [int] $pollCount = 0; $result = $null; do { # de-serialize outputed result object # $reference = Get-ChromiumSessionReference $result = $Page.EvaluateAsync($js, @()).Result if ($null -eq $result) { continue; } $result = ($result | Microsoft.PowerShell.Utility\ConvertFrom-Json); if ($null -ne $result) { Microsoft.PowerShell.Utility\Write-Verbose "Got results: [$($result.getType())] $($result | Microsoft.PowerShell.Utility\ConvertTo-Json -Compress -Depth 100)" } # all good? if ($result -is [PSCustomObject]) { # there was an exception thrown? if ($result.subtype -eq 'error') { # re-throw throw $result; } # got a data object? if ($null -ne $result.data) { # initialize $reference.data = @{} # enumerate properties $result.data | Microsoft.PowerShell.Utility\Get-Member -ErrorAction SilentlyContinue | Microsoft.PowerShell.Core\Where-Object -Property MemberType -Like *Property* | Microsoft.PowerShell.Core\ForEach-Object -ErrorAction SilentlyContinue { # set in a case-sensitive manner $reference.data."$($PSItem.Name)" = $result.data."$($PSItem.Name)" } Microsoft.PowerShell.Utility\Set-Variable -Name 'Data' -Value ($reference.data) -Scope Global } $pollCount++; if (($null -ne $result.returnValues) -and ($result.returnValues.Length -gt 0)) { $result.returnValues | Microsoft.PowerShell.Utility\Write-Output $result.returnValues = @(); } $result.returnValues = @(); } } while (!!$result -and !$result.done -and (-not [Console]::KeyAvailable)); # result indicate an exception thrown? if ($result.success -eq $false) { if ($result.returnValue -is [string]) { # re-throw throw $result.returnValue; } throw 'An unknown script parsing error occured'; } if ($null -ne $result.returnValue) { Microsoft.PowerShell.Utility\Write-Output $result.returnValue; } } Catch { throw " $($PSItem.Exception) $($PSItem.InvocationInfo.PositionMessage) $($PSItem.InvocationInfo.Line) " } } } End { } } |