Public/Responses.ps1

<#
.SYNOPSIS
Attaches a file onto the Response for downloading.
 
.DESCRIPTION
Attaches a file from the "/public", and static Routes, onto the Response for downloading.
If the supplied path is not in the Static Routes but is a literal/relative path, then this file is used instead.
 
.PARAMETER Path
The Path to a static file relative to the "/public" directory, or a static Route.
If the supplied Path doesn't match any custom static Route, then Pode will look in the "/public" directory.
Failing this, if the file path exists as a literal/relative file, then this file is used as a fall back.
 
.PARAMETER ContentType
Manually specify the content type of the response rather than infering it from the attachment's file extension.
The supplied value must match the valid ContentType format, e.g. application/json
 
.PARAMETER EndpointName
Optional EndpointName that the static route was creating under.
 
.EXAMPLE
Set-PodeResponseAttachment -Path 'downloads/installer.exe'
 
.EXAMPLE
Set-PodeResponseAttachment -Path './image.png'
 
.EXAMPLE
Set-PodeResponseAttachment -Path 'c:/content/accounts.xlsx'
 
.EXAMPLE
Set-PodeResponseAttachment -Path './data.txt' -ContentType 'application/json'
 
.EXAMPLE
Set-PodeResponseAttachment -Path '/assets/data.txt' -EndpointName 'Example'
#>

function Set-PodeResponseAttachment
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]
        $Path,

        [ValidatePattern('^\w+\/[\w\.\+-]+$')]
        [string]
        $ContentType,

        [Parameter()]
        [string]
        $EndpointName
    )

    # already sent? skip
    if ($WebEvent.Response.Sent) {
        return
    }

    # only attach files from public/static-route directories when path is relative
    $_path = (Find-PodeStaticRoute -Path $Path -CheckPublic -EndpointName $EndpointName).Content.Source

    # if there's no path, check the original path (in case it's literal/relative)
    if (!(Test-PodePath $_path -NoStatus)) {
        $Path = Get-PodeRelativePath -Path $Path -JoinRoot

        if (Test-PodePath $Path -NoStatus) {
            $_path = $Path
        }
    }

    # test the file path, and set status accordingly
    if (!(Test-PodePath $_path)) {
        return
    }

    $filename = Get-PodeFileName -Path $_path
    $ext = Get-PodeFileExtension -Path $_path -TrimPeriod

    try {
        # setup the content type and disposition
        if (!$ContentType) {
            $WebEvent.Response.ContentType = (Get-PodeContentType -Extension $ext)
        }
        else {
            $WebEvent.Response.ContentType = $ContentType
        }

        Set-PodeHeader -Name 'Content-Disposition' -Value "attachment; filename=$($filename)"

        # if serverless, get the content raw and return
        if (!$WebEvent.Streamed) {
            if (Test-PodeIsPSCore) {
                $content = (Get-Content -Path $_path -Raw -AsByteStream)
            }
            else {
                $content = (Get-Content -Path $_path -Raw -Encoding byte)
            }

            $WebEvent.Response.Body = $content
        }

        # else if normal, stream the content back
        else {
            # setup the response details and headers
            $WebEvent.Response.SendChunked = $false

            # set file as an attachment on the response
            $buffer = [byte[]]::new(64 * 1024)
            $read = 0

            # open up the file as a stream
            $fs = (Get-Item $_path).OpenRead()
            $WebEvent.Response.ContentLength64 = $fs.Length

            while (($read = $fs.Read($buffer, 0, $buffer.Length)) -gt 0) {
                $WebEvent.Response.OutputStream.Write($buffer, 0, $read)
            }
        }
    }
    finally {
        Close-PodeDisposable -Disposable $fs
    }
}

<#
.SYNOPSIS
Writes a String or a Byte[] to the Response.
 
.DESCRIPTION
Writes a String or a Byte[] to the Response, as some specified content type. This value can also be cached.
 
.PARAMETER Value
A String value to write.
 
.PARAMETER Bytes
An array of Bytes to write.
 
.PARAMETER ContentType
The content type of the data being written.
 
.PARAMETER MaxAge
The maximum age to cache the value on the browser, in seconds.
 
.PARAMETER StatusCode
The status code to set against the response.
 
.PARAMETER Cache
Should the value be cached by browsers, or not?
 
.EXAMPLE
Write-PodeTextResponse -Value 'Leeeeeerrrooooy Jeeeenkiiins!'
 
.EXAMPLE
Write-PodeTextResponse -Value '{"name": "Rick"}' -ContentType 'application/json'
 
.EXAMPLE
Write-PodeTextResponse -Bytes (Get-Content -Path ./some/image.png -Raw -AsByteStream) -Cache -MaxAge 1800
 
.EXAMPLE
Write-PodeTextResponse -Value 'Untitled Text Response' -StatusCode 418
#>

