Tools/Responses.ps1
# write data to http response stream, using a specific content-type function Text { param ( [Parameter()] [Alias('v')] $Value, [Parameter()] [Alias('ctype', 'ct')] [string] $ContentType = 'text/plain', [Parameter()] [Alias('a')] [int] $MaxAge = 3600, [switch] [Alias('c')] $Cache ) # if there's nothing to write, return if (Test-Empty $Value) { return } # if the value isn't a string/byte[] then error $valueType = $Value.GetType().Name if (@('string', 'byte[]') -inotcontains $valueType) { throw "Value to write to stream must be a String or Byte[], but got: $($valueType)" } # if the response stream isn't writable, return $res = $WebEvent.Response if (($null -eq $res) -or (!$PodeContext.Server.IsServerless -and (($null -eq $res.OutputStream) -or !$res.OutputStream.CanWrite))) { 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("ddd, dd MMM yyyy HH:mm:ss 'GMT'")) } # 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 ($PodeContext.Server.IsServerless) { $res.Body = $Value } else { # convert string to bytes if ($valueType -ieq 'string') { $Value = ConvertFrom-PodeValueToBytes -Value $Value } # write the content to the response stream $res.ContentLength64 = $Value.Length try { $ms = New-Object -TypeName System.IO.MemoryStream $ms.Write($Value, 0, $Value.Length) $ms.WriteTo($res.OutputStream) $ms.Close() } catch { if ((Test-PodeValidNetworkFailure $_.Exception)) { return } $_.Exception | Out-Default throw } } } function File { param ( [Parameter(Mandatory=$true)] [ValidateNotNull()] [Alias('p')] [string] $Path, [Parameter()] [Alias('d')] $Data = @{}, [Parameter()] [Alias('ctype', 'ct')] [string] $ContentType = $null, [Parameter()] [Alias('a')] [int] $MaxAge = 3600, [switch] [Alias('c')] $Cache ) # 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))) { $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 = (coalesce $subExt $mainExt) $ContentType = (coalesce $ContentType (Get-PodeContentType -Extension $subExt)) Text -Value $content -ContentType $ContentType } # this is a static file else { if (Test-IsPSCore) { $content = (Get-Content -Path $Path -Raw -AsByteStream) } else { $content = (Get-Content -Path $Path -Raw -Encoding byte) } $ContentType = (coalesce $ContentType (Get-PodeContentType -Extension $mainExt)) Text -Value $content -ContentType $ContentType -MaxAge $MaxAge -Cache:$Cache } } function Attach { param ( [Parameter(Mandatory=$true)] [Alias('p')] [string] $Path ) # only attach files from public/static-route directories when path is relative $Path = (Get-PodeStaticRoutePath -Route $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 $WebEvent.Response.ContentType = (Get-PodeContentType -Extension $ext) Set-PodeHeader -Name 'Content-Disposition' -Value "attachment; filename=$($filename)" # if serverless, get the content raw and return if ($PodeContext.Server.IsServerless) { if (Test-IsPSCore) { $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.ContentLength64 = $fs.Length $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() while (($read = $fs.Read($buffer, 0, $buffer.Length)) -gt 0) { $WebEvent.Response.OutputStream.Write($buffer, 0, $read) } } } finally { dispose $fs } } function Save { param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [Alias('n')] [string] $Name, [Parameter()] [Alias('p')] [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[$Name] if ([string]::IsNullOrWhiteSpace($fileName)) { throw "A parameter called '$($Name)' 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 = Join-Path $Path $fileName } # save the file [System.IO.File]::WriteAllBytes($Path, $WebEvent.Files[$fileName].Bytes) } function Status { param ( [Parameter(Mandatory=$true)] [ValidateNotNull()] [Alias('c')] [int] $Code, [Parameter()] [Alias('d')] [string] $Description, [Parameter()] [Alias('e')] $Exception, [Parameter()] [Alias('ctype', 'ct')] [string] $ContentType = $null, [switch] [Alias('nopage')] $NoErrorPage ) # 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 } } function Redirect { param ( [Parameter()] [Alias('u')] [string] $Url, [Parameter()] [Alias('p')] [int] $Port = 0, [Parameter()] [ValidateSet('', 'HTTP', 'HTTPS')] [Alias('pr')] [string] $Protocol, [Parameter()] [Alias('e')] [string] $Endpoint, [switch] [Alias('m')] $Moved ) if (Test-Empty $Url) { $uri = $WebEvent.Request.Url # set the protocol $Protocol = $Protocol.ToLowerInvariant() if (Test-Empty $Protocol) { $Protocol = $uri.Scheme } # set the endpoint if (Test-Empty $Endpoint) { $Endpoint = $uri.Host } # set the port if ($Port -le 0) { $Port = $uri.Port } $PortStr = [string]::Empty if ($Port -ne 80 -and $Port -ne 443) { $PortStr = ":$($Port)" } # combine to form the url $Url = "$($Protocol)://$($Endpoint)$($PortStr)$($uri.PathAndQuery)" } Set-PodeHeader -Name 'Location' -Value $Url if ($Moved) { status 301 'Moved' } else { status 302 'Redirect' } } function Json { param ( [Parameter()] [Alias('v')] $Value, [switch] $File ) if ($File) { # test the file path, and set status accordingly if (!(Test-PodePath $Value)) { return } else { $Value = Get-PodeFileContent -Path $Value } } elseif (Test-Empty $Value) { $Value = '{}' } elseif ($Value -isnot 'string') { $Value = ($Value | ConvertTo-Json -Depth 10 -Compress) } Text -Value $Value -ContentType 'application/json' } function Csv { param ( [Parameter(Mandatory=$true)] [Alias('v')] $Value, [switch] $File ) if ($File) { # test the file path, and set status accordingly if (!(Test-PodePath $Value)) { return } else { $Value = Get-PodeFileContent -Path $Value } } elseif (Test-Empty $Value) { $Value = [string]::Empty } elseif ($Value -isnot 'string') { $Value = @(foreach ($v in $Value) { New-Object psobject -Property $v }) if (Test-IsPSCore) { $Value = ($Value | ConvertTo-Csv -Delimiter ',' -IncludeTypeInformation:$false) } else { $Value = ($Value | ConvertTo-Csv -Delimiter ',' -NoTypeInformation) } } Text -Value $Value -ContentType 'text/csv' } function Xml { param ( [Parameter(Mandatory=$true)] [Alias('v')] $Value, [switch] $File ) if ($File) { # test the file path, and set status accordingly if (!(Test-PodePath $Value)) { return } else { $Value = Get-PodeFileContent -Path $Value } } elseif (Test-Empty $value) { $Value = [string]::Empty } elseif ($Value -isnot 'string') { $Value = @(foreach ($v in $Value) { New-Object psobject -Property $v }) $Value = ($Value | ConvertTo-Xml -Depth 10 -As String -NoTypeInformation) } Text -Value $Value -ContentType 'text/xml' } function Html { param ( [Parameter(Mandatory=$true)] [Alias('v')] $Value, [switch] $File ) if ($File) { # test the file path, and set status accordingly if (!(Test-PodePath $Value)) { return } else { $Value = Get-PodeFileContent -Path $Value } } elseif (Test-Empty $value) { $Value = [string]::Empty } elseif ($Value -isnot 'string') { $Value = ($Value | ConvertTo-Html) } Text -Value $Value -ContentType 'text/html' } # include helper to import the content of a view into another view function Include { param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [Alias('p')] [string] $Path, [Parameter()] [Alias('d')] $Data = @{} ) # default data if null if ($null -eq $Data) { $Data = @{} } # add view engine extension $ext = Get-PodeFileExtension -Path $Path if (Test-Empty $ext) { $Path += ".$($PodeContext.Server.ViewEngine.Extension)" } # only look in the view directory $Path = (Join-Path $PodeContext.Server.InbuiltDrives['views'] $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) } function Show-PodeErrorPage { param ( [Parameter()] [int] $Code, [Parameter()] [string] $Description, [Parameter()] $Exception, [Parameter()] [string] $ContentType ) # error page info $errorPage = Find-PodeErrorPage -Code $Code -ContentType $ContentType # if no page found, return if (Test-Empty $errorPage) { return } # if exception trace showing enabled then build the exception details object $ex = $null if (!(Test-Empty $Exception) -and $PodeContext.Server.Web.ErrorPages.ShowExceptions) { $ex = @{ 'Message' = [System.Web.HttpUtility]::HtmlEncode($Exception.Exception.Message); 'StackTrace' = [System.Web.HttpUtility]::HtmlEncode($Exception.ScriptStackTrace); 'Line' = [System.Web.HttpUtility]::HtmlEncode($Exception.InvocationInfo.PositionMessage); 'Category' = [System.Web.HttpUtility]::HtmlEncode($Exception.CategoryInfo.ToString()); } } # setup the data object for dynamic pages $data = @{ 'Url' = (Get-PodeUrl); 'Status' = @{ 'Code' = $Code; 'Description' = $Description; }; 'Exception' = $ex; 'ContentType' = $errorPage.ContentType; } # write the error page to the stream File -Path $errorPage.Path -Data $data -ContentType $errorPage.ContentType } function View { param ( [Parameter(Mandatory=$true)] [ValidateNotNull()] [Alias('p')] $Path, [Parameter()] [Alias('d')] $Data = @{}, [switch] [Alias('fm')] $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 !(Test-Empty $WebEvent.Session.Data.Flash)) { $Data['flash'] = @{} foreach ($key in (flash keys)) { $Data.flash[$key] = (flash get $key) } } elseif (Test-Empty $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 directory $Path = (Join-Path $PodeContext.Server.InbuiltDrives['views'] $Path) # test the file path, and set status accordingly if (!(Test-PodePath $Path)) { return } # run any engine logic and render it html -Value (Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data) } function Close-PodeTcpConnection { param ( [Parameter()] $Client, [switch] $Quit ) if ($null -eq $Client) { $Client = $TcpEvent.Client } if ($null -ne $Client) { if ($Quit -and $Client.Connected) { tcp write '221 Bye' } dispose $Client -Close } } function Tcp { param ( [Parameter(Mandatory=$true)] [ValidateSet('write', 'read')] [Alias('a')] [string] $Action, [Parameter()] [Alias('m')] [string] $Message, [Parameter()] [Alias('c')] $Client ) # error if serverless Test-PodeIsServerless -FunctionName 'tcp' -ThrowError # use the main client if one isn't supplied if ($null -eq $Client) { $Client = $TcpEvent.Client } switch ($Action.ToLowerInvariant()) { 'write' { $encoder = New-Object System.Text.ASCIIEncoding $buffer = $encoder.GetBytes("$($Message)`r`n") $stream = $Client.GetStream() await $stream.WriteAsync($buffer, 0, $buffer.Length) $stream.Flush() } 'read' { $bytes = New-Object byte[] 8192 $encoder = New-Object System.Text.ASCIIEncoding $stream = $Client.GetStream() $bytesRead = (await $stream.ReadAsync($bytes, 0, 8192)) return $encoder.GetString($bytes, 0, $bytesRead) } } } |