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.
 
.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
 
.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(DefaultParameterSetName = "Default")]
    [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(
            ParameterSetName = "Default",
            Mandatory = $false,
            ValueFromPipeline = $false,
            HelpMessage = "Prevent automatic tab selection"
        )]
        [switch] $NoAutoSelectTab,
        ###############################################################################
        [Alias("e")]
        [parameter(
            ParameterSetName = "Default",
            Mandatory = $false,
            HelpMessage = "Use Microsoft Edge browser"
        )]
        [switch] $Edge,
        ###############################################################################
        [Alias("ch")]
        [parameter(
            ParameterSetName = "Default",
            Mandatory = $false,
            HelpMessage = "Use Google Chrome browser"
        )]
        [switch] $Chrome,
        ###############################################################################
        [Parameter(
            ParameterSetName = "ByReference",
            Mandatory = $true,
            HelpMessage = "Browser page object reference",
            ValueFromPipeline = $false
        )]
        [object] $Page,
        ###############################################################################
        [Parameter(
            ParameterSetName = "ByReference",
            Mandatory = $true,
            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 = Get-ChromiumSessionReference
                $Page = $Global:chromeController
            }
            catch {
                if ($NoAutoSelectTab -eq $true) {
                    throw $PSItem.Exception
                }

                # attempt auto-selection of browser tab
                Select-WebbrowserTab -Chrome:$Chrome -Edge:$Edge | Out-Null
                $Page = $Global:chromeController
                $reference = Get-ChromiumSessionReference
            }
        }
        else {
            $reference = $ByReference
        }

        # validate browser context
        if (($null -eq $Page) -or ($null -eq $reference)) {
            throw "No browser tab selected"
        }
    }

    Process {
        Write-Verbose "Processing JavaScript evaluation request..."

        # 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 = Register-ObjectEvent -InputObject $page -EventName FrameNavigated -Action {
            $page.EvaluateAsync($visibilityScript).Wait()
            $page.EvaluateAsync($cssOverrideScript).Wait()
        }

        # enumerate provided scripts
        foreach ($js in $Scripts) {

            try {
                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) {
                            Write-Verbose "is Uri"
                            $httpResult = 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 | 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"
                }

                Write-Verbose "Processing: `r`n$($js.Trim())"

                # convert data object to json, and then again to make it a json string
                $json = ($reference.data | ConvertTo-Json -Compress -Depth 100 | 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);
                            }
                        })()
 
                        " | 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 | ConvertFrom-Json);

                    if ($null -ne $result) {

                        Write-Verbose "Got results: [$($result.getType())] $($result | 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 |
                            Get-Member -ErrorAction SilentlyContinue |
                            Where-Object -Property MemberType -Like *Property* |
                            ForEach-Object -ErrorAction SilentlyContinue {

                                # set in a case-sensitive manner
                                $reference.data."$($PSItem.Name)" = $result.data."$($PSItem.Name)"
                            }

                            Set-Variable -Name "Data" -Value ($reference.data) -Scope Global
                        }

                        $pollCount++;

                        if (($null -ne $result.returnValues) -and ($result.returnValues.Length -gt 0)) {

                            $result.returnValues | 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) {

                    Write-Output $result.returnValue;
                }
            }
            Catch {

                throw "
                        $($PSItem.Exception) $($PSItem.InvocationInfo.PositionMessage)
                        $($PSItem.InvocationInfo.Line)
                    "

            }
        }
    }

    End {

    }
}
################################################################################