function Write-PodeTextResponse
{
    [CmdletBinding(DefaultParameterSetName='String')]
    param (
        [Parameter(ParameterSetName='String', ValueFromPipeline=$true, Position=0)]
        [string]
        $Value,

        [Parameter(ParameterSetName='Bytes')]
        [byte[]]
        $Bytes,

        [Parameter()]
        [string]
        $ContentType = 'text/plain',

        [Parameter()]
        [int]
        $MaxAge = 3600,

        [Parameter()]
        [int]
        $StatusCode = 200,

        [switch]
        $Cache
    )

    $isStringValue = ($PSCmdlet.ParameterSetName -ieq 'string')
    $isByteValue = ($PSCmdlet.ParameterSetName -ieq 'bytes')

    # set the status code of the response, but only if it's not 200 (to prevent overriding)
    if ($StatusCode -ne 200) {
        Set-PodeResponseStatus -Code $StatusCode -NoErrorPage
    }

    # if there's nothing to write, return
    if ($isStringValue -and [string]::IsNullOrWhiteSpace($Value)) {
        return
    }

    if ($isByteValue -and (($null -eq $Bytes) -or ($Bytes.Length -eq 0))) {
        return
    }

    # if the response stream isn't writable or already sent, return
    $res = $WebEvent.Response
    if (($null -eq $res) -or ($WebEvent.Streamed -and (($null -eq $res.OutputStream) -or !$res.OutputStream.CanWrite -or $res.Sent))) {
        return
    }

    # set a cache value
    if ($Cache) {
        Set-PodeHeader -Name 'Cache-Control' -Value "max-age=$($MaxAge), must-revalidate"
        Set-PodeHeader -Name 'Expires' -Value ([datetime]::UtcNow.AddSeconds($MaxAge).ToString("r", [CultureInfo]::InvariantCulture))
    }

    # specify the content-type if supplied (adding utf-8 if missing)
    if (![string]::IsNullOrWhiteSpace($ContentType)) {
        $charset = 'charset=utf-8'
        if ($ContentType -inotcontains $charset) {
            $ContentType = "$($ContentType); $($charset)"
        }

        $res.ContentType = $ContentType
    }

    # if we're serverless, set the string as the body
    if (!$WebEvent.Streamed) {
        if ($isStringValue) {
            $res.Body = $Value
        }
        else {
            $res.Body = $Bytes
        }
    }

    else {
        # convert string to bytes
        if ($isStringValue) {
            $Bytes = ConvertFrom-PodeValueToBytes -Value $Value
        }

        # check if we only need a range of the bytes
        if (($null -ne $WebEvent.Ranges) -and ($WebEvent.Response.StatusCode -eq 200) -and ($StatusCode -eq 200)) {
            $lengths = @()
            $size = $Bytes.Length

            $Bytes = @(foreach ($range in $WebEvent.Ranges) {
                # ensure range not invalid
                if (([int]$range.Start -lt 0) -or ([int]$range.Start -ge $size) -or ([int]$range.End -lt 0)) {
                    Set-PodeResponseStatus -Code 416 -NoErrorPage
                    return
                }

                # skip start bytes only
                if ([string]::IsNullOrWhiteSpace($range.End)) {
                    $Bytes[$range.Start..($size - 1)]
                    $lengths += "$($range.Start)-$($size - 1)/$($size)"
                }

                # end bytes only
                elseif ([string]::IsNullOrWhiteSpace($range.Start)) {
                    if ([int]$range.End -gt $size) {
                        $range.End = $size
                    }

                    if ([int]$range.End -gt 0) {
                        $Bytes[$($size - $range.End)..($size - 1)]
                        $lengths += "$($size - $range.End)-$($size - 1)/$($size)"
                    }
                    else {
                        $lengths += "0-0/$($size)"
                    }
                }

                # normal range
                else {
                    if ([int]$range.End -ge $size) {
                        Set-PodeResponseStatus -Code 416 -NoErrorPage
                        return
                    }

                    $Bytes[$range.Start..$range.End]
                    $lengths += "$($range.Start)-$($range.End)/$($size)"
                }
            })

            Set-PodeHeader -Name 'Content-Range' -Value "bytes $($lengths -join ', ')"
            if ($StatusCode -eq 200) {
                Set-PodeResponseStatus -Code 206 -NoErrorPage
            }
        }

        # check if we need to compress the response
        if ($PodeContext.Server.Web.Compression.Enabled -and ![string]::IsNullOrWhiteSpace($WebEvent.AcceptEncoding)) {
            try {
                $ms = New-Object -TypeName System.IO.MemoryStream
                $stream = New-Object "System.IO.Compression.$($WebEvent.AcceptEncoding)Stream"($ms, [System.IO.Compression.CompressionMode]::Compress, $true)
                $stream.Write($Bytes, 0, $Bytes.Length)
                $stream.Close()
                $ms.Position = 0
                $Bytes = $ms.ToArray()
            }
            finally {
                if ($null -ne $stream) {
                    $stream.Close()
                }

                if ($null -ne $ms) {
                    $ms.Close()
                }
            }

            # set content encoding header
            Set-PodeHeader -Name 'Content-Encoding' -Value $WebEvent.AcceptEncoding
        }

        # write the content to the response stream
        $res.ContentLength64 = $Bytes.Length

        try {
            $ms = New-Object -TypeName System.IO.MemoryStream
            $ms.Write($Bytes, 0, $Bytes.Length)
            $ms.WriteTo($res.OutputStream)
        }
        catch {
            if ((Test-PodeValidNetworkFailure $_.Exception)) {
                return
            }

            $_ | Write-PodeErrorLog
            throw
        }
        finally {
            if ($null -ne $ms) {
                $ms.Close()
            }
        }
    }
}

