Public/ps1/Html/Start-ApprxrJobHtml.ps1

<#
    .SYNOPSIS
    Executes an HTTP job for Apprxr, handling authentication, headers, and response parsing.
 
    .DESCRIPTION
    Sends an HTTP request (GET or POST) to a specified URL with provided headers, body, and authentication. Handles authentication via Set-ApprxrAuthenticationRoute, logs all request and response details, and returns a structured result. Handles errors and extracts detailed error information from the response.
 
    .PARAMETER taskInformation
    An object containing the job parameters, including url, httpMethod, headers, body, contentType, channelId, id, and type.
 
    .PARAMETER DebugMode
    If specified, enables debug mode which logs the full result object before returning. Can also be controlled via ApprxrHtmlDebugMode configuration setting.
 
    .EXAMPLE
    Start-ApprxrJobHtml -taskInformation @{ url = 'https://example.com/api'; httpMethod = 'POST'; headers = @{ Authorization = 'Bearer ...' }; body = $jsonBody; contentType = 'application/json' }
 
    .EXAMPLE
    Start-ApprxrJobHtml -taskInformation $taskInfo -DebugMode
 
    .NOTES
    Used for running HTTP-based jobs in Apprxr automation, including REST API calls with authentication and custom headers.
#>

function Start-ApprxrJobHtml {
    param (
        $taskInformation,
        [switch]$debugMode
    )
    # Check if debug mode is enabled
    $debugMode = $false
    try {
        $debugConfig = Get-ApprxrConfigurationValue -name "ApprxrHtmlDebugMode" -ErrorAction SilentlyContinue
        if ($debugConfig -and ($debugConfig -eq $true -or $debugConfig -eq "true" -or $debugConfig -eq "1")) {
            $debugMode = $true
        }
    } catch {
        # Configuration value not found, continue without debug mode
    }

    if ($taskInformation.body) {
        if ($taskInformation.body -is [PSCustomObject]) {
            $taskInformation.body = [string]($taskInformation.body | ConvertTo-Json -Depth 10 -Compress)
             Log ("... Body converted to JSON string from PSCustomObject.")
        }
                Log ("... Body as provided. Because content type is not application/json $($taskInformation.contentType) or body is not an ps object $($taskInformation.body -is [PSCustomObject]) neither a hashtable $($taskInformation.body -is [hashtable]).")
        
        # Fix for PowerShell 5.1: use double quotes and double backslash in regex
        $taskInformation.body = $taskInformation.body -replace "\\\\u", "\u"
             Log ("....Body as provided. Because content type is not application/json $($taskInformation.contentType) or body is not an ps object $($taskInformation.body -is [PSCustomObject]) neither a hashtable $($taskInformation.body -is [hashtable]).")
  
        $taskInformation.body = [regex]::Replace($taskInformation.body, "\\u([0-9A-Fa-f]{4})", { param($m) [char]([convert]::ToInt32($m.Groups[1].Value,16)) })
         Log ("..... Body as provided. Because content type is not application/json $($taskInformation.contentType) or body is not an ps object $($taskInformation.body -is [PSCustomObject]) neither a hashtable $($taskInformation.body -is [hashtable]).")
  
    }


    if ($debugMode) {
        Log ("DEBUG MODE ENABLED")
        Log ("Headers: $($taskInformation.headers | Out-String)")
        Log ("Body: $($taskInformation.body | Out-String)")
        Log ("Method: $($taskInformation.httpMethod | Out-String)")
        Log ("Url: $($taskInformation.url | Out-String)")
        Log ("ContentType: $($taskInformation.contentType | Out-String)")
    }

    if (-not $taskInformation.url) {
        return @{
            message= "No url found"
            sucess= $false
        }
    }

    
    try {
        # Populate authentication info using Get-ApprxrAuthenticationRoute
        $authInfo = Get-ApprxrAuthenticationRoute -hostURI $taskInformation.url -channelId $taskInformation.channelId -id $taskInformation.id -type $taskInformation.type
        if ($authInfo) {
            if ($debugMode) {
                Log ("Authentication Info: $($authInfo | ConvertTo-Json -Depth 10)")
            }
        }
    } catch {
        Log ("Error setting authentication: $($_.Exception.Message)")
        return @{
            message= "Error setting authentication: $($_.Exception.Message)"
            sucess= $false
        
        }
    }

    $result = @{}
    $headers = @{}
    
    # Handle null or empty headers
    if ($taskInformation.headers) {
        $taskInformation.headers.psobject.Properties | ForEach-Object {
            $headers[$_.Name] = $_.Value
        }
    }



    try {
        # Decode any Unicode escape sequences in the URL (e.g., \u0026 for &)
        $decodedUrl = $taskInformation.url -replace '\\u([0-9A-Fa-f]{4})', { [char]([convert]::ToInt32($args[0],16)) }
        if ($DebugMode) { Log ("Decoded URL: $decodedUrl") }
        $invokeParams = @{
            Uri = $decodedUrl
            Method = $taskInformation.httpMethod
            Headers = $headers
            ErrorAction = 'Stop'
        }


        # Add authentication/credential if available
        if ($authInfo) {
            $isCore = $PSVersionTable.PSEdition -eq 'Core'
            # NTLM/Negotiate: Set Credential always, set Authentication only in PowerShell Core
            if ($authInfo.Authentication -eq 'Negotiate' -or $authInfo.Authentication -eq 'NTLM') {
                if ($authInfo.PSObject.Properties['username'] -and $authInfo.PSObject.Properties['password'] -and $authInfo.username -and $authInfo.password) {
                    try {
                        $securePassword = ConvertTo-SecureString -String $authInfo.password -AsPlainText -Force
                        $credential = New-Object System.Management.Automation.PSCredential (($authInfo.username -replace '\\\\','\\'), $securePassword)
                        $invokeParams.Credential = $credential
                        if ($DebugMode) {
                            Log ("Adding Credential for user: $($authInfo.username)")
                        }
                    } catch {
                        Log ("Error creating or assigning credential: $($_.Exception.Message)")
                        throw $_
                    }
                }
              
                $invokeParams | Add-Member -MemberType NoteProperty -Name "Authentication" -Value "Negotiate"
                if ($DebugMode) {
                    Log ("Credential and authentication assignment complete. Preparing to invoke web request.")
                }
            } elseif ($authInfo.Authentication -eq 'Basic') {
                # For Basic, add Authorization header manually
                if ($authInfo.PSObject.Properties['username'] -and $authInfo.PSObject.Properties['password'] -and $authInfo.username -and $authInfo.password) {
                    $pair = "$($authInfo.username):$($authInfo.password)"
                    $bytes = [System.Text.Encoding]::UTF8.GetBytes($pair)
                    $base64 = [Convert]::ToBase64String($bytes)
                    $headers['Authorization'] = "Basic $base64"
                    if ($DebugMode) {
                        Log ("Adding Basic Authorization header for user: $($authInfo.username)")
                    }
                }
                if ($isCore -and $authInfo.PSObject.Properties['Authentication'] -and $authInfo.Authentication) {
                    $invokeParams.Authentication = $authInfo.Authentication
                    if ($DebugMode) {
                        Log ("Adding Authentication (Core): $($authInfo.Authentication)")
                    }
                }
            }
        }
        
        if ($taskInformation.body) {
            $bodyToSend = $taskInformation.body
            if (-not $taskInformation.httpMethod) { $taskInformation.httpMethod = "POST" }
            # Always set Content-Type to include charset=utf-8
            if (-not $taskInformation.contentType) {
                $taskInformation.contentType = "application/json"
            }
            if ($taskInformation.contentType -notmatch "charset") {
                $taskInformation.contentType = $taskInformation.contentType.TrimEnd(';') + "; charset=utf-8"
            }
            $invokeParams.ContentType = $taskInformation.contentType
            # If body is an object and content type is application/json, serialize to JSON
            if ($taskInformation.contentType -like 'application/json*' -and ($bodyToSend -is [PSCustomObject] -or $bodyToSend -is [hashtable])) {
                Log ("Converting body object to JSON string.")
                $bodyToSend = $bodyToSend | ConvertTo-Json -Depth 10
            } else {
                Log ("Using body as provided. Because content type is not application/json $($taskInformation.contentType) or body is not an ps object $($bodyToSend -is [PSCustomObject]) neither a hashtable $($bodyToSend -is [hashtable]).")
            }
            # Ensure UTF-8 encoding if body is a string (send as string, not byte array)
            if ($bodyToSend -is [string]) {
                Log ("Ensuring UTF-8 encoding for request body.")
                $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
                $bytes = $utf8NoBom.GetBytes($bodyToSend)
                $utf8Body = $utf8NoBom.GetString($bytes)
                $invokeParams.Body = $utf8Body
                if ($DebugMode) {
                    Log ("Outgoing Body (UTF-8): $utf8Body")
                }
            } else {
                $invokeParams.Body = $bodyToSend
                if ($DebugMode) {
                    Log ("Outgoing Body (non-string): $($bodyToSend | Out-String)")
                }
            }
            if ($DebugMode) {
                Log ("Outgoing Headers: $($invokeParams.Headers | ConvertTo-Json -Depth 10)")
                Log ("Outgoing Content-Type: $($invokeParams.ContentType)")
            }
        } else {
            if (-not $taskInformation.httpMethod) { $taskInformation.httpMethod = "GET" }
            if ($taskInformation.body) {
                $bodyToSend = $taskInformation.body
                # If body is an object and content type is application/json, serialize to JSON
                if ($taskInformation.contentType -eq 'application/json' -and ($bodyToSend -is [PSCustomObject] -or $bodyToSend -is [hashtable])) {
                    $bodyToSend = $bodyToSend | ConvertTo-Json -Depth 10
                }
                $invokeParams.Body = $bodyToSend
                $invokeParams.ContentType = $taskInformation.contentType
            }
        }
        
        if ($DebugMode) {
            Log ("Send Invoke-WebRequest Parameters: $($invokeParams | ConvertTo-Json -Depth 10)")
        }

        if ($PSVersionTable.PSEdition -eq 'Core') {
            $response = Invoke-WebRequest @invokeParams
        } else {
            $response = Invoke-WebRequest @invokeParams -UseBasicParsing
        }

        if ($DebugMode) { Log ("Invoke-WebRequest completed.") }
        $cookiesObj = Get-CookiesObject $response.Cookies
        if ($DebugMode) {
            Log ("Cookies Object: $($cookiesObj | ConvertTo-Json -Depth 10)")
        }
        if ($null -eq $cookiesObj) { $cookiesObj = @() }
        $headersObj = @{}
        if ($response.PSObject.Properties['Headers']) {
            foreach ($key in $response.Headers.Keys) {
                $headersObj[$key] = $response.Headers[$key]
            }
        } elseif ($response.PSObject.Properties['BaseResponse'] -and $response.BaseResponse.Headers) {
            foreach ($key in $response.BaseResponse.Headers.Keys) {
                $headersObj[$key] = $response.BaseResponse.Headers[$key]
            }
        }
        $resultObj = @{}
        if ($response.PSObject.Properties["StatusCode"]) { $resultObj.StatusCode = $response.StatusCode }
        if ($response.PSObject.Properties["StatusDescription"]) { $resultObj.StatusDescription = $response.StatusDescription }
        $resultObj.Headers = $headersObj
        $resultObj.Cookies = $cookiesObj
        if ($response.PSObject.Properties["Content"]) { $resultObj.Content = $response.Content } else { $resultObj.Content = $response }
        if ($response.PSObject.Properties["RawContent"]) { $resultObj.RawContent = $response.RawContent }
        if ($response.PSObject.Properties["RawContentLength"]) { $resultObj.RawContentLength = $response.RawContentLength }
        if ($response.PSObject.Properties["BaseResponse"]) { $resultObj.BaseResponse = $response.BaseResponse }
        Log ("Response: $($resultObj | ConvertTo-Json -Depth 100)")
        
        $returnObject = @{
            message = $resultObj | ConvertTo-Json -Depth 100
            sucess = $true
        }
        
        if ($DebugMode) {
            Log ("DEBUG MODE - Full Result Object: $($returnObject | ConvertTo-Json -Depth 100)")
        }
        
        return $returnObject
    } catch {
        $headersObj = @{}
        $cookiesObj = @()
        $errorObj = @{}
        $errorContent = $null
        if ($_.Exception.Response) {
            $resp = $_.Exception.Response
            # Try to extract the raw response body (the red text)
            try {
                $reader = [System.IO.StreamReader]::new($resp.GetResponseStream())
                $errorContent = $reader.ReadToEnd()
                $reader.Close()
            } catch {}
            if ($resp.Headers) {
                foreach ($key in $resp.Headers.Keys) {
                    $headersObj[$key] = $resp.Headers[$key]
                }
            }
            $cookiesObj = Get-CookiesObject $resp.Cookies
            $errorObj.Headers = $headersObj
            $errorObj.Cookies = $cookiesObj
            if ($resp.StatusCode) { $errorObj.StatusCode = $resp.StatusCode }
            if ($resp.StatusDescription) { $errorObj.StatusDescription = $resp.StatusDescription }
            if ($resp.Content) { $errorObj.Content = $resp.Content }
            if ($resp.RawContent) { $errorObj.RawContent = $resp.RawContent }
            if ($resp.RawContentLength) { $errorObj.RawContentLength = $resp.RawContentLength }
            if ($resp.BaseResponse) { $errorObj.BaseResponse = $resp.BaseResponse }
            if ($_.Exception.Message) { $errorObj.Message = $_.Exception.Message }
            if ($errorContent) { $errorObj.ErrorContent = $errorContent }
        } else {
            if ($_.Exception.Message) { $errorObj.Message = $_.Exception.Message }
        }
        if ($null -eq $errorObj -or $errorObj.Count -eq 0) {
            Log ("Exception: $($_.Exception.Message)")
        } else {
            Log ("Error: $($errorObj | ConvertTo-Json -Depth 100)")
        }
        
        $returnObject = @{
            message = if ($null -eq $errorObj -or $errorObj.Count -eq 0) { $_.Exception.Message } else { $errorObj | ConvertTo-Json -Depth 100 }
            messageCode = if ($_.Exception.Response) { $_.Exception.Response.StatusCode.value__ } else { $null }
            sucess = $false
        }
        
        if ($DebugMode) {
            Log ("DEBUG MODE - Full Error Object: $($returnObject | ConvertTo-Json -Depth 100)")
        }
        
        return $returnObject
    }

}