Private/Helpers.ps1
using namespace Pode <# .SYNOPSIS Dynamically executes content as a Pode file, optionally passing data to it. .DESCRIPTION This function takes a string of content, which is expected to be PowerShell code, and optionally a hashtable of data. It constructs a script block that optionally includes a parameter declaration, and then executes this script block using the provided data. This is useful for dynamically generating content based on a template or script contained in a file or a string. .PARAMETER Content The PowerShell code as a string. This content is dynamically executed as a script block. It can include placeholders or logic that utilizes the passed data. .PARAMETER Data Optional hashtable of data that can be referenced within the content/script. This data is passed to the script block as parameters. .EXAMPLE $scriptContent = '"Hello, world! Today is $(Get-Date)"' ConvertFrom-PodeFile -Content $scriptContent This example will execute the content of the script and output "Hello, world! Today is [current date]". .EXAMPLE $template = '"Hello, $(Name)! Your balance is $$(Amount)"' $data = @{ Name = 'John Doe'; Amount = '100.50' } ConvertFrom-PodeFile -Content $template -Data $data This example demonstrates using the function with a data parameter to replace placeholders within the content. #> function ConvertFrom-PodeFile { param( [Parameter(Mandatory = $true)] [ValidateNotNull()] $Content, [Parameter()] $Data = @{} ) # if we have data, then setup the data param if ($null -ne $Data -and $Data.Count -gt 0) { $Content = "param(`$data)`nreturn `"$($Content -replace '"', '``"')`"" } else { $Content = "return `"$($Content -replace '"', '``"')`"" } # invoke the content as a script to generate the dynamic content return (Invoke-PodeScriptBlock -ScriptBlock ([scriptblock]::Create($Content)) -Arguments $Data -Return) } function Get-PodeViewEngineType { param( [Parameter(Mandatory = $true)] [string] $Path ) # work out the engine to use when parsing the file $type = $PodeContext.Server.ViewEngine.Type $ext = Get-PodeFileExtension -Path $Path -TrimPeriod if (![string]::IsNullOrWhiteSpace($ext) -and ($ext -ine $PodeContext.Server.ViewEngine.Extension)) { $type = $ext } return $type } function Get-PodeFileContentUsingViewEngine { param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter()] [hashtable] $Data ) # work out the engine to use when parsing the file $engine = Get-PodeViewEngineType -Path $Path # setup the content $content = [string]::Empty # run the relevant engine logic switch ($engine.ToLowerInvariant()) { 'html' { $content = Get-Content -Path $Path -Raw -Encoding utf8 } 'md' { $content = Get-Content -Path $Path -Raw -Encoding utf8 } 'pode' { $content = Get-Content -Path $Path -Raw -Encoding utf8 $content = ConvertFrom-PodeFile -Content $content -Data $Data } default { if ($null -ne $PodeContext.Server.ViewEngine.ScriptBlock) { $_args = @($Path) if (($null -ne $Data) -and ($Data.Count -gt 0)) { $_args = @($Path, $Data) } $content = (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.ViewEngine.ScriptBlock -Arguments $_args -UsingVariables $PodeContext.Server.ViewEngine.UsingVariables -Return -Splat) } } } return $content } function Get-PodeFileContent { param( [Parameter(Mandatory = $true)] [string] $Path ) return (Get-Content -Path $Path -Raw -Encoding utf8) } function Get-PodeType { param( [Parameter()] $Value ) if ($null -eq $Value) { return $null } $type = $Value.GetType() return @{ Name = $type.Name.ToLowerInvariant() BaseName = $type.BaseType.Name.ToLowerInvariant() } } function Get-PodePSVersionTable { return $PSVersionTable } function Test-PodeIsAdminUser { # check the current platform, if it's unix then return true if (Test-PodeIsUnix) { return $true } try { $principal = [System.Security.Principal.WindowsPrincipal]::new([System.Security.Principal.WindowsIdentity]::GetCurrent()) if ($null -eq $principal) { return $false } return $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } catch [exception] { Write-PodeHost 'Error checking user administrator priviledges' -ForegroundColor Red Write-PodeHost $_.Exception.Message -ForegroundColor Red return $false } } function Get-PodeHostIPRegex { param( [Parameter(Mandatory = $true)] [ValidateSet('Both', 'Hostname', 'IP')] [string] $Type ) $ip_rgx = '\[?([a-f0-9]*\:){1,}[a-f0-9]*((\d+\.){3}\d+)?\]?|((\d+\.){3}\d+)|\*|all' $host_rgx = '([a-z]|\*\.)(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])+' switch ($Type.ToLowerInvariant()) { 'both' { return "(?<host>($($ip_rgx)|$($host_rgx)))" } 'hostname' { return "(?<host>($($host_rgx)))" } 'ip' { return "(?<host>($($ip_rgx)))" } } } function Get-PodePortRegex { return '(?<port>\d+)' } function Get-PodeEndpointInfo { param( [Parameter()] [string] $Address, [switch] $AnyPortOnZero ) if ([string]::IsNullOrWhiteSpace($Address)) { return $null } $hostRgx = Get-PodeHostIPRegex -Type Both $portRgx = Get-PodePortRegex $cmbdRgx = "$($hostRgx)\:$($portRgx)" # validate that we have a valid ip/host:port address if (!(($Address -imatch "^$($cmbdRgx)$") -or ($Address -imatch "^$($hostRgx)[\:]{0,1}") -or ($Address -imatch "[\:]{0,1}$($portRgx)$"))) { throw ($PodeLocale.failedToParseAddressExceptionMessage -f $Address)#"Failed to parse '$($Address)' as a valid IP/Host:Port address" } # grab the ip address/hostname $_host = $Matches['host'] if ([string]::IsNullOrWhiteSpace($_host)) { $_host = '*' } # ensure we have a valid ip address/hostname if (!(Test-PodeIPAddress -IP $_host)) { throw ($PodeLocale.invalidIpAddressExceptionMessage -f $_host) #"The IP address supplied is invalid: $($_host)" } # grab the port $_port = $Matches['port'] if ([string]::IsNullOrWhiteSpace($_port)) { $_port = 0 } # ensure the port is valid if ($_port -lt 0) { throw ($PodeLocale.invalidPortExceptionMessage -f $_port)#"The port cannot be negative: $($_port)" } # return the info return @{ Host = $_host Port = (Resolve-PodeValue -Check ($AnyPortOnZero -and ($_port -eq 0)) -TrueValue '*' -FalseValue $_port) } } function Test-PodeIPAddress { param( [Parameter()] [string] $IP, [switch] $IPOnly ) if ([string]::IsNullOrWhiteSpace($IP) -or ($IP -iin @('*', 'all'))) { return $true } if ($IP -imatch "^$(Get-PodeHostIPRegex -Type Hostname)$") { return (!$IPOnly) } try { $null = [System.Net.IPAddress]::Parse($IP) return $true } catch [exception] { return $false } } function Test-PodeHostname { param( [Parameter()] [string] $Hostname ) return ($Hostname -imatch "^$(Get-PodeHostIPRegex -Type Hostname)$") } function ConvertTo-PodeIPAddress { param( [Parameter(Mandatory = $true)] [ValidateNotNull()] $Address ) return [System.Net.IPAddress]::Parse(([System.Net.IPEndPoint]$Address).Address.ToString()) } function Get-PodeIPAddressesForHostname { param( [Parameter(Mandatory = $true)] [string] $Hostname, [Parameter(Mandatory = $true)] [ValidateSet('All', 'IPv4', 'IPv6')] [string] $Type ) if (!(Test-PodeHostname -Hostname $Hostname)) { return $Hostname } # get the ip addresses for the hostname try { $ips = @([System.Net.Dns]::GetHostAddresses($Hostname)) } catch { return '127.0.0.1' } # return ips based on type switch ($Type.ToLowerInvariant()) { 'ipv4' { $ips = @(foreach ($ip in $ips) { if ($ip.AddressFamily -ieq 'InterNetwork') { $ip } }) } 'ipv6' { $ips = @(foreach ($ip in $ips) { if ($ip.AddressFamily -ieq 'InterNetworkV6') { $ip } }) } } return (@($ips)).IPAddressToString } function Test-PodeIPAddressLocal { param( [Parameter(Mandatory = $true)] [string] $IP ) return (@('127.0.0.1', '::1', '[::1]', '::ffff:127.0.0.1', 'localhost') -icontains $IP) } function Test-PodeIPAddressAny { param( [Parameter(Mandatory = $true)] [string] $IP ) return (@('0.0.0.0', '*', 'all', '::', '[::]') -icontains $IP) } function Test-PodeIPAddressLocalOrAny { param( [Parameter(Mandatory = $true)] [string] $IP ) return ((Test-PodeIPAddressLocal -IP $IP) -or (Test-PodeIPAddressAny -IP $IP)) } function Resolve-PodeIPDualMode { param( [Parameter()] [ipaddress] $IP ) # do nothing if IPv6Any if ($IP -eq [ipaddress]::IPv6Any) { return $IP } # check loopbacks if (($IP -eq [ipaddress]::Loopback) -and [System.Net.Sockets.Socket]::OSSupportsIPv6) { return @($IP, [ipaddress]::IPv6Loopback) } if ($IP -eq [ipaddress]::IPv6Loopback) { return @($IP, [ipaddress]::Loopback) } # if iIPv4, convert and return both if (($IP.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) -and [System.Net.Sockets.Socket]::OSSupportsIPv6) { return @($IP, $IP.MapToIPv6()) } # if IPv6, only convert if valid IPv4 if (($IP.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6) -and $IP.IsIPv4MappedToIPv6) { return @($IP, $IP.MapToIPv4()) } # just return the IP return $IP } function Get-PodeIPAddress { param( [Parameter()] [string] $IP, [switch] $DualMode ) # any address for IPv4 (or IPv6 for DualMode) if ([string]::IsNullOrWhiteSpace($IP) -or ($IP -iin @('*', 'all'))) { if ($DualMode) { return [System.Net.IPAddress]::IPv6Any } return [System.Net.IPAddress]::Any } # any address for IPv6 explicitly if ($IP -iin @('::', '[::]')) { return [System.Net.IPAddress]::IPv6Any } # localhost if ($IP -ieq 'localhost') { return [System.Net.IPAddress]::Loopback } # localhost IPv6 explicitly if ($IP -iin @('[::1]', '::1')) { return [System.Net.IPAddress]::IPv6Loopback } # hostname if ($IP -imatch "^$(Get-PodeHostIPRegex -Type Hostname)$") { return $IP } # raw ip return [System.Net.IPAddress]::Parse($IP) } function Test-PodeIPAddressInRange { param( [Parameter(Mandatory = $true)] $IP, [Parameter(Mandatory = $true)] $LowerIP, [Parameter(Mandatory = $true)] $UpperIP ) if ($IP.Family -ine $LowerIP.Family) { return $false } $valid = $true foreach ($i in 0..3) { if (($IP.Bytes[$i] -lt $LowerIP.Bytes[$i]) -or ($IP.Bytes[$i] -gt $UpperIP.Bytes[$i])) { $valid = $false break } } return $valid } function Test-PodeIPAddressIsSubnetMask { param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $IP ) return (($IP -split '/').Length -gt 1) } function Get-PodeSubnetRange { param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $SubnetMask ) # split for ip and number of 1 bits $split = $SubnetMask -split '/' if ($split.Length -le 1) { return $null } $ip_parts = $split[0] -isplit '\.' $bits = [int]$split[1] # generate the netmask $network = @('', '', '', '') $count = 0 foreach ($i in 0..3) { foreach ($b in 1..8) { $count++ if ($count -le $bits) { $network[$i] += '1' } else { $network[$i] += '0' } } } # covert netmask to bytes foreach ($i in 0..3) { $network[$i] = [Convert]::ToByte($network[$i], 2) } # calculate the bottom range $bottom = @(foreach ($i in 0..3) { [byte]([byte]$network[$i] -band [byte]$ip_parts[$i]) }) # calculate the range $range = @(foreach ($i in 0..3) { 256 + (-bnot [byte]$network[$i]) }) # calculate the top range $top = @(foreach ($i in 0..3) { [byte]([byte]$ip_parts[$i] + [byte]$range[$i]) }) return @{ 'Lower' = ($bottom -join '.') 'Upper' = ($top -join '.') 'Range' = ($range -join '.') 'Netmask' = ($network -join '.') 'IP' = ($ip_parts -join '.') } } function Get-PodeConsoleKey { if ([Console]::IsInputRedirected -or ![Console]::KeyAvailable) { return $null } return [Console]::ReadKey($true) } function Test-PodeTerminationPressed { param( [Parameter()] $Key = $null ) if ($PodeContext.Server.DisableTermination) { return $false } return (Test-PodeKeyPressed -Key $Key -Character 'c') } function Test-PodeRestartPressed { param( [Parameter()] $Key = $null ) return (Test-PodeKeyPressed -Key $Key -Character 'r') } function Test-PodeOpenBrowserPressed { param( [Parameter()] $Key = $null ) return (Test-PodeKeyPressed -Key $Key -Character 'b') } function Test-PodeKeyPressed { param( [Parameter()] $Key = $null, [Parameter(Mandatory = $true)] [string] $Character ) if ($null -eq $Key) { $Key = Get-PodeConsoleKey } return (($null -ne $Key) -and ($Key.Key -ieq $Character) -and (($Key.Modifiers -band [ConsoleModifiers]::Control) -or ((Test-PodeIsUnix) -and ($Key.Modifiers -band [ConsoleModifiers]::Shift)))) } function Close-PodeServerInternal { param( [switch] $ShowDoneMessage ) # ensure the token is cancelled if ($null -ne $PodeContext.Tokens.Cancellation) { Write-Verbose 'Cancelling main cancellation token' $PodeContext.Tokens.Cancellation.Cancel() } # stop all current runspaces Write-Verbose 'Closing runspaces' Close-PodeRunspace -ClosePool # stop the file monitor if it's running Write-Verbose 'Stopping file monitor' Stop-PodeFileMonitor try { # remove all the cancellation tokens Write-Verbose 'Disposing cancellation tokens' Close-PodeDisposable -Disposable $PodeContext.Tokens.Cancellation Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart # dispose mutex/semaphores Write-Verbose 'Diposing mutex and semaphores' Clear-PodeMutexes Clear-PodeSemaphores } catch { $_ | Out-Default } # remove all of the pode temp drives Write-Verbose 'Removing internal PSDrives' Remove-PodePSDrive if ($ShowDoneMessage -and ($PodeContext.Server.Types.Length -gt 0) -and !$PodeContext.Server.IsServerless) { Write-PodeHost $PodeLocale.doneMessage -ForegroundColor Green } } function New-PodePSDrive { param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter()] [string] $Name ) # if the path is a share, do nothing if ($Path.StartsWith('\\')) { return $Path } # if no name is passed, used a randomly generated one if ([string]::IsNullOrWhiteSpace($Name)) { $Name = "PodeDir$(New-PodeGuid)" } # if the path supplied doesn't exist, error if (!(Test-Path $Path)) { throw ($PodeLocale.pathNotExistExceptionMessage -f $Path)#"Path does not exist: $($Path)" } # resolve the path $Path = Get-PodeRelativePath -Path $Path -JoinRoot -Resolve # create the temp drive if (!(Test-PodePSDrive -Name $Name -Path $Path)) { $drive = (New-PSDrive -Name $Name -PSProvider FileSystem -Root $Path -Scope Global -ErrorAction Stop) } else { $drive = Get-PodePSDrive -Name $Name } # store internally, and return the drive's name if (!$PodeContext.Server.Drives.ContainsKey($drive.Name)) { $PodeContext.Server.Drives[$drive.Name] = $Path } return "$($drive.Name):$([System.IO.Path]::DirectorySeparatorChar)" } function Get-PodePSDrive { param( [Parameter(Mandatory = $true)] [string] $Name ) return (Get-PSDrive -Name $Name -PSProvider FileSystem -Scope Global -ErrorAction Ignore) } function Test-PodePSDrive { param( [Parameter(Mandatory = $true)] [string] $Name, [Parameter()] [string] $Path ) $drive = Get-PodePSDrive -Name $Name if ($null -eq $drive) { return $false } if (![string]::IsNullOrWhiteSpace($Path)) { return ($drive.Root -ieq $Path) } return $true } <# .SYNOPSIS Adds Pode PS drives to the session. .DESCRIPTION This function iterates through the keys of Pode drives stored in the `$PodeContext.Server.Drives` collection and creates corresponding PS drives using `New-PodePSDrive`. The drive paths are specified by the values associated with each key. .EXAMPLE Add-PodePSDrivesInternal # Creates Pode PS drives in the session based on the configured drive paths. .NOTES This is an internal function and may change in future releases of Pode. #> function Add-PodePSDrivesInternal { foreach ($key in $PodeContext.Server.Drives.Keys) { $null = New-PodePSDrive -Path $PodeContext.Server.Drives[$key] -Name $key } } <# .SYNOPSIS Imports other Pode modules into the session. .DESCRIPTION This function iterates through the paths of other Pode modules stored in the `$PodeContext.Server.Modules.Values` collection and imports them into the session. It uses the `-DisableNameChecking` switch to suppress name checking during module import. .EXAMPLE Import-PodeModulesInternal # Imports other Pode modules into the session. .NOTES This is an internal function and may change in future releases of Pode. #> function Import-PodeModulesInternal { # import other modules in the session foreach ($path in $PodeContext.Server.Modules.Values) { if (Test-Path $path) { $null = Import-Module $path -DisableNameChecking -Scope Global -ErrorAction Stop } } } <# .SYNOPSIS Creates and registers inbuilt PowerShell drives for the Pode server's default folders. .DESCRIPTION This function sets up inbuilt PowerShell drives for the Pode web server's default directories: views, public content, and error pages. For each of these directories, if the physical path exists on the server, a new PowerShell drive is created and mapped to this path. These drives provide an easy and consistent way to access server resources like views, static files, and custom error pages within the Pode application. The function leverages `$PodeContext` to access the server's configuration and to determine the paths for these default folders. If a folder's path exists, the function uses `New-PodePSDrive` to create a PowerShell drive for it and stores this drive in the server's `InbuiltDrives` dictionary, keyed by the folder type. .PARAMETER None .EXAMPLE Add-PodePSInbuiltDrive This example is typically called within the Pode server setup script or internally by the Pode framework to initialize the PowerShell drives for the server's default folders. .NOTES This is an internal function and may change in future releases of Pode. #> function Add-PodePSInbuiltDrive { # create drive for views, if path exists $path = (Join-PodeServerRoot -Folder $PodeContext.Server.DefaultFolders.Views) if (Test-Path $path) { $PodeContext.Server.InbuiltDrives[$PodeContext.Server.DefaultFolders.Views] = (New-PodePSDrive -Path $path) } # create drive for public content, if path exists $path = (Join-PodeServerRoot $PodeContext.Server.DefaultFolders.Public) if (Test-Path $path) { $PodeContext.Server.InbuiltDrives[$PodeContext.Server.DefaultFolders.Public] = (New-PodePSDrive -Path $path) } # create drive for errors, if path exists $path = (Join-PodeServerRoot $PodeContext.Server.DefaultFolders.Errors) if (Test-Path $path) { $PodeContext.Server.InbuiltDrives[$PodeContext.Server.DefaultFolders.Errors] = (New-PodePSDrive -Path $path) } } <# .SYNOPSIS Removes Pode PS drives from the session. .DESCRIPTION This function removes Pode PS drives from the session based on the specified drive name or pattern. If no specific name or pattern is provided, it removes all Pode PS drives by default. It uses `Get-PSDrive` to retrieve the drives and `Remove-PSDrive` to remove them. .PARAMETER Name The name or pattern of the Pode PS drives to remove. Defaults to 'PodeDir*'. .EXAMPLE Remove-PodePSDrive -Name 'myDir*' # Removes all PS drives with names matching the pattern 'myDir*'. .EXAMPLE Remove-PodePSDrive # Removes all Pode PS drives. .NOTES This is an internal function and may change in future releases of Pode. #> function Remove-PodePSDrive { [CmdletBinding()] param( $Name = 'PodeDir*' ) $null = Get-PSDrive -Name $Name | Remove-PSDrive } <# .SYNOPSIS Joins a folder and file path to the root path of the server. .DESCRIPTION This function combines a folder path, file path (optional), and the root path of the server to create a complete path. If the root path is not explicitly provided, it uses the default root path from the Pode context. .PARAMETER Folder The folder path to join. .PARAMETER FilePath The file path (optional) to join. If not provided, only the folder path is used. .PARAMETER Root The root path of the server. If not provided, the default root path from the Pode context is used. .OUTPUTS Returns the combined path as a string. .EXAMPLE Join-PodeServerRoot -Folder "uploads" -FilePath "document.txt" # Output: "/uploads/document.txt" This example combines the folder path "uploads" and the file path "document.txt" with the default root path from the Pode context. #> function Join-PodeServerRoot { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Folder, [Parameter()] [string] $FilePath, [Parameter()] [string] $Root ) # use the root path of the server if ([string]::IsNullOrWhiteSpace($Root)) { $Root = $PodeContext.Server.Root } # join the folder/file to the root path return [System.IO.Path]::Combine($Root, $Folder, $FilePath) } <# .SYNOPSIS Removes empty items (empty strings) from an array. .DESCRIPTION This function filters out empty items (empty strings) from an array. It returns a new array containing only non-empty items. .PARAMETER Array The array from which to remove empty items. .OUTPUTS Returns an array containing non-empty items. .EXAMPLE $myArray = "apple", "", "banana", "", "cherry" $filteredArray = Remove-PodeEmptyItemsFromArray -Array $myArray Write-PodeHost "Filtered array: $filteredArray" This example removes empty items from the array and displays the filtered array. #> function Remove-PodeEmptyItemsFromArray { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectComparisonWithNull', '')] [CmdletBinding()] [OutputType([System.Object[]])] param( [Parameter()] $Array ) if ($null -eq $Array) { return @() } return @( @($Array -ne ([string]::Empty)) -ne $null ) } <# .SYNOPSIS Retrieves the file extension from a given path. .DESCRIPTION This function extracts the file extension (including the period) from a specified path. Optionally, it can trim the period from the extension. .PARAMETER Path The path from which to extract the file extension. .PARAMETER TrimPeriod Switch parameter. If specified, trims the period from the file extension. .OUTPUTS Returns the file extension (with or without the period) as a string. .EXAMPLE Get-PodeFileExtension -Path "C:\MyFiles\document.txt" # Output: ".txt" Get-PodeFileExtension -Path "C:\MyFiles\document.txt" -TrimPeriod # Output: "txt" This example demonstrates how to retrieve the file extension with and without the period from a given path. #> function Get-PodeFileExtension { [CmdletBinding()] [OutputType([string])] param( [Parameter()] [string] $Path, [switch] $TrimPeriod ) # Get the file extension $ext = [System.IO.Path]::GetExtension($Path) # Trim the period if requested if ($TrimPeriod) { $ext = $ext.Trim('.') } return $ext } <# .SYNOPSIS Retrieves the file name from a given path. .DESCRIPTION This function extracts the file name (including the extension) or the file name without the extension from a specified path. .PARAMETER Path The path from which to extract the file name. .PARAMETER WithoutExtension Switch parameter. If specified, returns the file name without the extension. .OUTPUTS Returns the file name (with or without extension) as a string. .EXAMPLE Get-PodeFileName -Path "C:\MyFiles\document.txt" # Output: "document.txt" Get-PodeFileName -Path "C:\MyFiles\document.txt" -WithoutExtension # Output: "document" This example demonstrates how to retrieve the file name with and without the extension from a given path. .NOTES - If the path is a directory, the function returns the directory name. - Use this function to extract file names for further processing or display. #> function Get-PodeFileName { [CmdletBinding()] [OutputType([string])] param( [Parameter()] [string] $Path, [switch] $WithoutExtension ) if ($WithoutExtension) { return [System.IO.Path]::GetFileNameWithoutExtension($Path) } return [System.IO.Path]::GetFileName($Path) } <# .SYNOPSIS Tests whether an exception message indicates a valid network failure. .DESCRIPTION This function checks if an exception message contains specific phrases that commonly indicate network-related failures. It returns a boolean value indicating whether the exception message matches any of these network failure patterns. .PARAMETER Exception The exception object whose message needs to be tested. .OUTPUTS Returns $true if the exception message indicates a valid network failure, otherwise returns $false. .EXAMPLE $exception = [System.Exception]::new("The network name is no longer available.") $isNetworkFailure = Test-PodeValidNetworkFailure -Exception $exception Write-PodeHost "Is network failure: $isNetworkFailure" This example tests whether the exception message "The network name is no longer available." indicates a network failure. #> function Test-PodeValidNetworkFailure { [CmdletBinding()] [OutputType([bool])] param( [Parameter()] $Exception ) $msgs = @( '*network name is no longer available*', '*nonexistent network connection*', '*the response has completed*', '*broken pipe*' ) $match = @(foreach ($msg in $msgs) { if ($Exception.Message -ilike $msg) { $msg } })[0] return ($null -ne $match) } function ConvertFrom-PodeHeaderQValue { param( [Parameter()] [string] $Value ) process { $qs = [ordered]@{} # return if no value if ([string]::IsNullOrWhiteSpace($Value)) { return $qs } # split the values up $parts = @($Value -isplit ',').Trim() # go through each part and check its q-value foreach ($part in $parts) { # default of 1 if no q-value if ($part.IndexOf(';q=') -eq -1) { $qs[$part] = 1.0 continue } # parse for q-value $atoms = @($part -isplit ';q=') $qs[$atoms[0]] = [double]$atoms[1] } return $qs } } function Get-PodeAcceptEncoding { param( [Parameter()] [string] $AcceptEncoding, [switch] $ThrowError ) # return if no encoding if ([string]::IsNullOrWhiteSpace($AcceptEncoding)) { return [string]::Empty } # return empty if not compressing if (!$PodeContext.Server.Web.Compression.Enabled) { return [string]::Empty } # convert encoding form q-form $encodings = ConvertFrom-PodeHeaderQValue -Value $AcceptEncoding if ($encodings.Count -eq 0) { return [string]::Empty } # check the encodings for one that matches $normal = @('identity', '*') $valid = @() # build up supported and invalid foreach ($encoding in $encodings.Keys) { if (($encoding -iin $PodeContext.Server.Compression.Encodings) -or ($encoding -iin $normal)) { $valid += @{ Name = $encoding Value = $encodings[$encoding] } } } # if it's empty, just return empty if ($valid.Length -eq 0) { return [string]::Empty } # find the highest ranked match $found = @{} $failOnIdentity = $false foreach ($encoding in $valid) { if ($encoding.Value -gt $found.Value) { $found = $encoding } if (!$failOnIdentity -and ($encoding.Value -eq 0) -and ($encoding.Name -iin $normal)) { $failOnIdentity = $true } } # force found to identity/* if the 0 is not identity - meaning it's still allowed if (($found.Value -eq 0) -and !$failOnIdentity) { $found = @{ Name = 'identity' Value = 1.0 } } # return invalid, error, or return empty for idenity? if ($found.Value -eq 0) { if ($ThrowError) { throw (New-PodeRequestException -StatusCode 406) } } # else, we're safe if ($found.Name -iin $normal) { return [string]::Empty } if ($found.Name -ieq 'x-gzip') { return 'gzip' } return $found.Name } <# .SYNOPSIS Parses a range string and converts it into a hashtable array of start and end values. .DESCRIPTION This function takes a range string (typically used in HTTP headers) and extracts the relevant start and end values. It supports the 'bytes' unit and handles multiple ranges separated by commas. .PARAMETER Range The range string to parse. .PARAMETER ThrowError A switch parameter. If specified, the function throws an exception (HTTP status code 416) when encountering invalid range formats. .OUTPUTS An array of hashtables, each containing 'Start' and 'End' properties representing the parsed ranges. .EXAMPLE Get-PodeRange -Range 'bytes=100-200,300-400' # Returns an array of hashtables: # [ # @{ # Start = 100 # End = 200 # }, # @{ # Start = 300 # End = 400 # } # ] .NOTES This is an internal function and may change in future releases of Pode. #> function Get-PodeRange { [CmdletBinding()] [OutputType([hashtable[]])] param( [Parameter()] [string] $Range, [switch] $ThrowError ) # return if no ranges if ([string]::IsNullOrWhiteSpace($Range)) { return $null } # split on '=' $parts = @($Range -isplit '=').Trim() if (($parts.Length -le 1) -or ([string]::IsNullOrWhiteSpace($parts[1]))) { return $null } $unit = $parts[0] if ($unit -ine 'bytes') { if ($ThrowError) { throw (New-PodeRequestException -StatusCode 416) } return $null } # split on ',' $parts = @($parts[1] -isplit ',').Trim() # parse into From-To hashtable array $ranges = @() foreach ($atom in $parts) { if ($atom -inotmatch '(?<start>[\d]+){0,1}\s?\-\s?(?<end>[\d]+){0,1}') { if ($ThrowError) { throw (New-PodeRequestException -StatusCode 416) } return $null } $ranges += @{ Start = $Matches['start'] End = $Matches['end'] } } return $ranges } function Get-PodeTransferEncoding { param( [Parameter()] [string] $TransferEncoding, [switch] $ThrowError ) # return if no encoding if ([string]::IsNullOrWhiteSpace($TransferEncoding)) { return [string]::Empty } # convert encoding form q-form $encodings = ConvertFrom-PodeHeaderQValue -Value $TransferEncoding if ($encodings.Count -eq 0) { return [string]::Empty } # check the encodings for one that matches $normal = @('chunked', 'identity') $invalid = @() # if we see a supported one, return immediately. else build up invalid one foreach ($encoding in $encodings.Keys) { if ($encoding -iin $PodeContext.Server.Compression.Encodings) { if ($encoding -ieq 'x-gzip') { return 'gzip' } return $encoding } if ($encoding -iin $normal) { continue } $invalid += $encoding } # if we have any invalid, throw a 415 error if ($invalid.Length -gt 0) { if ($ThrowError) { throw (New-PodeRequestException -StatusCode 415) } return $invalid[0] } # else, we're safe return [string]::Empty } function Get-PodeEncodingFromContentType { param( [Parameter()] [string] $ContentType ) if ([string]::IsNullOrWhiteSpace($ContentType)) { return [System.Text.Encoding]::UTF8 } $parts = @($ContentType -isplit ';').Trim() foreach ($part in $parts) { if ($part.StartsWith('charset')) { return [System.Text.Encoding]::GetEncoding(($part -isplit '=')[1].Trim()) } } return [System.Text.Encoding]::UTF8 } function New-PodeRequestException { param( [Parameter(Mandatory = $true)] [int] $StatusCode ) $err = [System.Net.Http.HttpRequestException]::new() $err.Data.Add('PodeStatusCode', $StatusCode) return $err } function ConvertTo-PodeResponseContent { param( [Parameter()] $InputObject, [Parameter()] [string] $ContentType, [Parameter()] [int] $Depth = 10, [Parameter()] [string] $Delimiter = ',', [switch] $AsHtml ) # split for the main content type $ContentType = Split-PodeContentType -ContentType $ContentType # if there is no content-type then convert straight to string if ([string]::IsNullOrWhiteSpace($ContentType)) { return ([string]$InputObject) } # run action for the content type switch ($ContentType) { { $_ -match '^(.*\/)?(.*\+)?json$' } { if ($InputObject -isnot [string]) { if ($Depth -le 0) { return (ConvertTo-Json -InputObject $InputObject -Compress) } else { return (ConvertTo-Json -InputObject $InputObject -Depth $Depth -Compress) } } if ([string]::IsNullOrWhiteSpace($InputObject)) { return '{}' } } { $_ -match '^(.*\/)?(.*\+)?yaml$' } { if ($InputObject -isnot [string]) { if ($Depth -le 0) { return (ConvertTo-PodeYamlInternal -InputObject $InputObject ) } else { return (ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth ) } } if ([string]::IsNullOrWhiteSpace($InputObject)) { return '[]' } } { $_ -match '^(.*\/)?(.*\+)?xml$' } { if ($InputObject -isnot [string]) { $temp = @(foreach ($item in $InputObject) { [pscustomobject]$item }) return ($temp | ConvertTo-Xml -Depth $Depth -As String -NoTypeInformation) } if ([string]::IsNullOrWhiteSpace($InputObject)) { return [string]::Empty } } { $_ -ilike '*/csv' } { if ($InputObject -isnot [string]) { $temp = @(foreach ($item in $InputObject) { [pscustomobject]$item }) if (Test-PodeIsPSCore) { $temp = ($temp | ConvertTo-Csv -Delimiter $Delimiter -IncludeTypeInformation:$false) } else { $temp = ($temp | ConvertTo-Csv -Delimiter $Delimiter -NoTypeInformation) } return ($temp -join ([environment]::NewLine)) } if ([string]::IsNullOrWhiteSpace($InputObject)) { return [string]::Empty } } { $_ -ilike '*/html' } { if ($InputObject -isnot [string]) { return (($InputObject | ConvertTo-Html) -join ([environment]::NewLine)) } if ([string]::IsNullOrWhiteSpace($InputObject)) { return [string]::Empty } } { $_ -ilike '*/markdown' } { if ($AsHtml -and ($PSVersionTable.PSVersion.Major -ge 7)) { return ($InputObject | ConvertFrom-Markdown).Html } } } return ([string]$InputObject) } function ConvertFrom-PodeRequestContent { param( [Parameter()] $Request, [Parameter()] [string] $ContentType, [Parameter()] [string] $TransferEncoding ) # get the requests content type $ContentType = Split-PodeContentType -ContentType $ContentType # result object for data/files $Result = @{ Data = @{} Files = @{} } # if there is no content-type then do nothing if ([string]::IsNullOrWhiteSpace($ContentType)) { return $Result } # if the content-type is not multipart/form-data, get the string data if ($ContentType -ine 'multipart/form-data') { # get the content based on server type if ($PodeContext.Server.IsServerless) { switch ($PodeContext.Server.ServerlessType.ToLowerInvariant()) { 'awslambda' { $Content = $Request.body } 'azurefunctions' { $Content = $Request.RawBody } } } else { # if the request is compressed, attempt to uncompress it if (![string]::IsNullOrWhiteSpace($TransferEncoding)) { # create a compressed stream to decompress the req bytes $ms = [System.IO.MemoryStream]::new() $ms.Write($Request.RawBody, 0, $Request.RawBody.Length) $null = $ms.Seek(0, 0) $stream = Get-PodeCompressionStream -InputStream $ms -Encoding $TransferEncoding -Mode Decompress # read the decompressed bytes $Content = Read-PodeStreamToEnd -Stream $stream -Encoding $Request.ContentEncoding } else { $Content = $Request.Body } } # if there is no content then do nothing if ([string]::IsNullOrWhiteSpace($Content)) { return $Result } # check if there is a defined custom body parser if ($PodeContext.Server.BodyParsers.ContainsKey($ContentType)) { $parser = $PodeContext.Server.BodyParsers[$ContentType] $Result.Data = (Invoke-PodeScriptBlock -ScriptBlock $parser.ScriptBlock -Arguments $Content -UsingVariables $parser.UsingVariables -Return) $Content = $null return $Result } } # run action for the content type switch ($ContentType) { { $_ -ilike '*/json' } { if (Test-PodeIsPSCore) { $Result.Data = ($Content | ConvertFrom-Json -AsHashtable) } else { $Result.Data = ($Content | ConvertFrom-Json) } } { $_ -ilike '*/xml' } { $Result.Data = [xml]($Content) } { $_ -ilike '*/csv' } { $Result.Data = ($Content | ConvertFrom-Csv) } { $_ -ilike '*/x-www-form-urlencoded' } { $Result.Data = (ConvertFrom-PodeNameValueToHashTable -Collection ([System.Web.HttpUtility]::ParseQueryString($Content))) } { $_ -ieq 'multipart/form-data' } { # parse multipart form data $form = $null if ($PodeContext.Server.IsServerless) { switch ($PodeContext.Server.ServerlessType.ToLowerInvariant()) { 'awslambda' { $Content = $Request.body } 'azurefunctions' { $Content = $Request.Body } } $form = [PodeForm]::Parse($Content, $WebEvent.ContentType, [System.Text.Encoding]::UTF8) } else { $Request.ParseFormData() $form = $Request.Form } # set the files/data foreach ($file in $form.Files) { $Result.Files.Add($file.FileName, $file) } foreach ($item in $form.Data) { if ($item.IsSingular) { $Result.Data.Add($item.Key, $item.Values[0]) } else { $Result.Data.Add($item.Key, $item.Values) } } $form = $null } default { $Result.Data = $Content } } $Content = $null return $Result } <# .SYNOPSIS Extracts the base MIME type from a Content-Type string that may include additional parameters. .DESCRIPTION This function takes a Content-Type string as input and returns only the base MIME type by splitting the string at the semicolon (';') and trimming any excess whitespace. It is useful for handling HTTP headers or other contexts where Content-Type strings include parameters like charset, boundary, etc. .PARAMETER ContentType The Content-Type string from which to extract the base MIME type. This string can include additional parameters separated by semicolons. .EXAMPLE Split-PodeContentType -ContentType "text/html; charset=UTF-8" This example returns 'text/html', stripping away the 'charset=UTF-8' parameter. .EXAMPLE Split-PodeContentType -ContentType "application/json; charset=utf-8" This example returns 'application/json', removing the charset parameter. #> function Split-PodeContentType { param( [Parameter()] [string] $ContentType ) # Check if the input string is null, empty, or consists only of whitespace. if ([string]::IsNullOrWhiteSpace($ContentType)) { return [string]::Empty # Return an empty string if the input is not valid. } # Split the Content-Type string by the semicolon, which separates the base MIME type from other parameters. # Trim any leading or trailing whitespace from the resulting MIME type to ensure clean output. return @($ContentType -isplit ';')[0].Trim() } function ConvertFrom-PodeNameValueToHashTable { param( [Parameter()] [System.Collections.Specialized.NameValueCollection] $Collection ) if ((Get-PodeCount -Object $Collection) -eq 0) { return @{} } $ht = @{} foreach ($key in $Collection.Keys) { $htKey = $key if (!$key) { $htKey = '' } $ht[$htKey] = $Collection.Get($key) } return $ht } <# .SYNOPSIS Gets the count of elements in the provided object or the length of a string. .DESCRIPTION This function returns the count of elements in various types of objects including strings, collections, and arrays. If the object is a string, it returns the length of the string. If the object is null or an empty collection, it returns 0. This function is useful for determining the size or length of data containers in PowerShell scripts. .PARAMETER Object The object from which the count or length will be determined. This can be a string, array, collection, or any other object that has a Count property. .OUTPUTS [int] Returns an integer representing the count of elements or length of the string. .EXAMPLE $array = @(1, 2, 3) Get-PodeCount -Object $array This example returns 3, as there are three elements in the array. .EXAMPLE $string = "hello" Get-PodeCount -Object $string This example returns 5, as there are five characters in the string. .EXAMPLE $nullObject = $null Get-PodeCount -Object $nullObject This example returns 0, as the object is null. #> function Get-PodeCount { [CmdletBinding()] [OutputType([int])] param( [Parameter()] $Object # The object to be evaluated for its count. ) # Check if the object is null. if ($null -eq $Object) { return 0 # Return 0 if the object is null. } # Check if the object is a string and return its length. if ($Object -is [string]) { return $Object.Length } # Check if the object is a NameValueCollection and is empty. if ($Object -is [System.Collections.Specialized.NameValueCollection] -and $Object.Count -eq 0) { return 0 # Return 0 if the collection is empty. } # For other types of collections, return their Count property. return $Object.Count } <# .SYNOPSIS Tests if a given file system path is valid and optionally if it is not a directory. .DESCRIPTION This function tests if the provided file system path is valid. It checks if the path is not null or whitespace, and if the item at the path exists. If the item exists and is not a directory (unless the $FailOnDirectory switch is not used), it returns true. If the path is not valid, it can optionally set a 404 response status code. .PARAMETER Path The file system path to test for validity. .PARAMETER NoStatus A switch to suppress setting the 404 response status code if the path is not valid. .PARAMETER FailOnDirectory A switch to indicate that the function should return false if the path is a directory. .PARAMETER Force A switch to indicate that the file with the hidden attribute has to be includede .PARAMETER ReturnItem Return the item file item itself instead of true or false .EXAMPLE $isValid = Test-PodePath -Path "C:\temp\file.txt" if ($isValid) { # The file exists and is not a directory } .EXAMPLE $isValid = Test-PodePath -Path "C:\temp\folder" -FailOnDirectory if (!$isValid) { # The path is a directory or does not exist } .NOTES This function is used within the Pode framework to validate file system paths for serving static content. #> function Test-PodePath { param( [Parameter()] $Path, [switch] $NoStatus, [switch] $FailOnDirectory, [switch] $Force, [switch] $ReturnItem ) $statusCode = 404 if (![string]::IsNullOrWhiteSpace($Path)) { try { $item = Get-Item $Path -Force:$Force -ErrorAction Stop if (($null -ne $item) -and (!$FailOnDirectory -or !$item.PSIsContainer)) { $statusCode = 200 } } catch [System.Management.Automation.ItemNotFoundException] { $statusCode = 404 } catch [System.UnauthorizedAccessException] { $statusCode = 401 } catch { $statusCode = 400 } } if ($statusCode -eq 200) { if ($ReturnItem.IsPresent) { return $item } return $true } # if we failed to get the file, report back the status code and/or return true/false if (!$NoStatus.IsPresent) { Set-PodeResponseStatus -Code $statusCode } if ($ReturnItem.IsPresent) { return $null } return $false } function Test-PodePathIsFile { param( [Parameter()] [string] $Path, [switch] $FailOnWildcard ) if ([string]::IsNullOrWhiteSpace($Path)) { return $false } if ($FailOnWildcard -and (Test-PodePathIsWildcard $Path)) { return $false } return (![string]::IsNullOrWhiteSpace([System.IO.Path]::GetExtension($Path))) } function Test-PodePathIsWildcard { param( [Parameter()] [string] $Path ) if ([string]::IsNullOrWhiteSpace($Path)) { return $false } return $Path.Contains('*') } function Test-PodePathIsDirectory { param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Path, [switch] $FailOnWildcard ) if ($FailOnWildcard -and (Test-PodePathIsWildcard $Path)) { return $false } return ([string]::IsNullOrWhiteSpace([System.IO.Path]::GetExtension($Path))) } function Convert-PodePathPatternToRegex { param( [Parameter()] [string] $Path, [switch] $NotSlashes, [switch] $NotStrict ) if (!$NotSlashes) { if ($Path -match '[\\/]\*$') { $Path = $Path -replace '[\\/]\*$', '/{0,1}*' } $Path = $Path -ireplace '[\\/]', '[\\/]' } $Path = $Path -ireplace '\.', '\.' $Path = $Path -ireplace '\*', '.*?' if ($NotStrict) { return $Path } return "^$($Path)$" } function Convert-PodePathPatternsToRegex { param( [Parameter()] [string[]] $Paths, [switch] $NotSlashes, [switch] $NotStrict ) # replace certain chars $Paths = @(foreach ($path in $Paths) { if (![string]::IsNullOrEmpty($path)) { Convert-PodePathPatternToRegex -Path $path -NotStrict -NotSlashes:$NotSlashes } }) # if no paths, return null if (($null -eq $Paths) -or ($Paths.Length -eq 0)) { return $null } # join them all together $joined = "($($Paths -join '|'))" if ($NotStrict) { return $joined } return "^$($joined)$" } <# .SYNOPSIS Gets the default SSL protocol(s) based on the operating system. .DESCRIPTION This function determines the appropriate default SSL protocol(s) based on the operating system. On macOS, it returns TLS 1.2. On other platforms, it combines SSL 3.0 and TLS 1.2. .OUTPUTS A [System.Security.Authentication.SslProtocols] enum value representing the default SSL protocol(s). .EXAMPLE Get-PodeDefaultSslProtocol # Returns [System.Security.Authentication.SslProtocols]::Ssl3, [System.Security.Authentication.SslProtocols]::Tls12 (on non-macOS systems) # Returns [System.Security.Authentication.SslProtocols]::Tls12 (on macOS) .NOTES This is an internal function and may change in future releases of Pode. #> function Get-PodeDefaultSslProtocol { [CmdletBinding()] [OutputType([System.Security.Authentication.SslProtocols])] param() if (Test-PodeIsMacOS) { return (ConvertTo-PodeSslProtocol -Protocol Tls12) } return (ConvertTo-PodeSslProtocol -Protocol Ssl3, Tls12) } <# .SYNOPSIS Converts a string representation of SSL protocols to the corresponding SslProtocols enum value. .DESCRIPTION This function takes an array of SSL protocol strings (such as 'Tls', 'Tls12', etc.) and combines them into a single SslProtocols enum value. It's useful for configuring SSL/TLS settings in Pode or other PowerShell scripts. .PARAMETER Protocol An array of SSL protocol strings. Valid values are 'Ssl2', 'Ssl3', 'Tls', 'Tls11', 'Tls12', and 'Tls13'. .OUTPUTS A [System.Security.Authentication.SslProtocols] enum value representing the combined protocols. .EXAMPLE ConvertTo-PodeSslProtocol -Protocol 'Tls', 'Tls12' # Returns [System.Security.Authentication.SslProtocols]::Tls12 .NOTES This is an internal function and may change in future releases of Pode. #> function ConvertTo-PodeSslProtocol { [CmdletBinding()] [OutputType([System.Security.Authentication.SslProtocols])] param( [Parameter()] [ValidateSet('Ssl2', 'Ssl3', 'Tls', 'Tls11', 'Tls12', 'Tls13')] [string[]] $Protocol ) $protos = 0 foreach ($item in $Protocol) { $protos = [int]($protos -bor [System.Security.Authentication.SslProtocols]::$item) } return [System.Security.Authentication.SslProtocols]($protos) } <# .SYNOPSIS Retrieves details about the Pode module. .DESCRIPTION This function determines the relevant details of the Pode module. It first checks if the module is already imported. If so, it uses that module. Otherwise, it attempts to identify the module used for the 'engine' and retrieves its details. If there are multiple versions of the module, it selects the newest version. If no module is imported, it uses the latest installed version. .OUTPUTS A hashtable containing the module details. .EXAMPLE Get-PodeModuleInfo # Returns a hashtable with module details such as name, path, base path, data path, internal path, and whether it's in the system path. .NOTES This is an internal function and may change in future releases of Pode. #> function Get-PodeModuleInfo { [CmdletBinding()] [OutputType([hashtable])] param() # if there's 1 module imported already, use that $importedModule = @(Get-Module -Name Pode) if (($importedModule | Measure-Object).Count -eq 1) { return (Convert-PodeModuleInfo -Module @($importedModule)[0]) } # if there's none or more, attempt to get the module used for 'engine' try { $usedModule = (Get-Command -Name 'Set-PodeViewEngine').Module if (($usedModule | Measure-Object).Count -eq 1) { return (Convert-PodeModuleInfo -Module $usedModule) } } catch { $_ | Write-PodeErrorLog -Level Debug } # if there were multiple to begin with, use the newest version if (($importedModule | Measure-Object).Count -gt 1) { return (Convert-PodeModuleInfo -Module @($importedModule | Sort-Object -Property Version)[-1]) } # otherwise there were none, use the latest installed return (Convert-PodeModuleInfo -Module @(Get-Module -ListAvailable -Name Pode | Sort-Object -Property Version)[-1]) } <# .SYNOPSIS Converts Pode module details to a hashtable. .DESCRIPTION This function takes a Pode module and extracts relevant details such as name, path, base path, data path, internal path, and whether it's in the system path. .PARAMETER Module The Pode module to convert. .OUTPUTS A hashtable containing the module details. .EXAMPLE Convert-PodeModuleInfo -Module (Get-Module Pode) .NOTES This is an internal function and may change in future releases of Pode. #> function Convert-PodeModuleInfo { [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [psmoduleinfo] $Module ) $details = @{ Name = $Module.Name Path = $Module.Path BasePath = $Module.ModuleBase DataPath = (Find-PodeModuleFile -Module $Module -CheckVersion) InternalPath = $null InPath = (Test-PodeModuleInPath -Module $Module) } $details.InternalPath = $details.DataPath -ireplace 'Pode\.(ps[md]1)', 'Pode.Internal.$1' return $details } <# .SYNOPSIS Checks if a PowerShell module is located within the directories specified in the PSModulePath environment variable. .DESCRIPTION This function determines if the path of a provided PowerShell module starts with any path included in the system's PSModulePath environment variable. This is used to ensure that the module is being loaded from expected locations, which can be important for security and configuration verification. .PARAMETER Module The module to be checked. This should be a module info object, typically obtained via Get-Module or Import-Module. .OUTPUTS [bool] Returns $true if the module's path is under a path listed in PSModulePath, otherwise returns $false. .EXAMPLE $module = Get-Module -Name Pode Test-PodeModuleInPath -Module $module This example checks if the 'Pode' module is located within the paths specified by the PSModulePath environment variable. #> function Test-PodeModuleInPath { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory = $true)] [psmoduleinfo] $Module ) # Determine the path separator based on the operating system. $separator = if (Test-PodeIsUnix) { ':' } else { ';' } # Split the PSModulePath environment variable to get individual paths. $paths = @($env:PSModulePath -split $separator) # Check each path to see if the module's path starts with it. foreach ($path in $paths) { # Return true if the module is in one of the paths. if ($Module.Path.StartsWith($path)) { return $true } } # Return false if no matching path is found. return $false } <# .SYNOPSIS Retrieves a module and all of its recursive dependencies. .DESCRIPTION This function takes a PowerShell module as input and returns an array containing the module and all of its required dependencies, retrieved recursively. This is useful for understanding the full set of dependencies a module has. .PARAMETER Module The module for which to retrieve dependencies. This must be a valid PowerShell module object. .EXAMPLE $module = Get-Module -Name SomeModuleName $dependencies = Get-PodeModuleDependencyList -Module $module This example retrieves all dependencies for "SomeModuleName". .OUTPUTS Array[psmoduleinfo] Returns an array of psmoduleinfo objects, each representing a module in the dependency tree. #> function Get-PodeModuleDependencyList { param( [Parameter(Mandatory = $true)] [psmoduleinfo] $Module ) # Check if the module has any required modules (dependencies). if (!$Module.RequiredModules) { return $Module } # Initialize an array to hold all dependencies. $mods = @() # Iterate through each required module and recursively retrieve their dependencies. foreach ($mod in $Module.RequiredModules) { # Recursive call for each dependency. $mods += (Get-PodeModuleDependencyList -Module $mod) } # Return the list of all dependencies plus the original module. return ($mods + $module) } function Get-PodeModuleRootPath { return (Split-Path -Parent -Path $PodeContext.Server.PodeModule.Path) } function Get-PodeModuleMiscPath { return [System.IO.Path]::Combine((Get-PodeModuleRootPath), 'Misc') } function Get-PodeUrl { return "$($WebEvent.Endpoint.Protocol)://$($WebEvent.Endpoint.Address)$($WebEvent.Path)" } function Find-PodeErrorPage { param( [Parameter()] [int] $Code, [Parameter()] [string] $ContentType ) # if a defined content type is supplied, attempt to find an error page for that first if (![string]::IsNullOrWhiteSpace($ContentType)) { $path = Get-PodeErrorPage -Code $Code -ContentType $ContentType if (![string]::IsNullOrWhiteSpace($path)) { return @{ 'Path' = $path; 'ContentType' = $ContentType } } } # if a defined route error page content type is supplied, attempt to find an error page for that if (![string]::IsNullOrWhiteSpace($WebEvent.ErrorType)) { $path = Get-PodeErrorPage -Code $Code -ContentType $WebEvent.ErrorType if (![string]::IsNullOrWhiteSpace($path)) { return @{ 'Path' = $path; 'ContentType' = $WebEvent.ErrorType } } } # if route patterns have been defined, see if an error content type matches and attempt that if (!(Test-PodeIsEmpty $PodeContext.Server.Web.ErrorPages.Routes)) { # find type by pattern $matched = @(foreach ($key in $PodeContext.Server.Web.ErrorPages.Routes.Keys) { if ($WebEvent.Path -imatch $key) { $key } })[0] # if we have a match, see if a page exists if (!(Test-PodeIsEmpty $matched)) { $type = $PodeContext.Server.Web.ErrorPages.Routes[$matched] $path = Get-PodeErrorPage -Code $Code -ContentType $type if (![string]::IsNullOrWhiteSpace($path)) { return @{ 'Path' = $path; 'ContentType' = $type } } } } # if we're using strict typing, attempt that, if we have a content type if ($PodeContext.Server.Web.ErrorPages.StrictContentTyping -and ![string]::IsNullOrWhiteSpace($WebEvent.ContentType)) { $path = Get-PodeErrorPage -Code $Code -ContentType $WebEvent.ContentType if (![string]::IsNullOrWhiteSpace($path)) { return @{ 'Path' = $path; 'ContentType' = $WebEvent.ContentType } } } # if we have a default defined, attempt that if (!(Test-PodeIsEmpty $PodeContext.Server.Web.ErrorPages.Default)) { $path = Get-PodeErrorPage -Code $Code -ContentType $PodeContext.Server.Web.ErrorPages.Default if (![string]::IsNullOrWhiteSpace($path)) { return @{ 'Path' = $path; 'ContentType' = $PodeContext.Server.Web.ErrorPages.Default } } } # if there's still no error page, use default HTML logic $type = Get-PodeContentType -Extension 'html' $path = (Get-PodeErrorPage -Code $Code -ContentType $type) if (![string]::IsNullOrWhiteSpace($path)) { return @{ 'Path' = $path; 'ContentType' = $type } } return $null } function Get-PodeErrorPage { param( [Parameter()] [int] $Code, [Parameter()] [string] $ContentType ) # parse the passed content type $ContentType = Split-PodeContentType -ContentType $ContentType # object for the page path $path = $null # attempt to find a custom error page $path = Find-PodeCustomErrorPage -Code $Code -ContentType $ContentType # if there's no custom page found, attempt to find an inbuilt page if ([string]::IsNullOrWhiteSpace($path)) { $podeRoot = Get-PodeModuleMiscPath $path = Find-PodeFileForContentType -Path $podeRoot -Name 'default-error-page' -ContentType $ContentType -Engine 'pode' } # if there's no path found, or it's inaccessible, return null if (!(Test-PodePath $path -NoStatus)) { return $null } return $path } function Find-PodeCustomErrorPage { param( [Parameter()] [int] $Code, [Parameter()] [string] $ContentType ) # get the custom errors path $customErrPath = $PodeContext.Server.InbuiltDrives['errors'] # if there's no custom error path, return if ([string]::IsNullOrWhiteSpace($customErrPath)) { return $null } # retrieve a status code page $path = (Find-PodeFileForContentType -Path $customErrPath -Name "$($Code)" -ContentType $ContentType) if (![string]::IsNullOrWhiteSpace($path)) { return $path } # retrieve default page $path = (Find-PodeFileForContentType -Path $customErrPath -Name 'default' -ContentType $ContentType) if (![string]::IsNullOrWhiteSpace($path)) { return $path } # no file was found return $null } function Find-PodeFileForContentType { param( [Parameter()] [string] $Path, [Parameter()] [string] $Name, [Parameter()] [string] $ContentType, [Parameter()] [string] $Engine = $null ) # get all files at the path that start with the name $files = @(Get-ChildItem -Path ([System.IO.Path]::Combine($Path, "$($Name).*"))) # if there are no files, return if ($null -eq $files -or $files.Length -eq 0) { return $null } # filter the files by the view engine extension (but only if the current engine is dynamic - non-html) if ([string]::IsNullOrWhiteSpace($Engine) -and $PodeContext.Server.ViewEngine.IsDynamic) { $Engine = $PodeContext.Server.ViewEngine.Extension } $Engine = (Protect-PodeValue -Value $Engine -Default 'pode') if ($Engine -ine 'pode') { $Engine = "($($Engine)|pode)" } $engineFiles = @(foreach ($file in $files) { if ($file.Name -imatch "\.$($Engine)$") { $file } }) $files = @(foreach ($file in $files) { if ($file.Name -inotmatch "\.$($Engine)$") { $file } }) # only attempt static files if we still have files after any engine filtering if ($null -ne $files -and $files.Length -gt 0) { # get files of the format '<name>.<type>' $file = @(foreach ($f in $files) { if ($f.Name -imatch "^$($Name)\.(?<ext>.*?)$") { if (($ContentType -ieq (Get-PodeContentType -Extension $Matches['ext']))) { $f.FullName } } })[0] if (![string]::IsNullOrWhiteSpace($file)) { return $file } } # only attempt these formats if we have a files for the view engine if ($null -ne $engineFiles -and $engineFiles.Length -gt 0) { # get files of the format '<name>.<type>.<engine>' $file = @(foreach ($f in $engineFiles) { if ($f.Name -imatch "^$($Name)\.(?<ext>.*?)\.$($engine)$") { if ($ContentType -ieq (Get-PodeContentType -Extension $Matches['ext'])) { $f.FullName } } })[0] if (![string]::IsNullOrWhiteSpace($file)) { return $file } # get files of the format '<name>.<engine>' $file = @(foreach ($f in $engineFiles) { if ($f.Name -imatch "^$($Name)\.$($engine)$") { $f.FullName } })[0] if (![string]::IsNullOrWhiteSpace($file)) { return $file } } # no file was found return $null } function Get-PodeRelativePath { param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter()] [string] $RootPath, [switch] $JoinRoot, [switch] $Resolve, [switch] $TestPath ) # if the path is relative, join to root if flagged if ($JoinRoot -and ($Path -match '^\.{1,2}([\\\/]|$)')) { if ([string]::IsNullOrWhiteSpace($RootPath)) { $RootPath = $PodeContext.Server.Root } $Path = [System.IO.Path]::Combine($RootPath, $Path) } # if flagged, resolve the path if ($Resolve) { $_rawPath = $Path $Path = [System.IO.Path]::GetFullPath($Path.Replace('\', '/')) } # if flagged, test the path and throw error if it doesn't exist if ($TestPath -and !(Test-PodePath $Path -NoStatus)) { throw ($PodeLocale.pathNotExistExceptionMessage -f (Protect-PodeValue -Value $Path -Default $_rawPath))#"The path does not exist: $(Protect-PodeValue -Value $Path -Default $_rawPath)" } return $Path } <# .SYNOPSIS Retrieves files based on a wildcard pattern in a given path. .DESCRIPTION The `Get-PodeWildcardFile` function returns files from the specified path based on a wildcard pattern. You can customize the wildcard and provide an optional root path for relative paths. .PARAMETER Path Specifies the path to search for files. This parameter is mandatory. .PARAMETER Wildcard Specifies the wildcard pattern for file matching. Default is '*.*'. .PARAMETER RootPath Specifies an optional root path for relative paths. If provided, the function will join the root path with the specified path. .OUTPUTS Returns an array of file paths matching the wildcard pattern. .EXAMPLE # Example usage: $files = Get-PodeWildcardFile -Path '/path/to/files' -Wildcard '*.txt' # Returns an array of .txt files in the specified path. .NOTES This is an internal function and may change in future releases of Pode. #> function Get-PodeWildcardFile { [CmdletBinding()] [OutputType([object[]])] param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter()] [string] $Wildcard = '*.*', [Parameter()] [string] $RootPath ) # if the OriginalPath is a directory, add wildcard if (Test-PodePathIsDirectory -Path $Path) { $Path = [System.IO.Path]::Combine($Path, $Wildcard) } # if path has a *, assume wildcard if (Test-PodePathIsWildcard -Path $Path) { $Path = Get-PodeRelativePath -Path $Path -RootPath $RootPath -JoinRoot return @((Get-ChildItem $Path -Recurse -Force).FullName) } return $null } function Test-PodeIsServerless { param( [Parameter()] [string] $FunctionName, [switch] $ThrowError ) if ($PodeContext.Server.IsServerless -and $ThrowError) { throw ($PodeLocale.unsupportedFunctionInServerlessContextExceptionMessage -f $FunctionName) #"The $($FunctionName) function is not supported in a serverless context" } if (!$ThrowError) { return $PodeContext.Server.IsServerless } } function Get-PodeEndpointUrl { param( [Parameter()] $Endpoint ) # get the endpoint on which we're currently listening - use first http/https if there are many if ($null -eq $Endpoint) { $Endpoint = @($PodeContext.Server.Endpoints.Values | Where-Object { $_.Protocol -iin @('http', 'https') -and $_.Default })[0] if ($null -eq $Endpoint) { $Endpoint = @($PodeContext.Server.Endpoints.Values | Where-Object { $_.Protocol -iin @('http', 'https') })[0] } } $url = $Endpoint.Url if ([string]::IsNullOrWhiteSpace($url)) { $url = "$($Endpoint.Protocol)://$($Endpoint.FriendlyName):$($Endpoint.Port)" } return $url } function Get-PodeDefaultPort { param( [Parameter()] [ValidateSet('Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')] [string] $Protocol, [Parameter()] [ValidateSet('Implicit', 'Explicit')] [string] $TlsMode = 'Implicit', [switch] $Real ) # are we after the real default ports? if ($Real) { return (@{ Http = @{ Implicit = 80 } Https = @{ Implicit = 443 } Smtp = @{ Implicit = 25 } Smtps = @{ Implicit = 465; Explicit = 587 } Tcp = @{ Implicit = 9001 } Tcps = @{ Implicit = 9002; Explicit = 9003 } Ws = @{ Implicit = 80 } Wss = @{ Implicit = 443 } })[$Protocol.ToLowerInvariant()][$TlsMode.ToLowerInvariant()] } # if we running as iis, return the ASPNET port if ($PodeContext.Server.IsIIS) { return [int]$env:ASPNETCORE_PORT } # if we running as heroku, return the port if ($PodeContext.Server.IsHeroku) { return [int]$env:PORT } # otherwise, get the port for the protocol return (@{ Http = @{ Implicit = 8080 } Https = @{ Implicit = 8443 } Smtp = @{ Implicit = 25 } Smtps = @{ Implicit = 465; Explicit = 587 } Tcp = @{ Implicit = 9001 } Tcps = @{ Implicit = 9002; Explicit = 9003 } Ws = @{ Implicit = 9080 } Wss = @{ Implicit = 9443 } })[$Protocol.ToLowerInvariant()][$TlsMode.ToLowerInvariant()] } function Set-PodeServerHeader { param( [Parameter()] [string] $Type, [switch] $AllowEmptyType ) $name = 'Pode' if (![string]::IsNullOrWhiteSpace($Type) -or $AllowEmptyType) { $name += " - $($Type)" } Set-PodeHeader -Name 'Server' -Value $name } function Get-PodeHandler { param( [Parameter(Mandatory = $true)] [ValidateSet('Service', 'Smtp')] [string] $Type, [Parameter()] [string] $Name ) if ([string]::IsNullOrWhiteSpace($Name)) { return $PodeContext.Server.Handlers[$Type] } return $PodeContext.Server.Handlers[$Type][$Name] } function Convert-PodeFileToScriptBlock { param( [Parameter(Mandatory = $true)] [Alias('FilePath')] [string] $Path ) # resolve for relative path $Path = Get-PodeRelativePath -Path $Path -JoinRoot # if Path doesn't exist, error if (!(Test-PodePath -Path $Path -NoStatus)) { throw ($PodeLocale.pathNotExistExceptionMessage -f $Path) # "The Path supplied does not exist: $($Path)" } # if the path is a wildcard or directory, error if (!(Test-PodePathIsFile -Path $Path -FailOnWildcard)) { throw ($PodeLocale.invalidPathWildcardOrDirectoryExceptionMessage -f $Path) # "The Path supplied cannot be a wildcard or a directory: $($Path)" } return ([scriptblock](Use-PodeScript -Path $Path)) } function Convert-PodeQueryStringToHashTable { param( [Parameter()] [string] $Uri ) if ([string]::IsNullOrWhiteSpace($Uri)) { return @{} } $qmIndex = $Uri.IndexOf('?') if ($qmIndex -eq -1) { return @{} } if ($qmIndex -gt 0) { $Uri = $Uri.Substring($qmIndex) } $tmpQuery = [System.Web.HttpUtility]::ParseQueryString($Uri) return (ConvertFrom-PodeNameValueToHashTable -Collection $tmpQuery) } function Get-PodeAstFromFile { param( [Parameter(Mandatory = $true)] [Alias('FilePath')] [string] $Path ) if (!(Test-Path $Path)) { throw ($PodeLocale.pathNotExistExceptionMessage -f $Path) # "The Path supplied does not exist: $($Path)" } return [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$null, [ref]$null) } function Get-PodeFunctionsFromFile { param( [Parameter(Mandatory = $true)] [string] $FilePath ) $ast = Get-PodeAstFromFile -FilePath $FilePath return @(Get-PodeFunctionsFromAst -Ast $ast) } function Get-PodeFunctionsFromAst { param( [Parameter(Mandatory = $true)] [System.Management.Automation.Language.Ast] $Ast ) $funcs = @(($Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false))) return @(foreach ($func in $funcs) { # skip null if ($null -eq $func) { continue } # skip pode funcs if ($func.Name -ilike '*-Pode*') { continue } # definition $def = "$($func.Body)".Trim('{}').Trim() if (($null -ne $func.Parameters) -and ($func.Parameters.Count -gt 0)) { $def = "param($($func.Parameters.Name -join ','))`n$($def)" } # the found func @{ Name = $func.Name Definition = $def } }) } function Get-PodeFunctionsFromScriptBlock { param( [Parameter(Mandatory = $true)] [scriptblock] $ScriptBlock ) # functions that have been found $foundFuncs = @() # get each function in the callstack $callstack = Get-PSCallStack if ($callstack.Count -gt 3) { $callstack = ($callstack | Select-Object -Skip 4) $bindingFlags = [System.Reflection.BindingFlags]'NonPublic, Instance, Static' foreach ($call in $callstack) { $_funcContext = $call.GetType().GetProperty('FunctionContext', $bindingFlags).GetValue($call, $null) $_scriptBlock = $_funcContext.GetType().GetField('_scriptBlock', $bindingFlags).GetValue($_funcContext) $foundFuncs += @(Get-PodeFunctionsFromAst -Ast $_scriptBlock.Ast) } } # get each function from the main script $foundFuncs += @(Get-PodeFunctionsFromAst -Ast $ScriptBlock.Ast) # return the found functions return $foundFuncs } <# .SYNOPSIS Reads details from a web exception and returns relevant information. .DESCRIPTION The `Read-PodeWebExceptionInfo` function processes a web exception (either `WebException` or `HttpRequestException`) and extracts relevant details such as status code, status description, and response body. .PARAMETER ErrorRecord Specifies the error record containing the web exception. This parameter is mandatory. .OUTPUTS Returns a hashtable with the following keys: - `Status`: A nested hashtable with `Code` (status code) and `Description` (status description). - `Body`: The response body from the web exception. .EXAMPLE # Example usage: $errorRecord = Get-ErrorRecordFromWebException $details = Read-PodeWebExceptionInfo -ErrorRecord $errorRecord # Returns a hashtable with status code, description, and response body. .NOTES This is an internal function and may change in future releases of Pode #> function Read-PodeWebExceptionInfo { [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [System.Management.Automation.ErrorRecord] $ErrorRecord ) switch ($ErrorRecord) { { $_.Exception -is [System.Net.WebException] } { $stream = $_.Exception.Response.GetResponseStream() $stream.Position = 0 $body = [System.IO.StreamReader]::new($stream).ReadToEnd() $code = [int]$_.Exception.Response.StatusCode $desc = $_.Exception.Response.StatusDescription } { $_.Exception -is [System.Net.Http.HttpRequestException] } { $body = $_.ErrorDetails.Message $code = [int]$_.Exception.Response.StatusCode $desc = $_.Exception.Response.ReasonPhrase } default { #Exception is of an invalid type, should be either WebException or HttpRequestException throw ($PodeLocale.invalidWebExceptionTypeExceptionMessage -f ($_.Exception.GetType().Name)) } } return @{ Status = @{ Code = $code Description = $desc } Body = $body } } function Use-PodeFolder { param( [Parameter()] [string] $Path, [Parameter(Mandatory = $true)] [string] $DefaultPath ) # use default, or custom path if ([string]::IsNullOrWhiteSpace($Path)) { $Path = Join-PodeServerRoot -Folder $DefaultPath } else { $Path = Get-PodeRelativePath -Path $Path -JoinRoot } # fail if path not found if (!(Test-PodePath -Path $Path -NoStatus)) { throw ($PodeLocale.pathToLoadNotFoundExceptionMessage -f $DefaultPath, $Path) #"Path to load $($DefaultPath) not found: $($Path)" } # get .ps1 files and load them Get-ChildItem -Path $Path -Filter *.ps1 -Force -Recurse | ForEach-Object { Use-PodeScript -Path $_.FullName } } function Find-PodeModuleFile { param( [Parameter(Mandatory = $true, ParameterSetName = 'Name')] [string] $Name, [Parameter(Mandatory = $true, ParameterSetName = 'Module')] [psmoduleinfo] $Module, [switch] $ListAvailable, [switch] $DataOnly, [switch] $CheckVersion ) # get module and check psd1, then psm1 if ($null -eq $Module) { $Module = (Get-Module -Name $Name -ListAvailable:$ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1) } # if the path isn't already a psd1 do this $path = Join-Path $Module.ModuleBase "$($Module.Name).psd1" if (!(Test-Path $path)) { # if we only want a psd1, return null if ($DataOnly) { $path = $null } else { $path = $Module.Path } } # check the Version of the psd1 elseif ($CheckVersion) { $data = Import-PowerShellDataFile -Path $path -ErrorAction Stop $version = $null if (![version]::TryParse($data.ModuleVersion, [ref]$version)) { if ($DataOnly) { $path = $null } else { $path = $Module.Path } } } return $path } <# .SYNOPSIS Clears the inner keys of a hashtable. .DESCRIPTION This function takes a hashtable as input and clears the values associated with each inner key. If the input hashtable is empty or null, no action is taken. .PARAMETER InputObject The hashtable to process. .EXAMPLE $myHashtable = @{ 'Key1' = 'Value1' 'Key2' = 'Value2' } Clear-PodeHashtableInnerKey -InputObject $myHashtable # Clears the values associated with 'Key1' and 'Key2' in the hashtable. .NOTES This is an internal function and may change in future releases of Pode. #> function Clear-PodeHashtableInnerKey { param( [Parameter()] [hashtable] $InputObject ) if (Test-PodeIsEmpty $InputObject) { return } $InputObject.Keys.Clone() | ForEach-Object { $InputObject[$_].Clear() } } function Set-PodeCronInterval { param( [Parameter()] [hashtable] $Cron, [Parameter()] [string] $Type, [Parameter()] [int[]] $Value, [Parameter()] [int] $Interval ) if ($Interval -le 0) { return $false } if ($Value.Length -gt 1) { throw ($PodeLocale.singleValueForIntervalExceptionMessage -f $Type) #"You can only supply a single $($Type) value when using intervals" } if ($Value.Length -eq 1) { $Cron[$Type] = "$(@($Value)[0])" } $Cron[$Type] += "/$($Interval)" return ($Value.Length -eq 1) } function Test-PodeModuleInstalled { param( [Parameter(Mandatory = $true)] [string] $Name ) return ($null -ne (Get-Module -Name $Name -ListAvailable -ErrorAction Ignore -Verbose:$false)) } function Get-PodePlaceholderRegex { return '\:(?<tag>[\w]+)' } <# .SYNOPSIS Resolves placeholders in a given path using a specified regex pattern. .DESCRIPTION The `Resolve-PodePlaceholder` function replaces placeholders in the provided path with custom placeholders based on the specified regex pattern. You can customize the prepend and append strings for the new placeholders. Additionally, you can choose to escape slashes in the path. .PARAMETER Path Specifies the path to resolve. This parameter is mandatory. .PARAMETER Pattern Specifies the regex pattern for identifying placeholders. If not provided, the default placeholder regex pattern from `Get-PodePlaceholderRegex` is used. .PARAMETER Prepend Specifies the string to prepend to the new placeholders. Default is '(?<'. .PARAMETER Append Specifies the string to append to the new placeholders. Default is '>[^\/]+?)'. .PARAMETER Slashes If specified, escapes slashes in the path. .OUTPUTS Returns the resolved path with replaced placeholders. .EXAMPLE # Example usage: $originalPath = '/api/users/{id}' $resolvedPath = Resolve-PodePlaceholder -Path $originalPath # Returns '/api/users/(?<id>[^\/]+?)' with custom placeholders. .NOTES This is an internal function and may change in future releases of Pode. #> function Resolve-PodePlaceholder { param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter()] [string] $Pattern, [Parameter()] [string] $Prepend = '(?<', [Parameter()] [string] $Append = '>[^\/]+?)', [switch] $Slashes ) if ([string]::IsNullOrWhiteSpace($Pattern)) { $Pattern = Get-PodePlaceholderRegex } if ($Path -imatch $Pattern) { $Path = [regex]::Escape($Path) } if ($Slashes) { $Path = ($Path.TrimEnd('\/') -replace '(\\\\|\/)', '[\\\/]') $Path = "$($Path)[\\\/]" } return (Convert-PodePlaceholder -Path $Path -Pattern $Pattern -Prepend $Prepend -Append $Append) } <# .SYNOPSIS Converts placeholders in a given path using a specified regex pattern. .DESCRIPTION The `Convert-PodePlaceholder` function replaces placeholders in the provided path with custom placeholders based on the specified regex pattern. You can customize the prepend and append strings for the new placeholders. .PARAMETER Path Specifies the path to convert. This parameter is mandatory. .PARAMETER Pattern Specifies the regex pattern for identifying placeholders. If not provided, the default placeholder regex pattern from `Get-PodePlaceholderRegex` is used. .PARAMETER Prepend Specifies the string to prepend to the new placeholders. Default is '(?<'. .PARAMETER Append Specifies the string to append to the new placeholders. Default is '>[^\/]+?)'. .OUTPUTS Returns the path with replaced placeholders. .EXAMPLE # Example usage: $originalPath = '/api/users/{id}' $convertedPath = Convert-PodePlaceholder -Path $originalPath # Returns '/api/users/(?<id>[^\/]+?)' with custom placeholders. .NOTES This is an internal function and may change in future releases of Pode. #> function Convert-PodePlaceholder { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter()] [string] $Pattern, [Parameter()] [string] $Prepend = '(?<', [Parameter()] [string] $Append = '>[^\/]+?)' ) if ([string]::IsNullOrWhiteSpace($Pattern)) { $Pattern = Get-PodePlaceholderRegex } while ($Path -imatch $Pattern) { $Path = ($Path -ireplace $Matches[0], "$($Prepend)$($Matches['tag'])$($Append)") } return $Path } <# .SYNOPSIS Tests whether a given path contains a placeholder based on a specified regex pattern. .DESCRIPTION The `Test-PodePlaceholder` function checks if the provided path contains a placeholder by matching it against a regex pattern. Placeholders are typically used for dynamic values. .PARAMETER Path Specifies the path to test. This parameter is mandatory. .PARAMETER Placeholder Specifies the regex pattern for identifying placeholders. If not provided, the default placeholder regex pattern from `Get-PodePlaceholderRegex` is used. .OUTPUTS Returns `$true` if the path contains a placeholder; otherwise, returns `$false`. .EXAMPLE # Example usage: $isPlaceholder = Test-PodePlaceholder -Path '/api/users/{id}' # Returns $true because the path contains a placeholder. .NOTES This is an internal function and may change in future releases of Pode. #> function Test-PodePlaceholder { param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter()] [string] $Placeholder ) if ([string]::IsNullOrWhiteSpace($Placeholder)) { $Placeholder = Get-PodePlaceholderRegex } return ($Path -imatch $Placeholder) } <# .SYNOPSIS Retrieves the PowerShell module manifest object for the specified module. .DESCRIPTION This function constructs the path to a PowerShell module manifest file (.psd1) located in the parent directory of the script root. It then imports the module manifest file to access its properties and returns the manifest object. This can be useful for scripts that need to dynamically discover and utilize module metadata, such as version, dependencies, and exported functions. .PARAMETERS This function does not accept any parameters. .EXAMPLE $manifest = Get-PodeModuleManifest This example calls the `Get-PodeModuleManifest` function to retrieve the module manifest object and stores it in the variable `$manifest`. #> function Get-PodeModuleManifest { # Construct the path to the module manifest (.psd1 file) $moduleManifestPath = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Pode.psd1' # Import the module manifest to access its properties $moduleManifest = Import-PowerShellDataFile -Path $moduleManifestPath return $moduleManifest } <# .SYNOPSIS Tests the running PowerShell version for compatibility with Pode, identifying end-of-life (EOL) and untested versions. .DESCRIPTION The `Test-PodeVersionPwshEOL` function checks the current PowerShell version against a list of versions that were either supported or EOL at the time of the Pode release. It uses the module manifest to determine which PowerShell versions are considered EOL and which are officially supported. If the current version is EOL or was not tested with the current release of Pode, the function generates a warning. This function aids in maintaining best practices for using supported PowerShell versions with Pode. .PARAMETER ReportUntested If specified, the function will report if the current PowerShell version was not available and thus untested at the time of the Pode release. This is useful for identifying potential compatibility issues with newer versions of PowerShell. .OUTPUTS A hashtable containing two keys: - `eol`: A boolean indicating if the current PowerShell version was EOL at the time of the Pode release. - `supported`: A boolean indicating if the current PowerShell version was officially supported by Pode at the time of the release. .EXAMPLE Test-PodeVersionPwshEOL Checks the current PowerShell version against Pode's supported and EOL versions list. Outputs a warning if the version is EOL or untested, and returns a hashtable indicating the compatibility status. .EXAMPLE Test-PodeVersionPwshEOL -ReportUntested Similar to the basic usage, but also reports if the current PowerShell version was untested because it was not available at the time of the Pode release. .NOTES This function is part of the Pode module's utilities to ensure compatibility and encourage the use of supported PowerShell versions. #> function Test-PodeVersionPwshEOL { param( [switch] $ReportUntested ) $moduleManifest = Get-PodeModuleManifest if ($moduleManifest.ModuleVersion -eq '$version$') { return @{ eol = $false supported = $true } } $psVersion = $PSVersionTable.PSVersion $eolVersions = $moduleManifest.PrivateData.PwshVersions.Untested -split ',' $isEol = "$($psVersion.Major).$($psVersion.Minor)" -in $eolVersions if ($isEol) { # [WARNING] Pode version has not been tested on PowerShell version, as it is EOL Write-PodeHost ($PodeLocale.eolPowerShellWarningMessage -f $PodeVersion, $PSVersion) -ForegroundColor Yellow } $SupportedVersions = $moduleManifest.PrivateData.PwshVersions.Supported -split ',' $isSupported = "$($psVersion.Major).$($psVersion.Minor)" -in $SupportedVersions if ((! $isSupported) -and (! $isEol) -and $ReportUntested) { # [WARNING] Pode version has not been tested on PowerShell version, as it was not available when Pode was released Write-PodeHost ($PodeLocale.untestedPowerShellVersionWarningMessage -f $PodeVersion, $PSVersion) -ForegroundColor Yellow } return @{ eol = $isEol supported = $isSupported } } <# .SYNOPSIS creates a YAML description of the data in the object - based on https://github.com/Phil-Factor/PSYaml .DESCRIPTION This produces YAML from any object you pass to it. .PARAMETER Object The object that you want scripted out. This parameter accepts input via the pipeline. .PARAMETER Depth The depth that you want your object scripted to .EXAMPLE Get-PodeOpenApiDefinition|ConvertTo-PodeYaml #> function ConvertTo-PodeYaml { [CmdletBinding()] [OutputType([string])] param ( [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] [AllowNull()] $InputObject, [parameter()] [int] $Depth = 16 ) begin { $pipelineObject = @() } process { $pipelineObject += $_ } end { if ($pipelineObject.Count -gt 1) { $InputObject = $pipelineObject } if ($PodeContext.Server.Web.OpenApi.UsePodeYamlInternal) { return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine } if ($null -eq $PodeContext.Server.InternalCache.YamlModuleImported) { $PodeContext.Server.InternalCache.YamlModuleImported = ((Test-PodeModuleInstalled -Name 'PSYaml') -or (Test-PodeModuleInstalled -Name 'powershell-yaml')) } if ($PodeContext.Server.InternalCache.YamlModuleImported) { return ($InputObject | ConvertTo-Yaml) } else { return ConvertTo-PodeYamlInternal -InputObject $InputObject -Depth $Depth -NoNewLine } } } <# .SYNOPSIS Converts PowerShell objects into a YAML-formatted string. .DESCRIPTION This function takes PowerShell objects and converts them to a YAML string representation. It supports various data types including arrays, hashtables, strings, and more. The depth of conversion can be controlled, allowing for nested objects to be accurately represented. .PARAMETER InputObject The PowerShell object to convert to YAML. .PARAMETER Depth Specifies the maximum depth of object nesting to convert. Default is 10 levels deep. .PARAMETER NestingLevel Used internally to track the current depth of recursion. Generally not specified by the user. .PARAMETER NoNewLine If specified, suppresses the newline characters in the output to create a single-line string. .OUTPUTS System.String. Returns a string in YAML format. .EXAMPLE ConvertTo-PodeYamlInternal -InputObject $object Converts the object into a YAML string. .NOTES This is an internal function and may change in future releases of Pode. It converts only basic PowerShell types, such as strings, integers, booleans, arrays, hashtables, and ordered dictionaries into a YAML format. #> function ConvertTo-PodeYamlInternal { [CmdletBinding()] [OutputType([string])] param ( [parameter(Mandatory = $true)] [AllowNull()] $InputObject, [parameter()] [int] $Depth = 10, [parameter()] [int] $NestingLevel = 0, [parameter()] [switch] $NoNewLine ) #report the leaves in terms of object type if ($Depth -ilt $NestingLevel) { return '' } # if it is null return null If ( !($InputObject) ) { if ($InputObject -is [Object[]]) { return '[]' } else { return '' } } $padding = [string]::new(' ', $NestingLevel * 2) # lets just create our left-padding for the block try { $Type = $InputObject.GetType().Name # we start by getting the object's type if ($InputObject -is [object[]]) { #what it really is $Type = "$($InputObject.GetType().BaseType.Name)" } # Check for specific value types string if ($Type -ne 'String') { # prevent these values being identified as an object if ($InputObject -is [System.Collections.Specialized.OrderedDictionary]) { $Type = 'hashTable' } elseif ($Type -ieq 'List`1') { $Type = 'array' } elseif ($InputObject -is [array]) { $Type = 'array' } # whatever it thinks it is called elseif ($InputObject -is [hashtable] ) { $Type = 'hashTable' } # for our purposes it is a hashtable } $output += switch ($Type.ToLower()) { 'string' { $String = "$InputObject" if (($string -match '[\r\n]' -or $string.Length -gt 80) -and ($string -notlike 'http*')) { $multiline = [System.Text.StringBuilder]::new("|`n") $items = $string.Split("`n") for ($i = 0; $i -lt $items.Length; $i++) { $workingString = $items[$i] -replace '\r$' $length = $workingString.Length $index = 0 $wrap = 80 while ($index -lt $length) { $breakpoint = $wrap $linebreak = $false if (($length - $index) -gt $wrap) { $lastSpaceIndex = $workingString.LastIndexOf(' ', $index + $wrap, $wrap) if ($lastSpaceIndex -ne -1) { $breakpoint = $lastSpaceIndex - $index } else { $linebreak = $true $breakpoint-- } } else { $breakpoint = $length - $index } $null = $multiline.Append($padding).Append($workingString.Substring($index, $breakpoint).Trim()) if ($linebreak) { $null = $multiline.Append('\') } $index += $breakpoint if ($index -lt $length) { $null = $multiline.Append([System.Environment]::NewLine) } } if ($i -lt ($items.Length - 1)) { $null = $multiline.Append([System.Environment]::NewLine) } } $multiline.ToString().TrimEnd() break } else { if ($string -match '^[#\[\]@\{\}\!\*]') { "'$($string -replace '''', '''''')'" } else { $string } break } break } 'hashtable' { if ($InputObject.Count -gt 0 ) { $index = 0 $string = [System.Text.StringBuilder]::new() foreach ($item in $InputObject.Keys) { if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = "`n$padding" } $null = $string.Append( $NewPadding).Append( $item).Append(': ') if ($InputObject[$item] -is [System.ValueType]) { if ($InputObject[$item] -is [bool]) { $null = $string.Append($InputObject[$item].ToString().ToLower()) } else { $null = $string.Append($InputObject[$item]) } } else { if ($InputObject[$item] -is [string]) { $increment = 2 } else { $increment = 1 } $null = $string.Append((ConvertTo-PodeYamlInternal -InputObject $InputObject[$item] -Depth $Depth -NestingLevel ($NestingLevel + $increment))) } } $string.ToString() } else { '{}' } break } 'pscustomobject' { if ($InputObject.Count -gt 0 ) { $index = 0 $string = [System.Text.StringBuilder]::new() foreach ($item in ($InputObject | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name)) { if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = "`n$padding" } $null = $string.Append( $NewPadding).Append( $item).Append(': ') if ($InputObject.$item -is [System.ValueType]) { if ($InputObject.$item -is [bool]) { $null = $string.Append($InputObject.$item.ToString().ToLower()) } else { $null = $string.Append($InputObject.$item) } } else { if ($InputObject.$item -is [string]) { $increment = 2 } else { $increment = 1 } $null = $string.Append((ConvertTo-PodeYamlInternal -InputObject $InputObject.$item -Depth $Depth -NestingLevel ($NestingLevel + $increment))) } } $string.ToString() } else { '{}' } break } 'array' { $string = [System.Text.StringBuilder]::new() $index = 0 foreach ($item in $InputObject ) { if ($NoNewLine -and $index++ -eq 0) { $NewPadding = '' } else { $NewPadding = "`n$padding" } $null = $string.Append($NewPadding).Append('- ').Append((ConvertTo-PodeYamlInternal -InputObject $item -depth $Depth -NestingLevel ($NestingLevel + 1) -NoNewLine)) } $string.ToString() break } default { "'$InputObject'" } } return $Output } catch { $_ | Write-PodeErrorLog $_.Exception | Write-PodeErrorLog -CheckInnerException throw ($PodeLocale.scriptErrorExceptionMessage -f $_, $_.InvocationInfo.ScriptName, $_.InvocationInfo.Line.Trim(), $_.InvocationInfo.ScriptLineNumber, $_.InvocationInfo.OffsetInLine, $_.InvocationInfo.MyCommand, $type, $InputObject, $InputObject.GetType().Name, $InputObject.GetType().BaseType.Name) } } <# .SYNOPSIS Resolves various types of object arrays into PowerShell objects. .DESCRIPTION This function takes an input property and determines its type. It then resolves the property into a PowerShell object or an array of objects, depending on whether the property is a hashtable, array, or single object. .PARAMETER Property The property to be resolved. It can be a hashtable, an object array, or a single object. .RETURNS Returns a PowerShell object or an array of PowerShell objects, depending on the input property type. .EXAMPLE $result = Resolve-PodeObjectArray -Property $myProperty This example resolves the $myProperty into a PowerShell object or an array of objects. .NOTES This is an internal function and may change in future releases of Pode. #> function Resolve-PodeObjectArray { [CmdletBinding()] [OutputType([object[]])] [OutputType([psobject])] param ( [AllowNull()] [object] $Property ) # Check if the property is a hashtable if ($Property -is [hashtable]) { # If the hashtable has only one item, convert it to a PowerShell object if ($Property.Count -eq 1) { return [pscustomobject]$Property } else { # If the hashtable has more than one item, recursively resolve each item return @(foreach ($p in $Property) { Resolve-PodeObjectArray -Property $p }) } } # Check if the property is an array of objects elseif ($Property -is [object[]]) { # Recursively resolve each item in the array return @(foreach ($p in $Property) { Resolve-PodeObjectArray -Property $p }) } # Check if the property is already a PowerShell object elseif ($Property -is [psobject]) { return $Property } else { # For any other type, convert it to a PowerShell object return [pscustomobject]$Property } } <# .SYNOPSIS Creates a deep clone of a PSObject by serializing and deserializing the object. .DESCRIPTION The Copy-PodeObjectDeepClone function takes a PSObject as input and creates a deep clone of it. This is achieved by serializing the object using the PSSerializer class, and then deserializing it back into a new instance. This method ensures that nested objects, arrays, and other complex structures are copied fully, without sharing references between the original and the cloned object. .PARAMETER InputObject The PSObject that you want to deep clone. This object will be serialized and then deserialized to create a deep copy. .PARAMETER Depth Specifies the depth for the serialization. The depth controls how deeply nested objects and properties are serialized. The default value is 10. .INPUTS [PSObject] - The function accepts a PSObject to deep clone. .OUTPUTS [PSObject] - The function returns a new PSObject that is a deep clone of the original. .EXAMPLE $originalObject = [PSCustomObject]@{ Name = 'John Doe' Age = 30 Address = [PSCustomObject]@{ Street = '123 Main St' City = 'Anytown' Zip = '12345' } } $clonedObject = $originalObject | Copy-PodeObjectDeepClone -Deep 15 # The $clonedObject is now a deep clone of $originalObject. # Changes to $clonedObject will not affect $originalObject and vice versa. .NOTES This function uses the System.Management.Automation.PSSerializer class, which is available in PowerShell 5.1 and later versions. The default depth parameter is set to 10 to handle nested objects appropriately, but it can be customized via the -Deep parameter. This is an internal function and may change in future releases of Pode. #> function Copy-PodeObjectDeepClone { param ( [Parameter(Mandatory, ValueFromPipeline)] [PSObject]$InputObject, [Parameter()] [int]$Depth = 10 ) process { # Serialize the object to XML format using PSSerializer # The depth parameter controls how deeply nested objects are serialized $xmlSerializer = [System.Management.Automation.PSSerializer]::Serialize($InputObject, $Depth) # Deserialize the XML back into a new PSObject, creating a deep clone of the original return [System.Management.Automation.PSSerializer]::Deserialize($xmlSerializer) } } |