<#
.SYNOPSIS
Renders the content of a static, or dynamic, file on the Response.
 
.DESCRIPTION
Renders the content of a static, or dynamic, file on the Response.
You can set browser's to cache the content, and also override the file's content type.
 
.PARAMETER Path
The path to a file.
 
.PARAMETER Data
A HashTable of dynamic data to supply to a dynamic file.
 
.PARAMETER ContentType
The content type of the file's contents - this overrides the file's extension.
 
.PARAMETER MaxAge
The maximum age to cache the file's content on the browser, in seconds.
 
.PARAMETER StatusCode
The status code to set against the response.
 
.PARAMETER Cache
Should the file's content be cached by browsers, or not?
 
.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt'
 
.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt' -Cache -MaxAge 1800
 
.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt' -ContentType 'application/json'
 
.EXAMPLE
Write-PodeFileResponse -Path 'C:/Views/Index.pode' -Data @{ Counter = 2 }
 
.EXAMPLE
Write-PodeFileResponse -Path 'C:/Files/Stuff.txt' -StatusCode 201
#>

function Write-PodeFileResponse
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [ValidateNotNull()]
        [string]
        $Path,

        [Parameter()]
        $Data = @{},

        [Parameter()]
        [string]
        $ContentType = $null,

        [Parameter()]
        [int]
        $MaxAge = 3600,

        [Parameter()]
        [int]
        $StatusCode = 200,

        [switch]
        $Cache
    )

    # resolve for relative path
    $Path = Get-PodeRelativePath -Path $Path -JoinRoot

    # test the file path, and set status accordingly
    if (!(Test-PodePath $Path -FailOnDirectory)) {
        return
    }

    # are we dealing with a dynamic file for the view engine? (ignore html)
    $mainExt = Get-PodeFileExtension -Path $Path -TrimPeriod

    # generate dynamic content
    if (![string]::IsNullOrWhiteSpace($mainExt) -and (
        ($mainExt -ieq 'pode') -or
        ($mainExt -ieq $PodeContext.Server.ViewEngine.Extension -and $PodeContext.Server.ViewEngine.IsDynamic)
    )) {
        $content = Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data

        # get the sub-file extension, if empty, use original
        $subExt = Get-PodeFileExtension -Path (Get-PodeFileName -Path $Path -WithoutExtension) -TrimPeriod
        $subExt = (Protect-PodeValue -Value $subExt -Default $mainExt)

        $ContentType = (Protect-PodeValue -Value $ContentType -Default (Get-PodeContentType -Extension $subExt))
        Write-PodeTextResponse -Value $content -ContentType $ContentType -StatusCode $StatusCode
    }

    # this is a static file
    else {
        if (Test-PodeIsPSCore) {
            $content = (Get-Content -Path $Path -Raw -AsByteStream)
        }
        else {
            $content = (Get-Content -Path $Path -Raw -Encoding byte)
        }

        $ContentType = (Protect-PodeValue -Value $ContentType -Default (Get-PodeContentType -Extension $mainExt))
        Write-PodeTextResponse -Bytes $content -ContentType $ContentType -MaxAge $MaxAge -StatusCode $StatusCode -Cache:$Cache
    }
}

<#
.SYNOPSIS
Writes CSV data to the Response.
 
.DESCRIPTION
Writes CSV data to the Response, setting the content type accordingly.
 
.PARAMETER Value
A String, PSObject, or HashTable value.
 
.PARAMETER Path
The path to a CSV file.
 
.PARAMETER StatusCode
The status code to set against the response.
 
.EXAMPLE
Write-PodeCsvResponse -Value "Name`nRick"
 
.EXAMPLE
Write-PodeCsvResponse -Value @{ Name = 'Rick' }
 
.EXAMPLE
Write-PodeCsvResponse -Path 'E:/Files/Names.csv'
#>

function Write-PodeCsvResponse
{
    [CmdletBinding(DefaultParameterSetName='Value')]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='Value', ValueFromPipeline=$true, Position=0)]
        $Value,

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $Path,

        [Parameter()]
        [int]
        $StatusCode = 200
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }

        'value' {
            if ($Value -isnot [string]) {
                $Value = @(foreach ($v in $Value) {
                    New-Object psobject -Property $v
                })

                if (Test-PodeIsPSCore) {
                    $Value = ($Value | ConvertTo-Csv -Delimiter ',' -IncludeTypeInformation:$false)
                }
                else {
                    $Value = ($Value | ConvertTo-Csv -Delimiter ',' -NoTypeInformation)
                }

                $Value = ($Value -join ([environment]::NewLine))
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = [string]::Empty
    }

    Write-PodeTextResponse -Value $Value -ContentType 'text/csv' -StatusCode $StatusCode
}

