Functions/GenXdev.Webbrowser/Invoke-WebbrowserEvaluation.ps1

###############################################################################

<#
.SYNOPSIS
Executes JavaScript code in a selected web browser tab.
 
.DESCRIPTION
Runs one or more JavaScript scripts in a selected web browser tab. Scripts can be
provided as strings, files, or URLs. The function supports data synchronization
between PowerShell and the browser via a 'data' object.
 
.PARAMETER Scripts
JavaScript code, URL, or file path to execute. Accepts pipeline input.
 
.PARAMETER Inspect
Adds debugger statement before executing to enable debugging.
 
.PARAMETER AsJob
Executes the evaluation as a background job.
 
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 = "A string containing javascript, a url or a file reference to a javascript file",
            ValueFromPipeline,
            ValueFromPipelineByPropertyName)
        ]
        [Alias('FullName')]
        [object[]] $Scripts,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Will cause the developer tools of the webbrowser to break, before executing the scripts, allowing you to debug it",
            ValueFromPipeline = $false)
        ]
        [switch] $Inspect,
        ###############################################################################
        [Parameter(
            ParameterSetName = "Default",
            Mandatory = $false,
            ValueFromPipeline = $false
        )]
        [switch] $NoAutoSelectTab,
        ###############################################################################
        [Alias("e")]
        [parameter(
            ParameterSetName = "Default",
            Mandatory = $false,
            HelpMessage = "Select in Microsoft Edge"
        )]
        [switch] $Edge,
        ###############################################################################
        [Alias("ch")]
        [parameter(
            ParameterSetName = "Default",
            Mandatory = $false,
            HelpMessage = "Select in Google Chrome"
        )]
        [switch] $Chrome,
        ###############################################################################
        [Parameter(
            ParameterSetName = "ByReference",
            Mandatory = $true,
            ValueFromPipeline = $false
        )]
        [object] $Page,
        ###############################################################################
        [Parameter(
            ParameterSetName = "ByReference",
            Mandatory = $true,
            ValueFromPipeline = $false
        )]
        [PSCustomObject] $ByReference
    )

    Begin {
        $reference = $null;
        if (($null -eq $Page) -or ($null -eq $ByReference)) {

            try {
                $reference = Get-ChromiumSessionReference
                $Page = $Global:chromeController
            }
            catch {
                if ($NoAutoSelectTab -eq $true) {

                    throw $PSItem.Exception
                }

                Select-WebbrowserTab -Chrome:$Chrome -Edge:$Edge | Out-Null
                $Page = $Global:chromeController
                $reference = Get-ChromiumSessionReference
            }
        }
        else {

            $reference = $ByReference
        }

        if (($null -eq $Page) -or ($null -eq $reference)) {

            throw "No browser tab selected"
        }
    }

    Process {

        Write-Verbose "Processing.."

        # 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 {

    }
}