Private/Shared/Read-MenuSelection.ps1
|
function Read-MenuSelection { <# .SYNOPSIS Displays a menu of options and prompts the user to select one. .DESCRIPTION This function displays a numbered list of options and prompts the user to select one. It validates the selection and returns the selected option value or the value property if objects are provided. .PARAMETER Title The title/prompt to display before the menu options. If null/empty, no title is shown. .PARAMETER HelpText An array of help text lines to display after the title. .PARAMETER OptionsTitle Optional line of text to render before the options list. .PARAMETER Options An array of option values to display. Can be simple strings/values, or objects with 'label' and 'value' properties. When objects with label/value are provided, the label is displayed and the value is returned. .PARAMETER DefaultIndex The zero-based index of the default option (default: 0). .PARAMETER DefaultValue Alternative to DefaultIndex - specify the default value directly. If both are provided, DefaultValue takes precedence. .PARAMETER AllowManualEntry When set, adds an option [0] to allow manual entry. .PARAMETER ManualEntryPrompt The prompt to display when manual entry is selected (default: "Enter value"). .PARAMETER ManualEntryValidator A script block that validates manual entry input. Should return $true if valid, $false otherwise. The input value is passed as $args[0]. .PARAMETER ManualEntryErrorMessage The error message to display when manual entry validation fails. .PARAMETER IsRequired When set, the user must provide a value (cannot be empty). .PARAMETER RequiredMessage The error message to display when a required field is left empty. .PARAMETER EmptyMessage Message to display when the options array is empty. If set and options are empty, shows this message and falls back to manual entry. .PARAMETER Type The expected data type for validation: 'string' (default), 'number', 'guid', 'boolean', or 'array'. When AllowManualEntry is used, input will be validated against this type. For 'array', comma-separated input is parsed into an array. .PARAMETER IsSensitive When set, input is read securely using Read-Host -AsSecureString and the value is masked in display. .OUTPUTS Returns the selected option value. .EXAMPLE $selection = Read-MenuSelection -Title "Select IaC type:" -Options @("terraform", "bicep") -DefaultIndex 0 .EXAMPLE $subscriptions = @( @{ label = "Subscription 1 (sub-id-1)"; value = "sub-id-1" }, @{ label = "Subscription 2 (sub-id-2)"; value = "sub-id-2" } ) $selection = Read-MenuSelection -Title "Select subscription:" -Options $subscriptions -AllowManualEntry -ManualEntryPrompt "Enter subscription ID" #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [string] $Title, [Parameter(Mandatory = $false)] [string[]] $HelpText = @(), [Parameter(Mandatory = $false)] [string] $OptionsTitle = $null, [Parameter(Mandatory = $false)] [array] $Options = @(), [Parameter(Mandatory = $false)] [int] $DefaultIndex = 0, [Parameter(Mandatory = $false)] $DefaultValue = $null, [Parameter(Mandatory = $false)] [switch] $AllowManualEntry, [Parameter(Mandatory = $false)] [string] $ManualEntryPrompt = "Enter value", [Parameter(Mandatory = $false)] [scriptblock] $ManualEntryValidator = $null, [Parameter(Mandatory = $false)] [string] $ManualEntryErrorMessage = "Invalid input. Please try again.", [Parameter(Mandatory = $false)] [switch] $IsRequired, [Parameter(Mandatory = $false)] [string] $RequiredMessage = "This field is required. Please enter a value.", [Parameter(Mandatory = $false)] [string] $EmptyMessage = $null, [Parameter(Mandatory = $false)] [ValidateSet("string", "number", "guid", "boolean", "array")] [string] $Type = "string", [Parameter(Mandatory = $false)] [switch] $IsSensitive ) # Built-in type validators $typeValidators = @{ "guid" = { param($value) return $value -match "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" } "number" = { param($value) $intResult = 0 return [int]::TryParse($value, [ref]$intResult) } "boolean" = { param($value) $validBooleans = @('true', 'false', 'yes', 'no', '1', '0', 'y', 'n', 't', 'f') return $validBooleans -contains $value.ToString().ToLower() } "string" = { param($value) return $true } "array" = { param($value) return $true # Arrays are always valid as input } } $typeErrorMessages = @{ "guid" = "Invalid GUID format. Please enter a valid GUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)" "number" = "Invalid format. Please enter an integer number." "boolean" = "Invalid format. Please enter true or false." "string" = "Invalid input." "array" = "Invalid input." } # Function to convert value to appropriate type function ConvertTo-TypedValue { param($Value, $TargetType, $DefaultValue = $null) if ([string]::IsNullOrWhiteSpace($Value)) { return $DefaultValue } switch ($TargetType) { "number" { $intResult = 0 if ([int]::TryParse($Value, [ref]$intResult)) { return $intResult } return $Value } "boolean" { $valueStr = $Value.ToString().ToLower() return $valueStr -in @('true', 'yes', '1', 'y', 't') } "array" { # Parse comma-separated values into an array return @($Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) } default { return $Value } } } # Get the effective validator - use ManualEntryValidator if provided, otherwise use type validator $effectiveValidator = if ($null -ne $ManualEntryValidator) { $ManualEntryValidator } elseif ($Type -ne "string") { $typeValidators[$Type] } else { $null } # Get the effective error message $effectiveErrorMessage = if (-not [string]::IsNullOrWhiteSpace($ManualEntryErrorMessage) -and $ManualEntryErrorMessage -ne "Invalid input. Please try again.") { $ManualEntryErrorMessage } elseif ($Type -ne "string") { $typeErrorMessages[$Type] } else { $ManualEntryErrorMessage } # Helper function to read input (handles sensitive vs normal input) function Read-InputValue { param($Prompt, $Sensitive) if ($Sensitive) { $secureValue = Read-Host $Prompt -AsSecureString return [System.Runtime.InteropServices.Marshal]::PtrToStringAuto( [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureValue) ) } else { return Read-Host $Prompt } } # Helper function to mask sensitive values function Get-MaskedValue { param($Value) if ([string]::IsNullOrWhiteSpace($Value)) { return "(empty)" } $valueStr = $Value.ToString() if ($valueStr.Length -ge 8) { return $valueStr.Substring(0, 3) + "***" + $valueStr.Substring($valueStr.Length - 3) } else { return "********" } } # Helper function to get the value from an option (handles both simple values and label/value objects) function Get-OptionValue { param($Option) if ($Option -is [hashtable] -and $Option.ContainsKey('value')) { return $Option.value } elseif ($Option -is [PSCustomObject] -and $null -ne $Option.PSObject.Properties['value']) { return $Option.value } return $Option } # Helper function to get the label from an option function Get-OptionLabel { param($Option) if ($Option -is [hashtable] -and $Option.ContainsKey('label')) { return $Option.label } elseif ($Option -is [PSCustomObject] -and $null -ne $Option.PSObject.Properties['label']) { return $Option.label } return $Option.ToString() } # Determine if we have options to display $hasOptions = $null -ne $Options -and $Options.Count -gt 0 # If DefaultValue is provided and we have options, find its index if ($null -ne $DefaultValue -and $hasOptions) { for ($i = 0; $i -lt $Options.Count; $i++) { if ((Get-OptionValue -Option $Options[$i]) -eq $DefaultValue) { $DefaultIndex = $i break } } } # Display title if provided if (-not [string]::IsNullOrWhiteSpace($Title)) { Write-ToConsoleLog $Title -IsPrompt } # Display help text if provided foreach ($helpLine in $HelpText) { if (-not [string]::IsNullOrWhiteSpace($helpLine)) { Write-ToConsoleLog $helpLine -IsSelection } } # Display default value and required status if ($null -ne $DefaultValue -and -not [string]::IsNullOrWhiteSpace($DefaultValue)) { $displayDefault = if ($IsSensitive.IsPresent) { Get-MaskedValue -Value $DefaultValue } else { $DefaultValue } Write-ToConsoleLog "Default: $displayDefault" -Color Cyan -IsSelection } if ($IsRequired.IsPresent) { Write-ToConsoleLog "Required: Yes" -Color Yellow -IsSelection } # If no options, go directly to manual entry if (-not $hasOptions) { if (-not [string]::IsNullOrWhiteSpace($EmptyMessage)) { Write-ToConsoleLog $EmptyMessage -IsWarning } $result = $null do { $manualInput = Read-InputValue -Prompt $ManualEntryPrompt -Sensitive $IsSensitive.IsPresent if ([string]::IsNullOrWhiteSpace($manualInput)) { # For arrays, return empty array or default; for others return default if ($Type -eq "array") { if ($null -ne $DefaultValue -and $DefaultValue -is [System.Collections.IList]) { $result = $DefaultValue } else { $result = @() } } elseif ($null -ne $DefaultValue -and -not [string]::IsNullOrWhiteSpace($DefaultValue)) { $result = ConvertTo-TypedValue -Value $DefaultValue -TargetType $Type } } else { # Validate and convert if ($null -ne $effectiveValidator -and -not [string]::IsNullOrWhiteSpace($manualInput)) { if (-not (& $effectiveValidator $manualInput)) { Write-ToConsoleLog $effectiveErrorMessage -IsError $result = $null continue } } $result = ConvertTo-TypedValue -Value $manualInput -TargetType $Type -DefaultValue $DefaultValue } # Check required - for arrays, check if empty if ($IsRequired.IsPresent) { if ($Type -eq "array") { if ($null -eq $result -or $result.Count -eq 0) { Write-ToConsoleLog $RequiredMessage -IsError $result = $null } } elseif ([string]::IsNullOrWhiteSpace($result)) { Write-ToConsoleLog $RequiredMessage -IsError $result = $null } } } while ($IsRequired.IsPresent -and $null -eq $result) return $result } # Display options title if provided if (-not [string]::IsNullOrWhiteSpace($OptionsTitle)) { Write-ToConsoleLog $OptionsTitle -IsSelection } # Display options for ($i = 0; $i -lt $Options.Count; $i++) { $option = $Options[$i] $label = Get-OptionLabel -Option $option $value = Get-OptionValue -Option $option $isCurrent = ($null -ne $DefaultValue -and $value -eq $DefaultValue) -or ($null -eq $DefaultValue -and $i -eq $DefaultIndex) $currentMarker = if ($isCurrent) { " (current)" } else { "" } if ($isCurrent) { Write-ToConsoleLog "[$($i + 1)] $label$currentMarker" -IsSelection -Color Green -IndentLevel 1 } else { Write-ToConsoleLog "[$($i + 1)] $label" -IsSelection -IndentLevel 1 } } # Show manual entry option if allowed if ($AllowManualEntry.IsPresent) { Write-ToConsoleLog "[0] Enter manually" -IsSelection -IndentLevel 1 } # Build prompt text $promptText = "Enter selection (1-$($Options.Count)" if ($AllowManualEntry.IsPresent) { $promptText += ", 0 for manual entry" } $promptText += ", default: $($DefaultIndex + 1))" # Get selection $result = $null do { $selection = Read-InputValue -Prompt $promptText -Sensitive $IsSensitive.IsPresent if ([string]::IsNullOrWhiteSpace($selection)) { # Use default $result = Get-OptionValue -Option $Options[$DefaultIndex] } elseif ($AllowManualEntry.IsPresent -and $selection -eq "0") { # Manual entry do { $manualInput = Read-InputValue -Prompt $ManualEntryPrompt -Sensitive $IsSensitive.IsPresent if ([string]::IsNullOrWhiteSpace($manualInput) -and -not [string]::IsNullOrWhiteSpace($DefaultValue)) { $result = $DefaultValue break } if ($null -ne $effectiveValidator -and -not [string]::IsNullOrWhiteSpace($manualInput)) { if (-not (& $effectiveValidator $manualInput)) { Write-ToConsoleLog $effectiveErrorMessage -IsError continue } } $result = ConvertTo-TypedValue -Value $manualInput -TargetType $Type break } while ($true) } else { $selectedIndex = [int]$selection - 1 if ($selectedIndex -ge 0 -and $selectedIndex -lt $Options.Count) { $result = Get-OptionValue -Option $Options[$selectedIndex] } else { Write-ToConsoleLog "Invalid selection, please try again." -IsWarning continue } } # Check required if ($IsRequired.IsPresent -and [string]::IsNullOrWhiteSpace($result)) { Write-ToConsoleLog $RequiredMessage -IsError $result = $null } } while ($null -eq $result -and $IsRequired.IsPresent) return $result } # SIG # Begin signature block # MIIoUwYJKoZIhvcNAQcCoIIoRDCCKEACAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCByMSErcHx/F7v4 # xUVajKa+N197cJ9iUw37zC0UaPY8LqCCDYUwggYDMIID66ADAgECAhMzAAAEhJji # EuB4ozFdAAAAAASEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjUwNjE5MTgyMTM1WhcNMjYwNjE3MTgyMTM1WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDtekqMKDnzfsyc1T1QpHfFtr+rkir8ldzLPKmMXbRDouVXAsvBfd6E82tPj4Yz # aSluGDQoX3NpMKooKeVFjjNRq37yyT/h1QTLMB8dpmsZ/70UM+U/sYxvt1PWWxLj # MNIXqzB8PjG6i7H2YFgk4YOhfGSekvnzW13dLAtfjD0wiwREPvCNlilRz7XoFde5 # KO01eFiWeteh48qUOqUaAkIznC4XB3sFd1LWUmupXHK05QfJSmnei9qZJBYTt8Zh # ArGDh7nQn+Y1jOA3oBiCUJ4n1CMaWdDhrgdMuu026oWAbfC3prqkUn8LWp28H+2S # LetNG5KQZZwvy3Zcn7+PQGl5AgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUBN/0b6Fh6nMdE4FAxYG9kWCpbYUw # VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh # dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzUwNTM2MjAfBgNVHSMEGDAW # gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw # MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov # L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx # XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB # AGLQps1XU4RTcoDIDLP6QG3NnRE3p/WSMp61Cs8Z+JUv3xJWGtBzYmCINmHVFv6i # 8pYF/e79FNK6P1oKjduxqHSicBdg8Mj0k8kDFA/0eU26bPBRQUIaiWrhsDOrXWdL # m7Zmu516oQoUWcINs4jBfjDEVV4bmgQYfe+4/MUJwQJ9h6mfE+kcCP4HlP4ChIQB # UHoSymakcTBvZw+Qst7sbdt5KnQKkSEN01CzPG1awClCI6zLKf/vKIwnqHw/+Wvc # Ar7gwKlWNmLwTNi807r9rWsXQep1Q8YMkIuGmZ0a1qCd3GuOkSRznz2/0ojeZVYh # ZyohCQi1Bs+xfRkv/fy0HfV3mNyO22dFUvHzBZgqE5FbGjmUnrSr1x8lCrK+s4A+ # bOGp2IejOphWoZEPGOco/HEznZ5Lk6w6W+E2Jy3PHoFE0Y8TtkSE4/80Y2lBJhLj # 27d8ueJ8IdQhSpL/WzTjjnuYH7Dx5o9pWdIGSaFNYuSqOYxrVW7N4AEQVRDZeqDc # fqPG3O6r5SNsxXbd71DCIQURtUKss53ON+vrlV0rjiKBIdwvMNLQ9zK0jy77owDy # XXoYkQxakN2uFIBO1UNAvCYXjs4rw3SRmBX9qiZ5ENxcn/pLMkiyb68QdwHUXz+1 # fI6ea3/jjpNPz6Dlc/RMcXIWeMMkhup/XEbwu73U+uz/MIIHejCCBWKgAwIBAgIK # YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm # aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw # OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD # VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG # 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la # UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc # 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D # dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+ # lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk # kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6 # A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd # X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL # 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd # sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3 # T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS # 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI # bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL # BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD # uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv # c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf # MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf # MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF # BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h # cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA # YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn # 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7 # v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b # pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/ # KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy # CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp # mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi # hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb # BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS # oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL # gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX # cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGiQwghogAgEBMIGVMH4x # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p # Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAASEmOIS4HijMV0AAAAA # BIQwDQYJYIZIAWUDBAIBBQCggbAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw # HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIGnY # pf36Jd/eoAPithVR8hI2RVg04Mm3XdldZxLBnKCcMEQGCisGAQQBgjcCAQwxNjA0 # oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEcgBpodHRwczovL3d3dy5taWNyb3NvZnQu # Y29tIDANBgkqhkiG9w0BAQEFAASCAQBcy9M66pYrmfCqBtfDlSYww1HmlHVhfp/F # 5MEr7s32MKNZs09HcwZT75O+yD/Me+DNuEQYzS670qFUlEm97BXNb2W31G5Z5IBQ # t6+QOdXpEjtHqjNLZnOWPNUTdJ3Po7aK1FurW6QhKp73WlZWjblg1stSGxA9rpWL # jGY9Qu5UVows1VvpNU88Z3Aa4AMsKRoW+lIqumvUOBkwMZHKuMntUXRN0k87hcM+ # nDJFDXbY8jI5mcDSIC4rrd6wItwnGQ+0Ci/Z8JEh01RYUpcwbBaLo7tP7Rt+DoVh # zG0AIg4EiLMKnJWr5LcET6xzOAzZtguFrUq25mlWHKe+NzNrph7moYIXrDCCF6gG # CisGAQQBgjcDAwExgheYMIIXlAYJKoZIhvcNAQcCoIIXhTCCF4ECAQMxDzANBglg # hkgBZQMEAgEFADCCAVkGCyqGSIb3DQEJEAEEoIIBSASCAUQwggFAAgEBBgorBgEE # AYRZCgMBMDEwDQYJYIZIAWUDBAIBBQAEIAbw4gvj/oXUizrMqx0IMcFrm6LGEIvc # rgsG4tjEgb8bAgZpdCqXkawYEjIwMjYwMTMwMjAwNjEwLjk4WjAEgAIB9KCB2aSB # 1jCB0zELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UE # CxMkTWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMScwJQYDVQQL # Ex5uU2hpZWxkIFRTUyBFU046NTIxQS0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jv # c29mdCBUaW1lLVN0YW1wIFNlcnZpY2WgghH7MIIHKDCCBRCgAwIBAgITMwAAAhdx # +y6lrwEd6gABAAACFzANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFt # cCBQQ0EgMjAxMDAeFw0yNTA4MTQxODQ4MjNaFw0yNjExMTMxODQ4MjNaMIHTMQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNy # b3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGll # bGQgVFNTIEVTTjo1MjFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRp # bWUtU3RhbXAgU2VydmljZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB # AMDPNrBMPt/b2Ee4hgiBQ52DYUTPgcxdgGIquXVKWaSRT0YwnIqUOkVGyPkQGDUQ # KRMcecoK122lrbsL6rKgFUlm1P1un3nLo4yC5D6p/aZ4LVkZH0IG2IL2lwC5ej2N # sjh/X+9jA/ugMsUHp0qQDHVM2P0JP47Y7B3RV1QXvrbIZSJEidxmcPFVSqk4NxmR # Jc7cGyVjba4QRL4X+mp8THoEDCT+7TvPwSeyE7OPwFdw9m7/Hh5PNWPnzpkPJkOt # Vi6tsCLb5cyE+W83pSvS7xMnH+cdZV8QMBMOWu3aUvgik2p4bb/kNpsHmwMm43/Y # qOJOPLLU2qta7XKzog8HXalZtUvXvIdU7M1xc4yy7xPRJo5zXyHsLGPkQoVGh52O # BRcMCRJxL/yUu+qB09KBncu/ietHxjpewNVKFMgJIaW9gY2vksEiIh20OBQ8iYi5 # Wena2WfODKCfOdiUsTNIQYxjuhWZTzvIrjcpOvNA4vFZ20jaSHSTfg4ZXqXk1DAQ # x6gpndJlVkVmT5tab4Lcvd2rDbfzYOqOJtnZ6KFjnTN8irCWlo8h6onPYH062QTP # jwl6nk2jtaBImkPoeMNJx1XVfl0ryNptcjeIom6o5uRz2gDtzifjpjS37TVZq+Ge # lNk+qSHCwcrKCnA1LwqsnF4+Av/p9ShQaQxnQzMPYJrvAgMBAAGjggFJMIIBRTAd # BgNVHQ4EFgQUUKzfY5cDTqHhOWUKpxecfEtWyUMwHwYDVR0jBBgwFoAUn6cVXQBe # Yl2D9OXSZacbUzUZ6XIwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNy # b3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBD # QSUyMDIwMTAoMSkuY3JsMGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0 # cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBU # aW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNV # HSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQEL # BQADggIBAEZoBXYQe8SACBxBIOSP+UvoMeu2kekYlvGQMLRN5s/KNHcp/qSD/pYn # TUdSCkENK/kY2ICPXGepm7wMr4d3tqvwVfK4xxN7nfT8mMqt9nrhYHWd71+G+UF3 # j1paQQGK3c4kdu6x8+lsKR+XWbEsqW1wwW0JFpDZeoPNsk8twGwWyg1wXc8WbBGr # mjqZWrSuxK+HYYJSgXsfCUNnTEpEmcHgjQ16nfa//VtlXUWbAySj/13OFMkJVbG6 # AaLSrWBElxZI8EdR1bB/kNAuvfzeQj/06t2ICNbm+G/ftzCRSloSVwnCRhWZHC8F # JmMKBYNVy6OwyyXRo6yB9Y7CNjRVyRUB3n+gHUXtREb2EHqrcqwb8SL0fj//NxTW # Ms8dOIp1E/UxdgMEzrqggumeu6DcEeKRBCcgCBERL5HYY431nILJP5xk5X2obzWP # 0jh6zUSqOPSg5XHDc0QiUMkPg+fa+/6nmzUskD038CPfvoxGNEP1FilTS4YqOeRm # AbHiffbOzc5HcTq/VH7aefexxKL2wOvahpWdzqVBNx9UQRg1afxbzNrl07pvC6zJ # D18eQKf5GzwErAvY7vaDAntiBFztkng9Wg9yKrwDnRxSh4Nb6Wz1EElm+oNsXwVd # 9MBXKmj2IM3G8T1j8maivkvJe88OuGjuYZie3kMKDH07tFWsuq+PMIIHcTCCBVmg # AwIBAgITMwAAABXF52ueAptJmQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9z # b2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgy # MjI1WhcNMzAwOTMwMTgzMjI1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz # aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAx # MDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ck # eb0O1YLT/e6cBwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+ # uDZnhUYjDLWNE893MsAQGOhgfWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4 # bo3t1w/YJlN8OWECesSq/XJprx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhi # JdxqD89d9P6OU8/W7IVWTe/dvI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD # 4MmPfrVUj9z6BVWYbWg7mka97aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKN # iOSWrAFKu75xqRdbZ2De+JKRHh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXf # tnIv231fgLrbqn427DZM9ituqBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8 # P0zbr17C89XYcz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMY # ctenIPDC+hIK12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9 # stQcxWv2XFJRXRLbJbqvUAV6bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUe # h17aj54WcmnGrnu3tz5q4i6tAgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQID # AQABMCMGCSsGAQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4E # FgQUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9 # AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w # cy9Eb2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsG # AQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTAD # AQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0w # S6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3Rz # L01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYI # KwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWlj # Um9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38 # Kq3hLB9nATEkW+Geckv8qW/qXBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTlt # uw8x5MKP+2zRoZQYIu7pZmc6U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99q # b74py27YP0h1AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQ # JL1AoL8ZthISEV09J+BAljis9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1 # ijbCHcNhcy4sa3tuPywJeBTpkbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP # 9pEB9s7GdP32THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkk # vnNtyo4JvbMBV0lUZNlz138eW0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFH # qfG3rsjoiV5PndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g7 # 5LcVv7TOPqUxUYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr # 4A245oyZ1uEi6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghi # f9lwY1NNje6CbaUFEMFxBmoQtB1VM1izoXBm8qGCA1YwggI+AgEBMIIBAaGB2aSB # 1jCB0zELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UE # CxMkTWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMScwJQYDVQQL # Ex5uU2hpZWxkIFRTUyBFU046NTIxQS0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jv # c29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoBATAHBgUrDgMCGgMVAGmygBWirdoW # lHapB3xWMwM34EjLoIGDMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh # c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw # MTAwDQYJKoZIhvcNAQELBQACBQDtJzpiMCIYDzIwMjYwMTMwMTQxMDQyWhgPMjAy # NjAxMzExNDEwNDJaMHQwOgYKKwYBBAGEWQoEATEsMCowCgIFAO0nOmICAQAwBwIB # AAICKEEwBwIBAAICErswCgIFAO0oi+ICAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYK # KwYBBAGEWQoDAqAKMAgCAQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQsF # AAOCAQEANnr7YpRc+uEPPGll6rALF0kHd93hdy617BxMYHLZ9gL5UJc/1zJkj8g7 # RVUCMFzmlPo0zGoSHDZTfaVKVBjyBvqxJyvsx+HKAi7xQz2Qw0zGops8ylH88Oud # iwIVmLTmTGcJ/Rh8ao6GA/Qeq6GOTF3OBcNiYEIqQq5whGjsrGfOeiBYvv3XSe5M # bCsynrKVUY+klMyBTzyAzJRrqDOqXFEN4W7289TeYaWdCkongZsEZUYk5P9/t5q7 # MHAyEblb8HjryX/DpvOmNJ4cVXsgxaYofrc3a7Z5lQuP1dwJ/O8Zbys5ewuzHeXk # oTisRCF2RXm9Q94fZccAzHtFBz9xgDGCBA0wggQJAgEBMIGTMHwxCzAJBgNVBAYT # AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBU # aW1lLVN0YW1wIFBDQSAyMDEwAhMzAAACF3H7LqWvAR3qAAEAAAIXMA0GCWCGSAFl # AwQCAQUAoIIBSjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcN # AQkEMSIEIKbT0ld9ookKDwJs6LtLr9zAMNtWpGsbIO90ccrA023oMIH6BgsqhkiG # 9w0BCRACLzGB6jCB5zCB5DCBvQQg0PJQYD5dt8mdEs1EreaYTiHoBz8sK5DcHr8X # tpPkWlswgZgwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv # bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAA # Ahdx+y6lrwEd6gABAAACFzAiBCBeDeAtGihAD964rKx1xWOSZFBIktw6SyGjxQte # EzZRSTANBgkqhkiG9w0BAQsFAASCAgAxndAlesfyK7I0X3iYo0Vv+Tc8XK5qKnNY # 5FLuiGcsuLtfofqCVIoEHGanzD7u4B1ZTZ6EdCgkZAuatcC7QhilmOXIrS+v+E9G # EAT0fP6OVJErlJZMGcJc5ny5X4BxoveTiAPbtEPkM/OI8QuN6maom+nT6+xfAn8P # rO0OYPbk/sViIpXqwrs937hQ5zVOBPP82IbqpU7r5MBPCNy99Q7UPTFUYStDG7LM # qo1nPqo2NrqKFHPZGZ9/3tclTT2uM6Qtseo/y5uK7CMw13P5UByFhNDYhsOvA8pr # yqb8Yxfdy/c1M++Q6xr4OdfB/KWknNhQuQDUfUtK4BdcqWYMRoORBWpzgsi9aytD # a8mflw1IJ2ToFNJB3a24okAOCJVm2eOpEsrzj8TYr9xxw5cjpuRugONmncuAZGfG # g8Ry8yW7VdgCUamJC3sblc/Hnbib/sk6zB87jYWmDxf7YWgH761E/JLtJtqpFQsi # QFpADUJbZx1nAS3YPDZ6pJOxHDVHzfqfxtHmeu73zLwn01GXWA1oE4uNrUnvmiYC # yXMhI8DfdtPvs1mxQHZZhXnKmdIAPogLfo83k9VzahdISdc2LPac6LCsMtUO8N1l # 4UWz0irpIYxkLJoOdAUP6mqOTDM2bLgfJG3D0iBRwKIPI6jyCcerolLvku+wUJoD # UN31ynRCgA== # SIG # End signature block |