<#
.SYNOPSIS
Writes HTML data to the Response.
 
.DESCRIPTION
Writes HTML data to the Response, setting the content type accordingly.
 
.PARAMETER Value
A String, PSObject, or HashTable value.
 
.PARAMETER Path
The path to a HTML file.
 
.PARAMETER StatusCode
The status code to set against the response.
 
.EXAMPLE
Write-PodeHtmlResponse -Value '<html><body>Hello!</body></html>'
 
.EXAMPLE
Write-PodeHtmlResponse -Value @{ Message = 'Hello, all!' }
 
.EXAMPLE
Write-PodeHtmlResponse -Path 'E:/Site/About.html'
#>

function Write-PodeHtmlResponse
{
    [CmdletBinding(DefaultParameterSetName='Value')]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='Value', ValueFromPipeline=$true, Position=0)]
        $Value,

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $Path,

        [Parameter()]
        [int]
        $StatusCode = 200
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }

        'value' {
            if ($Value -isnot [string]) {
                $Value = ($Value | ConvertTo-Html)
                $Value = ($Value -join ([environment]::NewLine))
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = [string]::Empty
    }

    Write-PodeTextResponse -Value $Value -ContentType 'text/html' -StatusCode $StatusCode
}

<#
.SYNOPSIS
Writes Markdown data to the Response.
 
.DESCRIPTION
Writes Markdown data to the Response, with the option to render it as HTML.
 
.PARAMETER Value
A String, PSObject, or HashTable value.
 
.PARAMETER Path
The path to a Markdown file.
 
.PARAMETER StatusCode
The status code to set against the response.
 
.PARAMETER AsHtml
If supplied, the Markdown will be converted to HTML. (This is only supported in PS7+)
 
.EXAMPLE
Write-PodeMarkdownResponse -Value '# Hello, world!' -AsHtml
 
.EXAMPLE
Write-PodeMarkdownResponse -Path 'E:/Site/About.md'
#>

function Write-PodeMarkdownResponse
{
    [CmdletBinding(DefaultParameterSetName='Value')]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='Value', ValueFromPipeline=$true, Position=0)]
        $Value,

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $Path,

        [Parameter()]
        [int]
        $StatusCode = 200,

        [switch]
        $AsHtml
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = [string]::Empty
    }

    $mimeType = 'text/markdown'

    if ($AsHtml) {
        if ($PSVersionTable.PSVersion.Major -ge 7) {
            $mimeType = 'text/html'
            $Value = ($Value | ConvertFrom-Markdown).Html
        }
    }

    Write-PodeTextResponse -Value $Value -ContentType $mimeType -StatusCode $StatusCode
}

<#
.SYNOPSIS
Writes JSON data to the Response.
 
.DESCRIPTION
Writes JSON data to the Response, setting the content type accordingly.
 
.PARAMETER Value
A String, PSObject, or HashTable value. For non-string values, they will be converted to JSON.
 
.PARAMETER Path
The path to a JSON file.
 
.PARAMETER Depth
The Depth to generate the JSON document - the larger this value the worse performance gets.
 
.PARAMETER StatusCode
The status code to set against the response.
 
.EXAMPLE
Write-PodeJsonResponse -Value '{"name": "Rick"}'
 
.EXAMPLE
Write-PodeJsonResponse -Value @{ Name = 'Rick' } -StatusCode 201
 
.EXAMPLE
Write-PodeJsonResponse -Path 'E:/Files/Names.json'
#>

function Write-PodeJsonResponse
{
    [CmdletBinding(DefaultParameterSetName='Value')]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='Value', ValueFromPipeline=$true, Position=0)]
        $Value,

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $Path,

        [Parameter()]
        [int]
        $Depth = 10,

        [Parameter()]
        [int]
        $StatusCode = 200
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }

        'value' {
            if ($Value -isnot [string]) {
                if ($Depth -le 0) {
                    $Value = (ConvertTo-Json -InputObject $Value -Compress)
                }
                else {
                    $Value = (ConvertTo-Json -InputObject $Value -Depth $Depth -Compress)
                }
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = '{}'
    }

    Write-PodeTextResponse -Value $Value -ContentType 'application/json' -StatusCode $StatusCode
}

<#
.SYNOPSIS
Writes XML data to the Response.
 
.DESCRIPTION
Writes XML data to the Response, setting the content type accordingly.
 
.PARAMETER Value
A String, PSObject, or HashTable value.
 
.PARAMETER Path
The path to an XML file.
 
.PARAMETER StatusCode
The status code to set against the response.
 
.EXAMPLE
Write-PodeXmlResponse -Value '<root><name>Rick</name></root>'
 
.EXAMPLE
Write-PodeXmlResponse -Value @{ Name = 'Rick' } -StatusCode 201
 
.EXAMPLE
Write-PodeXmlResponse -Path 'E:/Files/Names.xml'
#>

function Write-PodeXmlResponse
{
    [CmdletBinding(DefaultParameterSetName='Value')]
    param (
        [Parameter(Mandatory=$true, ParameterSetName='Value', ValueFromPipeline=$true, Position=0)]
        $Value,

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $Path,

        [Parameter()]
        [int]
        $StatusCode = 200
    )

    switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) {
        'file' {
            if (Test-PodePath $Path) {
                $Value = Get-PodeFileContent -Path $Path
            }
        }

        'value' {
            if ($Value -isnot [string]) {
                $Value = @(foreach ($v in $Value) {
                    New-Object psobject -Property $v
                })

                $Value = ($Value | ConvertTo-Xml -Depth 10 -As String -NoTypeInformation)
            }
        }
    }

    if ([string]::IsNullOrWhiteSpace($Value)) {
        $Value = [string]::Empty
    }

    Write-PodeTextResponse -Value $Value -ContentType 'text/xml' -StatusCode $StatusCode
}

<#
.SYNOPSIS
Renders a dynamic, or static, View on the Response.
 
.DESCRIPTION
Renders a dynamic, or static, View on the Response; allowing for dynamic data to be supplied.
 
.PARAMETER Path
The path to a View, relative to the "/views" directory. (Extension is optional).
 
.PARAMETER Data
Any dynamic data to supply to a dynamic View.
 
.PARAMETER StatusCode
The status code to set against the response.
 
.PARAMETER Folder
If supplied, a custom views folder will be used.
 
.PARAMETER FlashMessages
Automatically supply all Flash messages in the current session to the View.
 
.EXAMPLE
Write-PodeViewResponse -Path 'index'
 
.EXAMPLE
Write-PodeViewResponse -Path 'accounts/profile_page' -Data @{ Username = 'Morty' }
 
.EXAMPLE
Write-PodeViewResponse -Path 'login' -FlashMessages
#>

function Write-PodeViewResponse
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]
        $Path,

        [Parameter()]
        [hashtable]
        $Data = @{},

        [Parameter()]
        [int]
        $StatusCode = 200,

        [Parameter()]
        [string]
        $Folder,

        [switch]
        $FlashMessages
    )

    # default data if null
    if ($null -eq $Data) {
        $Data = @{}
    }

    # add path to data as "pagename" - unless key already exists
    if (!$Data.ContainsKey('pagename')) {
        $Data['pagename'] = $Path
    }

    # load all flash messages if needed
    if ($FlashMessages -and ($null -ne $WebEvent.Session.Data.Flash)) {
        $Data['flash'] = @{}

        foreach ($name in (Get-PodeFlashMessageNames)) {
            $Data.flash[$name] = (Get-PodeFlashMessage -Name $name)
        }
    }
    elseif ($null -eq $Data['flash']) {
        $Data['flash'] = @{}
    }

    # add view engine extension
    $ext = Get-PodeFileExtension -Path $Path
    if ([string]::IsNullOrWhiteSpace($ext)) {
        $Path += ".$($PodeContext.Server.ViewEngine.Extension)"
    }

    # only look in the view directories
    $viewFolder = $PodeContext.Server.InbuiltDrives['views']
    if (![string]::IsNullOrWhiteSpace($Folder)) {
        $viewFolder = $PodeContext.Server.Views[$Folder]
    }

    $Path = [System.IO.Path]::Combine($viewFolder, $Path)

    # test the file path, and set status accordingly
    if (!(Test-PodePath $Path)) {
        return
    }

    # run any engine logic and render it
    $engine = (Get-PodeViewEngineType -Path $Path)
    $value = (Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data)

    switch ($engine.ToLowerInvariant()) {
        'md' {
            Write-PodeMarkdownResponse -Value $value -StatusCode $StatusCode -AsHtml
        }

        default {
            Write-PodeHtmlResponse -Value $value -StatusCode $StatusCode
        }
    }
}

<#
.SYNOPSIS
Sets the Status Code of the Response, and controls rendering error pages.
 
.DESCRIPTION
Sets the Status Code of the Response, and controls rendering error pages.
 
.PARAMETER Code
The Status Code to set on the Response.
 
.PARAMETER Description
An optional Status Description.
 
.PARAMETER Exception
An exception to use when detailing error information on error pages.
 
.PARAMETER ContentType
The content type of the error page to use.
 
.PARAMETER NoErrorPage
Don't render an error page when the Status Code is 400+.
 
.EXAMPLE
Set-PodeResponseStatus -Code 404
 
.EXAMPLE
Set-PodeResponseStatus -Code 500 -Exception $_.Exception
 
.EXAMPLE
Set-PodeResponseStatus -Code 500 -Exception $_.Exception -ContentType 'application/json'
#>

function Set-PodeResponseStatus
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [int]
        $Code,

        [Parameter()]
        [string]
        $Description,

        [Parameter()]
        $Exception,

        [Parameter()]
        [string]
        $ContentType = $null,

        [switch]
        $NoErrorPage
    )

    # already sent? skip
    if ($WebEvent.Response.Sent) {
        return
    }

    # set the code
    $WebEvent.Response.StatusCode = $Code

    # set an appropriate description (mapping if supplied is blank)
    if ([string]::IsNullOrWhiteSpace($Description)) {
        $Description = (Get-PodeStatusDescription -StatusCode $Code)
    }

    if (!$PodeContext.Server.IsServerless -and ![string]::IsNullOrWhiteSpace($Description)) {
        $WebEvent.Response.StatusDescription = $Description
    }

    # if the status code is >=400 then attempt to load error page
    if (!$NoErrorPage -and ($Code -ge 400)) {
        Show-PodeErrorPage -Code $Code -Description $Description -Exception $Exception -ContentType $ContentType
    }
}

<#
.SYNOPSIS
Redirecting a user to a new URL.
 
.DESCRIPTION
Redirecting a user to a new URL, or the same URL as the Request but a different Protocol - or other components.
 
.PARAMETER Url
Redirect the user to a new URL, or a relative path.
 
.PARAMETER EndpointName
The Name of an Endpoint to redirect to.
 
.PARAMETER Port
Change the port of the current Request before redirecting.
 
.PARAMETER Protocol
Change the protocol of the current Request before redirecting.
 
.PARAMETER Address
Change the domain address of the current Request before redirecting.
 
.PARAMETER Moved
Set the Status Code as "301 Moved", rather than "302 Redirect".
 
.EXAMPLE
Move-PodeResponseUrl -Url 'https://google.com'
 
.EXAMPLE
Move-PodeResponseUrl -Url '/about'
 
.EXAMPLE
Move-PodeResponseUrl -Protocol HTTPS
 
.EXAMPLE
Move-PodeResponseUrl -Port 9000 -Moved
#>

function Move-PodeResponseUrl
{
    [CmdletBinding(DefaultParameterSetName='Url')]
    param(
        [Parameter(Mandatory=$true, ParameterSetName='Url')]
        [string]
        $Url,

        [Parameter(ParameterSetName='Endpoint')]
        [string]
        $EndpointName,

        [Parameter(ParameterSetName='Components')]
        [int]
        $Port = 0,

        [Parameter(ParameterSetName='Components')]
        [ValidateSet('', 'Http', 'Https')]
        [string]
        $Protocol,

        [Parameter(ParameterSetName='Components')]
        [string]
        $Address,

        [switch]
        $Moved
    )

    # build the url
    if ($PSCmdlet.ParameterSetName -ieq 'components') {
        $uri = $WebEvent.Request.Url

        # set the protocol
        $Protocol = $Protocol.ToLowerInvariant()
        if ([string]::IsNullOrWhiteSpace($Protocol)) {
            $Protocol = $uri.Scheme
        }

        # set the domain
        if ([string]::IsNullOrWhiteSpace($Address)) {
            $Address = $uri.Host
        }

        # set the port
        if ($Port -le 0) {
            $Port = $uri.Port
        }

        $PortStr = [string]::Empty
        if (@(80, 443) -notcontains $Port) {
            $PortStr = ":$($Port)"
        }

        # combine to form the url
        $Url = "$($Protocol)://$($Address)$($PortStr)$($uri.PathAndQuery)"
    }

    # build the url from an endpoint
    elseif ($PSCmdlet.ParameterSetName -ieq 'endpoint') {
        $endpoint = Get-PodeEndpointByName -Name $EndpointName -ThrowError

        # set the port
        $PortStr = [string]::Empty
        if (@(80, 443) -notcontains $endpoint.Port) {
            $PortStr = ":$($endpoint.Port)"
        }

        $Url = "$($endpoint.Protocol)://$($endpoint.FriendlyName)$($PortStr)$($WebEvent.Request.Url.PathAndQuery)"
    }

    Set-PodeHeader -Name 'Location' -Value $Url

    if ($Moved) {
        Set-PodeResponseStatus -Code 301 -Description 'Moved'
    }
    else {
        Set-PodeResponseStatus -Code 302 -Description 'Redirect'
    }
}

<#
.SYNOPSIS
Writes data to a TCP socket stream.
 
.DESCRIPTION
Writes data to a TCP socket stream.
 
.PARAMETER Message
The message to write
 
.EXAMPLE
Write-PodeTcpClient -Message '250 OK'
#>

function Write-PodeTcpClient
{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true)]
        [string]
        $Message
    )

    $TcpEvent.Response.WriteLine($Message, $true)
}

<#
.SYNOPSIS
Reads data from a TCP socket stream.
 
.DESCRIPTION
Reads data from a TCP socket stream.
 
.PARAMETER Timeout
An optional Timeout in milliseconds.
 
.PARAMETER CheckBytes
An optional array of bytes to check at the end of a receievd data stream, to determine if the data is complete.
 
.PARAMETER CRLFMessageEnd
If supplied, the CheckBytes will be set to 13 and 10 to make sure a message ends with CR and LF.
 
.EXAMPLE
$data = Read-PodeTcpClient
 
.EXAMPLE
$data = Read-PodeTcpClient -CRLFMessageEnd
#>

function Read-PodeTcpClient
{
    [CmdletBinding(DefaultParameterSetName='default')]
    [OutputType([string])]
    param(
        [Parameter()]
        [int]
        $Timeout = 0,

        [Parameter(ParameterSetName='CheckBytes')]
        [byte[]]
        $CheckBytes = $null,

        [Parameter(ParameterSetName='CRLF')]
        [switch]
        $CRLFMessageEnd
    )

    $cBytes = $CheckBytes
    if ($CRLFMessageEnd) {
        $cBytes = [byte[]]@(13, 10)
    }

    return (Wait-PodeTask -Task $TcpEvent.Request.Read($cBytes, $PodeContext.Tokens.Cancellation.Token) -Timeout $Timeout)
}

<#
.SYNOPSIS
Close an open TCP client connection
 
.DESCRIPTION
Close an open TCP client connection
 
.EXAMPLE
Close-PodeTcpClient
#>

function Close-PodeTcpClient
{
    [CmdletBinding()]
    param()

    $TcpEvent.Request.Close()
}

<#
.SYNOPSIS
Saves an uploaded file on the Request to the File System.
 
.DESCRIPTION
Saves an uploaded file on the Request to the File System.
 
.PARAMETER Key
The name of the key within the web event's Data HashTable that stores the file's name.
 
.PARAMETER Path
The path to save files.
 
.EXAMPLE
Save-PodeRequestFile -Key 'avatar'
 
.EXAMPLE
Save-PodeRequestFile -Key 'avatar' -Path 'F:/Images'
#>

function Save-PodeRequestFile
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Key,

        [Parameter()]
        [string]
        $Path = '.'
    )

    # if path is '.', replace with server root
    $Path = Get-PodeRelativePath -Path $Path -JoinRoot

    # ensure the parameter name exists in data
    $fileName = $WebEvent.Data[$Key]
    if ([string]::IsNullOrWhiteSpace($fileName)) {
        throw "A parameter called '$($Key)' was not supplied in the request"
    }

    # ensure the file data exists
    if (!$WebEvent.Files.ContainsKey($fileName)) {
        throw "No data for file '$($fileName)' was uploaded in the request"
    }

    # if the path is a directory, add the filename
    if (Test-PodePathIsDirectory -Path $Path) {
        $Path = [System.IO.Path]::Combine($Path, $fileName)
    }

    # save the file
    $WebEvent.Files[$fileName].Save($Path)
}

<#
.SYNOPSIS
Short description
 
.DESCRIPTION
Long description
 
.PARAMETER Type
The type name of the view engine (inbuilt types are: Pode and HTML).
 
.PARAMETER ScriptBlock
A ScriptBlock for specifying custom view engine rendering rules.
 
.PARAMETER Extension
A custom extension for the engine's files.
 
.EXAMPLE
Set-PodeViewEngine -Type HTML
 
.EXAMPLE
Set-PodeViewEngine -Type Markdown
 
.EXAMPLE
Set-PodeViewEngine -Type PSHTML -Extension PS1 -ScriptBlock { param($path, $data) /* logic */ }
#>

function Set-PodeViewEngine
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]
        $Type,

        [Parameter()]
        [scriptblock]
        $ScriptBlock = $null,

        [Parameter()]
        [string]
        $Extension
    )

    # truncate markdown
    if ($Type -ieq 'Markdown') {
        $Type = 'md'
    }

    # override extension with type
    if ([string]::IsNullOrWhiteSpace($Extension)) {
        $Extension = $Type
    }

    # check if the scriptblock has any using vars
    if ($null -ne $ScriptBlock) {
        $ScriptBlock, $usingVars = Invoke-PodeUsingScriptConversion -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $ScriptBlock = Invoke-PodeStateScriptConversion -ScriptBlock $ScriptBlock
        $ScriptBlock = Invoke-PodeSessionScriptConversion -ScriptBlock $ScriptBlock
    }

    # setup view engine config
    $PodeContext.Server.ViewEngine.Type = $Type.ToLowerInvariant()
    $PodeContext.Server.ViewEngine.Extension = $Extension.ToLowerInvariant()
    $PodeContext.Server.ViewEngine.ScriptBlock = $ScriptBlock
    $PodeContext.Server.ViewEngine.UsingVariables = $usingVars
    $PodeContext.Server.ViewEngine.IsDynamic = (@('html', 'md') -inotcontains $Type)
}

<#
.SYNOPSIS
Includes the contents of a partial View into another dynamic View.
 
.DESCRIPTION
Includes the contents of a partial View into another dynamic View. The partial View can be static or dynamic.
 
.PARAMETER Path
The path to a partial View, relative to the "/views" directory. (Extension is optional).
 
.PARAMETER Data
Any dynamic data to supply to a dynamic partial View.
 
.PARAMETER Folder
If supplied, a custom views folder will be used.
 
.EXAMPLE
Use-PodePartialView -Path 'shared/footer'
#>

function Use-PodePartialView
{
    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]
        $Path,

        [Parameter()]
        $Data = @{},

        [Parameter()]
        [string]
        $Folder
    )

    # default data if null
    if ($null -eq $Data) {
        $Data = @{}
    }

    # add view engine extension
    $ext = Get-PodeFileExtension -Path $Path
    if ([string]::IsNullOrWhiteSpace($ext)) {
        $Path += ".$($PodeContext.Server.ViewEngine.Extension)"
    }

    # only look in the view directory
    $viewFolder = $PodeContext.Server.InbuiltDrives['views']
    if (![string]::IsNullOrWhiteSpace($Folder)) {
        $viewFolder = $PodeContext.Server.Views[$Folder]
    }

    $Path = [System.IO.Path]::Combine($viewFolder, $Path)

    # test the file path, and set status accordingly
    if (!(Test-PodePath $Path -NoStatus)) {
        throw "File not found at path: $($Path)"
    }

    # run any engine logic
    return (Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data)
}

<#
.SYNOPSIS
Broadcasts a message to connected WebSocket clients.
 
.DESCRIPTION
Broadcasts a message to all, or some, connected WebSocket clients. You can specify a path to send messages to, or a specific ClientId.
 
.PARAMETER Value
A String, PSObject, or HashTable value. For non-string values, they will be converted to JSON.
 
.PARAMETER Path
The Path of connected clients to send the message.
 
.PARAMETER ClientId
A specific ClientId of a connected client to send a message. Not currently used.
 
.PARAMETER Depth
The Depth to generate the JSON document - the larger this value the worse performance gets.
 
.PARAMETER Mode
The Mode to broadcast a message: Auto, Broadcast, Direct. (Default: Auto)
 
.PARAMETER IgnoreEvent
If supplied, if a SignalEvent is available it's data, such as path/clientId, will be ignored.
 
.EXAMPLE
Send-PodeSignal -Value @{ Message = 'Hello, world!' }
 
.EXAMPLE
Send-PodeSignal -Value @{ Data = @(123, 100, 101) } -Path '/response-charts'
#>

function Send-PodeSignal
{
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true)]
        $Value,

        [Parameter()]
        [string]
        $Path,

        [Parameter()]
        [string]
        $ClientId,

        [Parameter()]
        [int]
        $Depth = 10,

        [Parameter()]
        [ValidateSet('Auto', 'Broadcast', 'Direct')]
        [string]
        $Mode = 'Auto',

        [switch]
        $IgnoreEvent
    )

    # error if not configured
    if (!$PodeContext.Server.Signals.Enabled) {
        throw "WebSockets have not been configured to send signal messages"
    }

    # do nothing if no value
    if (($null -eq $Value) -or ([string]::IsNullOrEmpty($Value))) {
        return
    }

    # jsonify the value
    if ($Value -isnot [string]) {
        if ($Depth -le 0) {
            $Value = (ConvertTo-Json -InputObject $Value -Compress)
        }
        else {
            $Value = (ConvertTo-Json -InputObject $Value -Depth $Depth -Compress)
        }
    }

    # check signal event
    if (!$IgnoreEvent -and ($null -ne $SignalEvent)) {
        if ([string]::IsNullOrWhiteSpace($Path)) {
            $Path = $SignalEvent.Data.Path
        }

        if ([string]::IsNullOrWhiteSpace($ClientId)) {
            $ClientId = $SignalEvent.Data.ClientId
        }

        if (($Mode -ieq 'Auto') -and ($SignalEvent.Data.Direct -or ($SignalEvent.ClientId -ieq $SignalEvent.Data.ClientId))) {
            $Mode = 'Direct'
        }
    }

    # broadcast or direct?
    if ($Mode -iin @('Auto', 'Broadcast')) {
        $PodeContext.Server.Signals.Listener.AddServerSignal($Value, $Path, $ClientId)
    }
    else {
        $SignalEvent.Response.Write($Value)
    }
}

<#
.SYNOPSIS
Add a custom path that contains additional views.
 
.DESCRIPTION
Add a custom path that contains additional views.
 
.PARAMETER Name
The Name of the views folder.
 
.PARAMETER Source
The literal, or relative, path to the directory that contains views.
 
.EXAMPLE
Add-PodeViewFolder -Name 'assets' -Source './assets'
#>

function Add-PodeViewFolder
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true)]
        [string]
        $Source
    )

    # ensure the folder doesn't already exist
    if ($PodeContext.Server.Views.ContainsKey($Name)) {
        throw "The Views folder name already exists: $($Name)"
    }

    # ensure the path exists at server root
    $Source = Get-PodeRelativePath -Path $Source -JoinRoot
    if (!(Test-PodePath -Path $Source -NoStatus)) {
        throw "The Views path does not exist: $($Source)"
    }

    # setup a temp drive for the path
    $Source = New-PodePSDrive -Path $Source

    # add the route(s)
    Write-Verbose "Adding View Folder: [$($Name)] $($Source)"
    $PodeContext.Server.Views[$Name] = $Source
}