TheDashboard.psm1
function Format-StringToSentence { <# .SYNOPSIS Formats a given string by adding spaces before uppercase letters, digits, and non-word characters. .DESCRIPTION The Format-AddSpaceToSentence function takes a string or an array of strings and adds a space before each uppercase letter, digit, and non-word character (excluding dots, spaces, and underscores). It also provides options to convert the string to lowercase, remove certain characters before or after the formatting, and remove double spaces. .PARAMETER Text The string or array of strings to be formatted. .PARAMETER RemoveCharsBefore An array of characters to be removed from the string before the formatting is applied. .PARAMETER RemoveCharsAfter An array of characters to be removed from the string after the formatting is applied. .PARAMETER ToLowerCase If this switch is present, the function will convert the string to lowercase. .PARAMETER RemoveDoubleSpaces If this switch is present, the function will remove any double spaces from the string. .PARAMETER MakeWordsUpperCase An array of words that should be converted to uppercase after the formatting is applied. .PARAMETER DisableAddingSpace If this switch is present, the function will not add spaces before uppercase letters, digits, and non-word characters. .EXAMPLE $test = @( 'OnceUponATime', 'OnceUponATime1', 'Money@Risk', 'OnceUponATime123', 'AHappyMan2014' 'OnceUponATime_123' 'Domain test.com' ) Format-StringToSentence -Text $Test -RemoveCharsAfter '_' -RemoveDoubleSpaces This example formats each string in the $test array, removes any underscores after the formatting, and removes any double spaces. .EXAMPLE $test = @( 'OnceUponATime', 'OnceUponATime1', 'Money@Risk', 'OnceUponATime123', 'AHappyMan2014' 'OnceUponATime_123' 'Domain test.com' ) $Test | Format-StringToSentence -ToLowerCase -RemoveCharsAfter '_' -RemoveDoubleSpaces This example does the same as the previous one, but also converts each string to lowercase. .EXAMPLE $test = @( 'OnceUponATime', 'OnceUponATime1', 'Money@Risk', 'OnceUponATime123', 'AHappyMan2014' 'OnceUponATime_123' 'Domain test.com' ) Format-StringToSentence -Text $Test -RemoveCharsAfter '_' -RemoveDoubleSpaces -MakeWordsUpperCase 'test.com', 'money' .NOTES The function uses the -creplace operator to add spaces, which is case-insensitive. Therefore, it will add spaces before both uppercase and lowercase letters if they are specified in the RemoveCharsBefore or RemoveCharsAfter parameters. #> [alias('Format-AddSpaceToSentence')] [CmdletBinding()] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, Position = 0)][string[]] $Text, [string[]] $RemoveCharsBefore, [string[]] $RemoveCharsAfter, [switch] $ToLowerCase, [switch] $RemoveDoubleSpaces, [string[]] $MakeWordsUpperCase, [switch] $DisableAddingSpace ) Process { foreach ($T in $Text) { if ($RemoveCharsBefore) { foreach ($R in $RemoveCharsBefore) { $T = $T -ireplace [regex]::Escape($R), "" } } if (-not $DisableAddingSpace) { $T = $T -creplace '([A-Z]|[^a-zA-Z0-9_.\s]|_|\d+)(?<![a-z])', ' $&' } if ($ToLowerCase) { $T = $T.ToLower() } if ($RemoveCharsAfter) { foreach ($R in $RemoveCharsAfter) { $T = $T -ireplace [regex]::Escape($R), "" } } if ($RemoveDoubleSpaces) { $T = $T -creplace '\s+', ' ' } if ($MakeWordsUpperCase) { foreach ($W in $MakeWordsUpperCase) { $T = $T -ireplace [regex]::Escape($W), $W.ToUpper() } } $T.Trim() } } } function Get-FileEncoding { <# .SYNOPSIS Get the encoding of a file (ASCII, UTF8, UTF8BOM, Unicode, BigEndianUnicode, UTF7) .DESCRIPTION Get the encoding of a file (ASCII, UTF8, UTF8BOM, Unicode, BigEndianUnicode, UTF7). Encoding is determined by the first few bytes of the file or by the presence of non-ASCII characters. .PARAMETER Path Path to the file to check .EXAMPLE Get-FileEncoding -Path 'C:\Users\pklys\Desktop\test.txt' .NOTES General notes #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [string] $Path ) if (-not (Test-Path -Path $Path)) { if ($ErrorActionPreference -eq 'Stop') { throw "Get-FileEncoding - File not found: $Path" } Write-Warning -Message "Get-FileEncoding - File not found: $Path" return } $fileStream = [System.IO.FileStream]::new($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) $byte = [byte[]]::new(4) $null = $fileStream.Read($byte, 0, 4) $fileStream.Close() if ($byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf) { return 'UTF8BOM' } elseif ($byte[0] -eq 0xff -and $byte[1] -eq 0xfe) { return 'Unicode' } elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) { return 'BigEndianUnicode' } elseif ($byte[0] -eq 0x2b -and $byte[1] -eq 0x2f -and $byte[2] -eq 0x76) { return 'UTF7' } else { $fileStream = [System.IO.FileStream]::new($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) $byte = [byte[]]::new(1) while ($fileStream.Read($byte, 0, 1) -gt 0) { if ($byte[0] -gt 0x7F) { $fileStream.Close() return 'UTF8' } } $fileStream.Close() return 'ASCII' } } function Get-GitHubLatestRelease { <# .SYNOPSIS Gets one or more releases from GitHub repository .DESCRIPTION Gets one or more releases from GitHub repository .PARAMETER Url Url to github repository .EXAMPLE Get-GitHubLatestRelease -Url "https://api.github.com/repos/evotecit/Testimo/releases" | Format-Table .NOTES General notes #> [CmdLetBinding()] param( [parameter(Mandatory)][alias('ReleasesUrl')][uri] $Url ) $ProgressPreference = 'SilentlyContinue' $Responds = Test-Connection -ComputerName $URl.Host -Quiet -Count 1 if ($Responds) { Try { [Array] $JsonOutput = (Invoke-WebRequest -Uri $Url -ErrorAction Stop | ConvertFrom-Json) foreach ($JsonContent in $JsonOutput) { [PSCustomObject] @{ PublishDate = [DateTime] $JsonContent.published_at CreatedDate = [DateTime] $JsonContent.created_at PreRelease = [bool] $JsonContent.prerelease Version = [version] ($JsonContent.name -replace 'v', '') Tag = $JsonContent.tag_name Branch = $JsonContent.target_commitish Errors = '' } } } catch { [PSCustomObject] @{ PublishDate = $null CreatedDate = $null PreRelease = $null Version = $null Tag = $null Branch = $null Errors = $_.Exception.Message } } } else { [PSCustomObject] @{ PublishDate = $null CreatedDate = $null PreRelease = $null Version = $null Tag = $null Branch = $null Errors = "No connection (ping) to $($Url.Host)" } } $ProgressPreference = 'Continue' } function Remove-EmptyValue { <# .SYNOPSIS Removes empty values from a hashtable recursively. .DESCRIPTION This function removes empty values from a given hashtable. It can be used to clean up a hashtable by removing keys with null, empty string, empty array, or empty dictionary values. The function supports recursive removal of empty values. .PARAMETER Hashtable The hashtable from which empty values will be removed. .PARAMETER ExcludeParameter An array of keys to exclude from the removal process. .PARAMETER Recursive Indicates whether to recursively remove empty values from nested hashtables. .PARAMETER Rerun Specifies the number of times to rerun the removal process recursively. .PARAMETER DoNotRemoveNull If specified, null values will not be removed. .PARAMETER DoNotRemoveEmpty If specified, empty string values will not be removed. .PARAMETER DoNotRemoveEmptyArray If specified, empty array values will not be removed. .PARAMETER DoNotRemoveEmptyDictionary If specified, empty dictionary values will not be removed. .EXAMPLE $hashtable = @{ 'Key1' = ''; 'Key2' = $null; 'Key3' = @(); 'Key4' = @{} } Remove-EmptyValue -Hashtable $hashtable -Recursive Description ----------- This example removes empty values from the $hashtable recursively. #> [alias('Remove-EmptyValues')] [CmdletBinding()] param( [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable, [string[]] $ExcludeParameter, [switch] $Recursive, [int] $Rerun, [switch] $DoNotRemoveNull, [switch] $DoNotRemoveEmpty, [switch] $DoNotRemoveEmptyArray, [switch] $DoNotRemoveEmptyDictionary ) foreach ($Key in [string[]] $Hashtable.Keys) { if ($Key -notin $ExcludeParameter) { if ($Recursive) { if ($Hashtable[$Key] -is [System.Collections.IDictionary]) { if ($Hashtable[$Key].Count -eq 0) { if (-not $DoNotRemoveEmptyDictionary) { $Hashtable.Remove($Key) } } else { Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive -ExcludeParameter $ExcludeParameter -DoNotRemoveNull:$DoNotRemoveNull -DoNotRemoveEmpty:$DoNotRemoveEmpty -DoNotRemoveEmptyArray:$DoNotRemoveEmptyArray -DoNotRemoveEmptyDictionary:$DoNotRemoveEmptyDictionary } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } } if ($Rerun) { for ($i = 0; $i -lt $Rerun; $i++) { Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive -ExcludeParameter $ExcludeParameter -DoNotRemoveNull:$DoNotRemoveNull -DoNotRemoveEmpty:$DoNotRemoveEmpty -DoNotRemoveEmptyArray:$DoNotRemoveEmptyArray -DoNotRemoveEmptyDictionary:$DoNotRemoveEmptyDictionary } } } function Remove-FileItem { <# .SYNOPSIS Removes one or more files and/or folders using one of three methods: Remove-Item, .NET Delete(), or move to Recycle Bin. .DESCRIPTION - If the path is a file, removes it in one shot. - If the path is a folder and -SkipFolder is NOT used, removes that folder in one shot (recursively if -Recursive is specified). - If -SkipFolder is used (and the path is a folder), calls Remove-ChildItems to remove only the folder's contents, leaving the folder itself. Supports: - RemoveItem, DotNetDelete, RecycleBin - Retries - WhatIf/Confirm - Passthru (detailed objects) - SimpleReturn (boolean overall result) .PARAMETER Paths One or more paths to files/folders. .PARAMETER DeleteMethod "RemoveItem", "DotNetDelete", or "RecycleBin". Defaults to "RemoveItem". .PARAMETER Recursive If set, allows recursive removal for folder contents. .PARAMETER SkipTopFolder If set for a folder, removes the contents only, not the folder itself. .PARAMETER Retries Specifies how many times to attempt removal before giving up. Defaults to 1. .PARAMETER Include Patterns (e.g., *.log) to include if removing child items individually. Will work only if SkipTopFolder is used. .PARAMETER Exclude Patterns (e.g., *.log) to exclude if removing child items individually. Will work only if SkipTopFolder is used. .PARAMETER Filter Filter for Get-ChildItem when removing child items individually. Will work only if SkipTopFolder is used. .PARAMETER Passthru Outputs an object for each path with details on success or failure. .PARAMETER SimpleReturn Outputs a single boolean indicating overall success or failure. .EXAMPLE Remove-FileItem -Paths "C:\SomeFolder" -DeleteMethod RemoveItem -Recursive -WhatIf Shows which items would be removed, but doesn't actually remove them, because -WhatIf is in effect. .EXAMPLE Remove-FileItem -Paths "C:\SomeFolder", "C:\SomeFile.txt" -Passthru -DeleteMethod DotNetDelete Removes both the folder (and everything in it) and the file, then returns details on each removal attempt. #> [Alias('Remove-ItemAlternative')] [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Include')] param( [Parameter(Mandatory, ValueFromPipeline, Position = 0)][Alias('LiteralPath', 'Path')][string[]] $Paths, [ValidateSet("RemoveItem", "DotNetDelete", "RecycleBin")] [string] $DeleteMethod = "RecycleBin", [switch] $Recursive, [alias('SkipFolder')][switch] $SkipTopFolder, [int] $Retries = 1, [Parameter(ParameterSetName = 'Include')][string[]] $Include, [Parameter(ParameterSetName = 'Exclude')][string[]] $Exclude, [string] $Filter, [switch] $Passthru, [switch] $SimpleReturn ) begin { $Results = [System.Collections.Generic.List[PSObject]]::new() $AllSucceeded = $true } process { foreach ($Path in $Paths) { $UnresolvedFullPath = $null try { $UnresolvedFullPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) } catch { $UnresolvedFullPath = $null } if (-not $UnresolvedFullPath) { $Message = "Remove-FileItem - Unable to resolve path '$Path'." if ($PSCmdlet.ErrorAction -eq 'Stop') { throw $Message } else { Write-Warning -Message $Message } $AllSucceeded = $false if ($Passthru) { $Results.Add( [PSCustomObject]@{ Path = $Path Status = $false Reason = "Path could not be resolved." AttemptCount = 0 WhatIf = $WhatIfPreference } ) } continue } if (-not (Test-Path -LiteralPath $UnresolvedFullPath)) { $Message = "Remove-FileItem - Path '$UnresolvedFullPath' does not exist." if ($PSCmdlet.ErrorAction -eq 'Stop') { throw $Message } else { Write-Warning -Message $Message } $AllSucceeded = $false if ($Passthru) { $Results.Add( [PSCustomObject]@{ Path = $UnresolvedFullPath Status = $false Reason = "Path does not exist." AttemptCount = 0 WhatIf = $WhatIfPreference } ) } continue } $ItemInfo = Get-Item -LiteralPath $UnresolvedFullPath -Force $IsDirectory = $ItemInfo.PSIsContainer if ($IsDirectory -and $SkipTopFolder) { $removeChildItemsSplat = @{ TopLevelPath = $UnresolvedFullPath DeleteMethod = $DeleteMethod Recursive = $Recursive Retries = $Retries Include = $Include Exclude = $Exclude Filter = $Filter Passthru = $Passthru Results = $Results AllSucceededRef = ([ref]$AllSucceeded) } Remove-ChildItems @removeChildItemsSplat } else { $Attempt = 0 $Deleted = $false $LastError = $null $WhatIfUsed = $WhatIfPreference while ($Attempt -lt $Retries -and -not $Deleted) { $Attempt++ Write-Verbose -Message "Remove-FileItem - Attempting to remove '$UnresolvedFullPath' (Attempt $Attempt of $Retries)." if ($WhatIfUsed) { $LastError = "Skipped because -WhatIf was used." } if ($PSCmdlet.ShouldProcess($UnresolvedFullPath, "Remove top-level")) { try { switch ($DeleteMethod) { 'RemoveItem' { if ($IsDirectory -and $Recursive) { Remove-Item -LiteralPath $UnresolvedFullPath -Recurse -Force -ErrorAction Stop } else { Remove-Item -LiteralPath $UnresolvedFullPath -Force -ErrorAction Stop } } 'DotNetDelete' { if ($IsDirectory) { $DirInfo = [System.IO.DirectoryInfo] $UnresolvedFullPath if ($Recursive) { $DirInfo.Delete($true) } else { $DirInfo.Delete() } } else { $FileInfo = [System.IO.FileInfo] $UnresolvedFullPath $FileInfo.Delete() } } 'RecycleBin' { $Shell = [Activator]::CreateInstance([Type]::GetTypeFromProgID("Shell.Application")) $FolderPath = Split-Path $UnresolvedFullPath $Leaf = Split-Path $UnresolvedFullPath -Leaf $Folder = $Shell.NameSpace($FolderPath) if (-not $Folder) { throw "Could not open folder: $FolderPath" } $ShellItem = $Folder.ParseName($Leaf) if (-not $ShellItem) { throw "Item '$Leaf' not found in folder: $FolderPath" } $ShellItem.InvokeVerb("delete") } } $Deleted = $true } catch { $LastError = $_.Exception.Message Write-Verbose -Message "Remove-FileItem - Error deleting '$UnresolvedFullPath': $LastError" if ($PSCmdlet.ErrorAction -eq 'Stop') { if ($Attempt -ge $Retries) { throw "Remove-FileItem - Couldn't delete '$UnresolvedFullPath' after $Retries attempts. Error: $LastError" } } else { if ($Attempt -ge $Retries) { Write-Warning -Message "Remove-FileItem - Couldn't delete '$UnresolvedFullPath' after $Retries attempts. Error: $LastError" } } } } } if (-not $Deleted -and -not $WhatIfUsed) { $AllSucceeded = $false } if ($WhatIfUsed) { $AllSucceeded = $false } if ($Passthru) { $Results.Add( [PSCustomObject]@{ Path = $UnresolvedFullPath Status = $Deleted Reason = if ($Deleted) { $null } else { $LastError } AttemptCount = $Attempt WhatIf = $WhatIfUsed } ) } } } } end { if ($Passthru) { $Results } elseif ($SimpleReturn) { $AllSucceeded } } } function Set-LoggingCapabilities { <# .SYNOPSIS Sets up logging capabilities by managing log files. .DESCRIPTION This function sets up logging capabilities by creating the necessary directories and managing the number of log files based on the specified maximum. .PARAMETER LogPath The path where the log files will be stored. .PARAMETER ScriptPath The path of the script that generates the logs. .PARAMETER LogMaximum The maximum number of log files to keep. Older files will be deleted if this limit is exceeded. .PARAMETER ShowTime Switch to include timestamps in the log entries. .PARAMETER TimeFormat The format of the timestamps in the log entries. .PARAMETER ParameterPSDefaultParameterValues The hashtable of default parameter values for the Write-Color function. If this parameter is not provided, the function will create a new hashtable. This will only work properly if the function is nested as private function in another module. It's advised to provide the hashtable from the parent function for this to work always. .EXAMPLE Set-LoggingCapabilities -LogPath "C:\Logs\log.log" -ScriptPath "C:\Scripts\script.ps1" -LogMaximum 10 -ShowTime -TimeFormat "yyyy-MM-dd HH:mm:ss" -ParameterPSDefaultParameterValues $Script:PSDefaultParameterValues .EXAMPLE Set-LoggingCapabilities -LogPath "C:\Logs\log.log" -ScriptPath "C:\Scripts\script.ps1" -LogMaximum 10 -ShowTime -TimeFormat "yyyy-MM-dd HH:mm:ss" .NOTES This function is used in: - CleanupMonster - PasswordSolution - SharePointEssentials And many other scripts. #> [CmdletBinding()] param( [Alias('Path', 'Log', 'Folder', 'LiteralPath', 'FilePath')][string] $LogPath, [string] $ScriptPath, [Alias('Maximum')][int] $LogMaximum, [switch] $ShowTime, [string] $TimeFormat, [System.Collections.IDictionary] $ParameterPSDefaultParameterValues ) if (-not $ParameterPSDefaultParameterValues) { $Script:PSDefaultParameterValues = @{ "Write-Color:LogFile" = $LogPath "Write-Color:ShowTime" = if ($PSBoundParameters.ContainsKey('ShowTime')) { $ShowTime.IsPresent } else { $null } "Write-Color:TimeFormat" = $TimeFormat } } else { $ParameterPSDefaultParameterValues["Write-Color:LogFile"] = $LogPath $ParameterPSDefaultParameterValues["Write-Color:ShowTime"] = if ($PSBoundParameters.ContainsKey('ShowTime')) { $ShowTime.IsPresent } else { $null } $ParameterPSDefaultParameterValues["Write-Color:TimeFormat"] = $TimeFormat } if ($LogPath) { try { $FolderPath = [io.path]::GetDirectoryName($LogPath) if (-not (Test-Path -LiteralPath $FolderPath)) { $null = New-Item -Path $FolderPath -ItemType Directory -Force -WhatIf:$false } if ($LogMaximum -gt 0) { if ($ScriptPath) { $ScriptPathFolder = [io.path]::GetDirectoryName($ScriptPath) if ($ScriptPathFolder -eq $FolderPath) { Write-Color -Text '[i] ', "LogMaximum is set to ", $LogMaximum, " but log files are in the same folder as the script. Cleanup disabled." -Color Yellow, White, DarkCyan, White return } $LogPathExtension = [io.path]::GetExtension($LogPath) if ($LogPathExtension) { $CurrentLogs = Get-ChildItem -LiteralPath $FolderPath -Filter "*$LogPathExtension" -ErrorAction Stop | Sort-Object -Property CreationTime -Descending | Select-Object -Skip $LogMaximum } else { $CurrentLogs = $null Write-Color -Text '[i] ', "Log file has no extension (?!). Cleanup disabled." -Color Yellow, White, DarkCyan, White } if ($CurrentLogs) { Write-Color -Text '[i] ', "Logs directory has more than ", $LogMaximum, " log files. Cleanup required..." -Color Yellow, DarkCyan, Red, DarkCyan foreach ($Log in $CurrentLogs) { try { Remove-Item -LiteralPath $Log.FullName -Confirm:$false -WhatIf:$false Write-Color -Text '[+] ', "Deleted ", "$($Log.FullName)" -Color Yellow, White, Green } catch { Write-Color -Text '[-] ', "Couldn't delete log file $($Log.FullName). Error: ', "$($_.Exception.Message) -Color Yellow, White, Red } } } } else { Write-Color -Text '[i] ', "LogMaximum is set to ", $LogMaximum, " but no script path detected. Most likely running interactively. Cleanup disabled." -Color Yellow, White, DarkCyan, White } } else { Write-Color -Text '[i] ', "LogMaximum is set to 0 (Unlimited). No log files will be deleted." -Color Yellow, DarkCyan } } catch { Write-Color -Text "[e] ", "Couldn't create the log directory. Error: $($_.Exception.Message)" -Color Yellow, Red $Script:PSDefaultParameterValues["Write-Color:LogFile"] = $null } } else { $Script:PSDefaultParameterValues["Write-Color:LogFile"] = $null } Remove-EmptyValue -Hashtable $Script:PSDefaultParameterValues } function Start-TimeLog { <# .SYNOPSIS Starts a new stopwatch for logging time. .DESCRIPTION This function starts a new stopwatch that can be used for logging time durations. .EXAMPLE Start-TimeLog Starts a new stopwatch for logging time. #> [CmdletBinding()] param() [System.Diagnostics.Stopwatch]::StartNew() } function Stop-TimeLog { <# .SYNOPSIS Stops the stopwatch and returns the elapsed time in a specified format. .DESCRIPTION The Stop-TimeLog function stops the provided stopwatch and returns the elapsed time in a specified format. The function can output the elapsed time as a single string or an array of days, hours, minutes, seconds, and milliseconds. .PARAMETER Time Specifies the stopwatch object to stop and retrieve the elapsed time from. .PARAMETER Option Specifies the format in which the elapsed time should be returned. Valid values are 'OneLiner' (default) or 'Array'. .PARAMETER Continue Indicates whether the stopwatch should continue running after retrieving the elapsed time. .EXAMPLE $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() # Perform some operations Stop-TimeLog -Time $stopwatch # Output: "0 days, 0 hours, 0 minutes, 5 seconds, 123 milliseconds" .EXAMPLE $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() # Perform some operations Stop-TimeLog -Time $stopwatch -Option Array # Output: ["0 days", "0 hours", "0 minutes", "5 seconds", "123 milliseconds"] #> [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)][System.Diagnostics.Stopwatch] $Time, [ValidateSet('OneLiner', 'Array')][string] $Option = 'OneLiner', [switch] $Continue ) Begin { } Process { if ($Option -eq 'Array') { $TimeToExecute = "$($Time.Elapsed.Days) days", "$($Time.Elapsed.Hours) hours", "$($Time.Elapsed.Minutes) minutes", "$($Time.Elapsed.Seconds) seconds", "$($Time.Elapsed.Milliseconds) milliseconds" } else { $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds" } } End { if (-not $Continue) { $Time.Stop() } return $TimeToExecute } } function Write-Color { <# .SYNOPSIS Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options. .DESCRIPTION Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options. It provides: - Easy manipulation of colors, - Logging output to file (log) - Nice formatting options out of the box. - Ability to use aliases for parameters .PARAMETER Text Text to display on screen and write to log file if specified. Accepts an array of strings. .PARAMETER Color Color of the text. Accepts an array of colors. If more than one color is specified it will loop through colors for each string. If there are more strings than colors it will start from the beginning. Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White .PARAMETER BackGroundColor Color of the background. Accepts an array of colors. If more than one color is specified it will loop through colors for each string. If there are more strings than colors it will start from the beginning. Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White .PARAMETER StartTab Number of tabs to add before text. Default is 0. .PARAMETER LinesBefore Number of empty lines before text. Default is 0. .PARAMETER LinesAfter Number of empty lines after text. Default is 0. .PARAMETER StartSpaces Number of spaces to add before text. Default is 0. .PARAMETER LogFile Path to log file. If not specified no log file will be created. .PARAMETER DateTimeFormat Custom date and time format string. Default is yyyy-MM-dd HH:mm:ss .PARAMETER LogTime If set to $true it will add time to log file. Default is $true. .PARAMETER LogRetry Number of retries to write to log file, in case it can't write to it for some reason, before skipping. Default is 2. .PARAMETER Encoding Encoding of the log file. Default is Unicode. .PARAMETER ShowTime Switch to add time to console output. Default is not set. .PARAMETER NoNewLine Switch to not add new line at the end of the output. Default is not set. .PARAMETER NoConsoleOutput Switch to not output to console. Default all output goes to console. .EXAMPLE Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1 .EXAMPLE Write-Color "1. ", "Option 1" -Color Yellow, Green Write-Color "2. ", "Option 2" -Color Yellow, Green Write-Color "3. ", "Option 3" -Color Yellow, Green Write-Color "4. ", "Option 4" -Color Yellow, Green Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1 .EXAMPLE Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss" Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" .EXAMPLE Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow Write-Color -t "my text" -c yellow -b green Write-Color -text "my text" -c red .EXAMPLE Write-Color -Text "Testuję czy się ładnie zapisze, czy będą problemy" -Encoding unicode -LogFile 'C:\temp\testinggg.txt' -Color Red -NoConsoleOutput .NOTES Understanding Custom date and time format strings: https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings Project support: https://github.com/EvotecIT/PSWriteColor Original idea: Josh (https://stackoverflow.com/users/81769/josh) #> [alias('Write-Colour')] [CmdletBinding()] param ( [alias ('T')] [String[]]$Text, [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White, [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null, [alias ('Indent')][int] $StartTab = 0, [int] $LinesBefore = 0, [int] $LinesAfter = 0, [int] $StartSpaces = 0, [alias ('L')] [string] $LogFile = '', [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss', [alias ('LogTimeStamp')][bool] $LogTime = $true, [int] $LogRetry = 2, [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode', [switch] $ShowTime, [switch] $NoNewLine, [alias('HideConsole')][switch] $NoConsoleOutput ) if (-not $NoConsoleOutput) { $DefaultColor = $Color[0] if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) { Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated." return } if ($LinesBefore -ne 0) { for ($i = 0; $i -lt $LinesBefore; $i++) { Write-Host -Object "`n" -NoNewline } } if ($StartTab -ne 0) { for ($i = 0; $i -lt $StartTab; $i++) { Write-Host -Object "`t" -NoNewline } } if ($StartSpaces -ne 0) { for ($i = 0; $i -lt $StartSpaces; $i++) { Write-Host -Object ' ' -NoNewline } } if ($ShowTime) { Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline } if ($Text.Count -ne 0) { if ($Color.Count -ge $Text.Count) { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } } else { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } } } else { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Color.Length ; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline } } else { for ($i = 0; $i -lt $Color.Length ; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline } } } } if ($NoNewLine -eq $true) { Write-Host -NoNewline } else { Write-Host } if ($LinesAfter -ne 0) { for ($i = 0; $i -lt $LinesAfter; $i++) { Write-Host -Object "`n" -NoNewline } } } if ($Text.Count -and $LogFile) { $TextToFile = "" for ($i = 0; $i -lt $Text.Length; $i++) { $TextToFile += $Text[$i] } $Saved = $false $Retry = 0 Do { $Retry++ try { if ($LogTime) { "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } else { "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } $Saved = $true } catch { if ($Saved -eq $false -and $Retry -eq $LogRetry) { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Tried ($Retry/$LogRetry))" } else { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)" } } } Until ($Saved -eq $true -or $Retry -ge $LogRetry) } } function Remove-ChildItems { <# .SYNOPSIS Helper function that removes the child items of a given folder. .DESCRIPTION - Enumerates the folder's contents (recursively if requested). - Removes each child item individually, respecting the chosen DeleteMethod. - Respects -WhatIf and -Confirm (because of CmdletBinding with SupportsShouldProcess). - Records attempts in Passthru objects if desired. .PARAMETER TopLevelPath Specifies the path to the folder whose contents are to be removed. .PARAMETER DeleteMethod Specifies the removal method: RemoveItem, DotNetDelete, or RecycleBin. .PARAMETER Recursive Specifies whether to enumerate child items recursively. .PARAMETER Retries Number of retry attempts for each item. .PARAMETER Include Include patterns for Get-ChildItem. .PARAMETER Exclude Exclude patterns for Get-ChildItem. .PARAMETER Filter Filter for Get-ChildItem. .PARAMETER Passthru If true, records results in $Results. .PARAMETER Results [System.Collections.Generic.List[PSObject]] passed in from the parent function. .PARAMETER AllSucceededRef [ref] reference to a boolean tracking overall success/failure. .EXAMPLE Remove-ChildItems -TopLevelPath "C:\Folder" -DeleteMethod RemoveItem -Recursive -WhatIf Shows (via WhatIf) which items would be removed inside "C:\Folder". #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)][string] $TopLevelPath, [Parameter(Mandatory)][ValidateSet("RemoveItem", "DotNetDelete", "RecycleBin")] [string] $DeleteMethod, [switch] $Recursive, [int] $Retries, [string[]] $Include, [string[]] $Exclude, [string] $Filter, [switch] $Passthru, [System.Collections.Generic.List[PSObject]] $Results, [ref] $AllSucceededRef ) begin { Write-Verbose -Message "Remove-ItemAlternative - Removing contents of '$TopLevelPath' (SkipFolder)." $getChildItemSplat = @{ LiteralPath = $TopLevelPath Force = $true Include = $Include Exclude = $Exclude Filter = $Filter ErrorAction = 'SilentlyContinue' } Remove-EmptyValue -Hashtable $getChildItemSplat $Items = Get-ChildItem @getChildItemSplat if ($Recursive) { $Items = $Items | Sort-Object -Property FullName -Descending } $WhatIfUsed = $WhatIfPreference } process { foreach ($Item in $Items) { $Attempt = 0 $Deleted = $false $LastError = $null while ($Attempt -lt $Retries -and -not $Deleted) { $Attempt++ Write-Verbose -Message "Remove-ItemAlternative - Attempting to remove '$($Item.FullName)' (Attempt $Attempt of $Retries)." if ($WhatIfUsed) { $LastError = "Skipped because -WhatIf was used." } if ($PSCmdlet.ShouldProcess($Item.FullName, "Remove child item")) { try { switch ($DeleteMethod) { 'RemoveItem' { if ($Recursive -and $Item.PSIsContainer) { Remove-Item -LiteralPath $Item.FullName -Recurse -Force -ErrorAction Stop } else { Remove-Item -LiteralPath $Item.FullName -Force -ErrorAction Stop } } 'DotNetDelete' { if ($Item.PSIsContainer) { $Dir = [System.IO.DirectoryInfo] $Item.FullName if ($Recursive) { $Dir.Delete($true) } else { $Dir.Delete() } } else { $File = [System.IO.FileInfo] $Item.FullName $File.Delete() } } 'RecycleBin' { $Shell = [Activator]::CreateInstance([Type]::GetTypeFromProgID("Shell.Application")) $FolderPath = Split-Path $Item.FullName $Leaf = Split-Path $Item.FullName -Leaf $Folder = $Shell.NameSpace($FolderPath) if (-not $Folder) { throw "Could not open folder: $FolderPath" } $ShellItem = $Folder.ParseName($Leaf) if (-not $ShellItem) { throw "Item '$Leaf' not found in folder: $FolderPath" } $ShellItem.InvokeVerb("delete") } } $Deleted = $true } catch { $LastError = $_.Exception.Message Write-Verbose -Message "Remove-ItemAlternative - Error deleting '$($Item.FullName)': $LastError" if ($PSCmdlet.ErrorAction -eq 'Stop') { if ($Attempt -ge $Retries) { throw "Remove-ItemAlternative - Couldn't delete '$($Item.FullName)' after $Retries attempts. Error: $LastError" } } else { if ($Attempt -ge $Retries) { Write-Warning -Message "Remove-ItemAlternative - Couldn't delete '$($Item.FullName)' after $Retries attempts. Error: $LastError" } } } } } if (-not $Deleted -and -not $WhatIfUsed) { $AllSucceededRef.Value = $false } if ($Passthru) { $Results.Add( [PSCustomObject]@{ Path = $Item.FullName Status = $Deleted Reason = if ($Deleted) { $null } else { $LastError } AttemptCount = $Attempt WhatIf = $WhatIfUsed } ) } } } } function Convert-FilesToKeepOrRemove { [CmdletBinding(SupportsShouldProcess)] param( [Array] $FilePathsGenerated, [Array] $Files, [switch] $RemoveNotIncluded, [ValidateSet("RemoveItem", "DotNetDelete", "RecycleBin")] [string] $DeleteMethod = "RecycleBin" ) [Array] $FullSummary = @( foreach ($File in $FilePathsGenerated) { try { $FileInfo = Get-Item -LiteralPath $File -ErrorAction Stop $Output = [PSCustomObject] @{ Type = 'Dashboard' Path = $File Include = $true Date = ($FileInfo).LastWriteTime Status = 'Included' } } catch { $Output = [PSCustomObject] @{ Type = 'Dashboard' Path = $File Include = $false Date = $null Status = 'Included (WhatIf)' } } if ($WhatIfPreference) { $Output.Status = 'Included (WhatIf)' } $Output } foreach ($File in $Files) { [PSCustomObject] @{ Type = 'Report' Path = $File.FullPath Include = $File.Include Date = $File.Date Status = if ($File.Include) { 'Included' } else { 'Excluded' } } } ) if ($RemoveNotIncluded) { foreach ($File in $FullSummary) { if (-not $File.Include) { Remove-FileItem -Paths $File.Path -DeleteMethod $DeleteMethod -WhatIf:$WhatIfPreference if ($WhatIfPreference) { $File.Status = 'Removed (WhatIf)' } else { $File.Status = 'Removed' } } } } $FullSummary } function Convert-FilesToMenu { <# .SYNOPSIS Generates a structured menu from a collection of files and folders. .DESCRIPTION Builds ordered folders and file entries, applies filtering and history logic. .PARAMETER Folders Dictionary defining folder metadata and optional limits configuration. .PARAMETER Files Array of file data, each containing attributes such as Date, Menu, and Name. .PARAMETER ExportData Dictionary containing exported data from previous runs. .PARAMETER Force Forces the regeneration of the menu. .EXAMPLE Convert-FilesToMenu -Folders $Folders -Files $FileList .NOTES Part of TheDashboard module, creates the navigation menu from report files. #> [CmdletBinding()] param( [System.Collections.IDictionary] $Folders, [Array] $Files, [System.Collections.IDictionary] $ExportData, [switch] $Force ) $CurrentDate = Get-Date $MenuBuilder = [ordered] @{} foreach ($Folder in $Folders.Keys) { if (-not $MenuBuilder[$Folder]) { $MenuBuilder[$Folder] = [ordered] @{} } } foreach ($Entry in $Files | Sort-Object { $_.Date } -Descending) { $LimitsConfiguration = $Folders[$Entry.Menu].LimitsConfiguration if ($LimitsConfiguration) { $Limits = $LimitsConfiguration[$Entry.Name] if (-not $Limits) { if ($LimitsConfiguration['*']) { $Limits = $LimitsConfiguration['*'] } else { $Limits = $null } } } else { $Limits = $null } if (-not $MenuBuilder[$Entry.Menu][$Entry.Name]) { $Entry.Include = $true $MenuBuilder[$Entry.Menu][$Entry.Name] = @{ Current = $Entry Full = [System.Collections.Generic.List[Object]]::new() History = [System.Collections.Generic.List[Object]]::new() } } if ($Entry.Include -ne $true) { if ($Limits.LimitItem) { if ($MenuBuilder[$Entry.Menu][$Entry.Name]['Full'].Count -ge $Limits.LimitItem) { $Entry.Include = $true Limit-FilesHistory -MenuBuilder $MenuBuilder -Entry $Entry -Limits $Limits -CurrentDate $CurrentDate continue } } elseif ($Limits.LimitDate) { if ($Entry.Date -lt $Limits.LimitDate) { $Entry.Include = $true Limit-FilesHistory -MenuBuilder $MenuBuilder -Entry $Entry -Limits $Limits -CurrentDate $CurrentDate continue } } elseif ($Limits.LimitDays) { if ($Entry.Date -lt ($CurrentDate).AddDays(-$Limits.LimitDays)) { $Entry.Include = $true Limit-FilesHistory -MenuBuilder $MenuBuilder -Entry $Entry -Limits $Limits -CurrentDate $CurrentDate continue } } } $Entry.Include = $true $MenuBuilder[$Entry.Menu][$Entry.Name]['Full'].Add($Entry) } if ($Force) { return $MenuBuilder } if ($ExportData.MenuBuilder.Keys.Count -eq 0) { return $MenuBuilder } if ($MenuBuilder.Keys -and $ExportData.MenuBuilder.Keys -and $MenuBuilder.Keys.Count -ne $ExportData.MenuBuilder.Keys.Count) { return $MenuBuilder } $Comparison = Compare-Object -ReferenceObject $MenuBuilder.Keys -DifferenceObject $ExportData.MenuBuilder.Keys if ($Comparison) { return $MenuBuilder } foreach ($Section in $MenuBuilder.Keys) { if ($ExportData.MenuBuilder[$Section].Count -ne $MenuBuilder[$Section].Count) { return $MenuBuilder } $Comparison = Compare-Object -ReferenceObject $MenuBuilder[$Section].Keys -DifferenceObject $ExportData.MenuBuilder[$Section].Keys if ($Comparison) { return $MenuBuilder } } foreach ($Section in $MenuBuilder.Keys) { foreach ($Name in $MenuBuilder[$Section].Keys) { $MenuData = $MenuBuilder[$Section][$Name] $MenuDataFromImport = $ExportData.MenuBuilder[$Section][$Name] $RegenerateNotRequired = $true $Comparison = Compare-Object -ReferenceObject $MenuData.Current -DifferenceObject $MenuDataFromImport.Current -Property Date, Name, Href if ($Comparison) { $RegenerateNotRequired = $false } $Comparison = Compare-Object -ReferenceObject $MenuData.Full -DifferenceObject $MenuDataFromImport.Full -Property Date, Name, Href if ($Comparison) { $RegenerateNotRequired = $false } $Comparison = Compare-Object -ReferenceObject $MenuData.History -DifferenceObject $MenuDataFromImport.History -Property Date, Name, Href if ($Comparison) { $RegenerateNotRequired = $false } if ($RegenerateNotRequired -eq $true) { $MenuData.Current.SkipGeneration = $true foreach ($Entry in $MenuData.Full) { $Entry.SkipGeneration = $true } foreach ($Entry in $MenuData.History) { $Entry.SkipGeneration = $true } } } } $MenuBuilder } function Convert-FilesToMenuData { [CmdletBinding()] param( [System.Collections.IDictionary] $Folders, [System.Collections.IDictionary] $Replacements, [string] $Extension ) Write-Color -Text '[i]', "[TheDashboard] ", 'Creating Menu from files', ' [Informative] ' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta foreach ($FolderName in $Folders.Keys) { $Folder = $Folders[$FolderName] $FilesInFolder = Get-ChildItem -LiteralPath $Folders[$FolderName].Path -ErrorAction SilentlyContinue -Filter "*$Extension" | Sort-Object -Property Name Write-Color -Text '[i]', "[TheDashboard] ", "Creating Menu from files in folder ", "'$FolderName'", " files in folder ", $($FilesInFolder.Count), ' [Informative] ' -Color Yellow, DarkGray, Yellow, Magenta, Yellow, Magenta, DarkGray foreach ($File in $FilesInFolder) { $RelativeFolder = Split-Path -Path $Folders[$FolderName].Path -Leaf $Href = "$($RelativeFolder)/$($File.Name)" $MenuName = $File.BaseName $ResultsFile = [ordered] @{ File = $File.Name Href = $Href MenuName = $MenuName } $ResultsFile['ReplaceCaseInsenstive'] = $Replacements.ReplaceCaseInsenstive $ResultsFile['ReplaceSkipRegex'] = $Replacements.ReplaceSkipRegex if ($Folder.ReplacementsGlobal -eq $true) { foreach ($Replace in $Replacements.BeforeSplit.Keys) { if ($Replacements.ReplaceCaseInsenstive -and $Replacements.ReplaceSkipRegex) { $MenuName = $MenuName -ireplace $Replace, $Replacements.BeforeSplit[$Replace] } elseif ($Replacements.ReplaceCaseInsenstive) { $MenuName = $MenuName -ireplace [regex]::Escape($Replace), $Replacements.BeforeSplit[$Replace] } else { $MenuName = $MenuName.Replace($Replace, $Replacements.BeforeSplit[$Replace]) } } $ResultsFile['MenuNameStep2AfterBeforeSplitReplacements'] = $MenuName if ($Replacements.SplitOn) { $Splitted = $MenuName -split $Replacements.SplitOn if ($null -ne $Replacements.AfterSplitPositionName) { $PositionPlace = @(0) $Name = '' if ($Replacements.AfterSplitPositionName -is [System.Collections.IDictionary]) { foreach ($FileNameToFind in $Replacements.AfterSplitPositionName.Keys) { if ($MenuName -like $FileNameToFind) { [Array] $PositionPlace = $Replacements.AfterSplitPositionName[$FileNameToFind] break } } } else { [Array] $PositionPlace = $Replacements.AfterSplitPositionName } $NameParts = foreach ($Position in $PositionPlace) { $Splitted[$Position] } $Name = $NameParts -join ' ' $ResultsFile['SplitPosition'] = $PositionPlace -join ', ' $ResultsFile['SplitPositionName'] = $Name } else { $Name = $Splitted[0] $ResultsFile['SplitPosition'] = 0 $ResultsFile['SplitPositionName'] = $Name } } else { $Name = $MenuName $ResultsFile['SplitPosition'] = $null $ResultsFile['SplitPositionName'] = $Name } $ResultsFile['MenuNameStep3AfterSplittingPosition'] = $Name $formatStringToSentenceSplat = @{ Text = $Name RemoveCharsBefore = $Replacements.BeforeRemoveChars RemoveCharsAfter = $Replacements.AfterRemoveChars RemoveDoubleSpaces = $Replacements.AfterRemoveDoubleSpaces MakeWordsUpperCase = $Replacements.AfterUpperChars DisableAddingSpace = -not $Replacements.AddSpaceToName } $Name = Format-StringToSentence @formatStringToSentenceSplat $ResultsFile['BeforeRemoveChars'] = $Replacements.BeforeRemoveChars $ResultsFile['AfterRemoveChars'] = $Replacements.AfterRemoveChars $ResultsFile['AfterRemoveDoubleSpaces'] = $Replacements.AfterRemoveDoubleSpaces $ResultsFile['AfterUpperChars'] = $Replacements.AfterUpperChars $ResultsFile['AddSpaceToName'] = $Replacements.AddSpaceToName $ResultsFile['MenuNameStep4AfterFormat'] = $Name foreach ($Replace in $Replacements.AfterSplit.Keys) { if ($Replacements.ReplaceCaseInsenstive -and $Replacements.ReplaceSkipRegex) { $Name = $Name -ireplace $Replace, $Replacements.AfterSplit[$Replace] } elseif ($Replacements.ReplaceCaseInsenstive) { $Name = $Name -ireplace [regex]::Escape($Replace), $Replacements.AfterSplit[$Replace] } else { $Name = $Name.Replace($Replace, $Replacements.AfterSplit[$Replace]) } } $ResultsFile['MenuNameFinal'] = $Name $Type = 'global replacements' $ResultsFile['Type'] = $Type if ($DebugPreference) { [PSCustomObject] $ResultsFile | Out-String | Write-Debug } } elseif ($Folder.Replacements) { foreach ($Replace in $Folder.Replacements.BeforeSplit.Keys) { if ($Folder.Replacements.ReplaceCaseInsenstive -and $Folder.Replacements.ReplaceSkipRegex) { $MenuName = $MenuName -ireplace $Replace, $Folder.Replacements.BeforeSplit[$Replace] } elseif ($Folder.Replacements.ReplaceCaseInsenstive) { $MenuName = $MenuName -ireplace [regex]::Escape($Replace), $Folder.Replacements.BeforeSplit[$Replace] } else { $MenuName = $MenuName.Replace($Replace, $Folder.Replacements.BeforeSplit[$Replace]) } } $ResultsFile['MenuNameStep2AfterBeforeSplitReplacements'] = $MenuName if ($Folder.Replacements.SplitOn) { $Splitted = $MenuName -split $Folder.Replacements.SplitOn if ($null -ne $Folder.Replacements.AfterSplitPositionName) { $PositionPlace = @(0) $Name = '' if ($Folder.Replacements.AfterSplitPositionName -is [System.Collections.IDictionary]) { foreach ($FileNameToFind in $Folder.Replacements.AfterSplitPositionName.Keys) { if ($MenuName -like $FileNameToFind) { [Array] $PositionPlace = $Folder.Replacements.AfterSplitPositionName[$FileNameToFind] break } } } else { [Array] $PositionPlace = $Folder.Replacements.AfterSplitPositionName } $NameParts = foreach ($Position in $PositionPlace) { $Splitted[$Position] } $Name = $NameParts -join ' ' $ResultsFile['SplitPosition'] = $PositionPlace -join ', ' $ResultsFile['SplitPositionName'] = $Name } else { $Name = $Splitted[0] $ResultsFile['SplitPosition'] = 0 $ResultsFile['SplitPositionName'] = $Name } } else { $Name = $MenuName $ResultsFile['SplitPosition'] = $null $ResultsFile['SplitPositionName'] = $Name } $ResultsFile['MenuNameStep3AfterSplittingPosition'] = $Name $formatStringToSentenceSplat = @{ Text = $Name RemoveCharsBefore = $Folder.Replacements.BeforeRemoveChars RemoveCharsAfter = $Folder.Replacements.AfterRemoveChars RemoveDoubleSpaces = $Folder.Replacements.AfterRemoveDoubleSpaces MakeWordsUpperCase = $Folder.Replacements.AfterUpperChars DisableAddingSpace = -not $Folder.Replacements.AddSpaceToName } $Name = Format-StringToSentence @formatStringToSentenceSplat $ResultsFile['BeforeRemoveChars'] = $Folder.Replacements.BeforeRemoveChars $ResultsFile['AfterRemoveChars'] = $Folder.Replacements.AfterRemoveChars $ResultsFile['AfterRemoveDoubleSpaces'] = $Folder.Replacements.AfterRemoveDoubleSpaces $ResultsFile['AfterUpperChars'] = $Folder.Replacements.AfterUpperChars $ResultsFile['AddSpaceToName'] = $Folder.Replacements.AddSpaceToName $ResultsFile['MenuNameStep4AfterFormat'] = $Name foreach ($Replace in $Folder.Replacements.AfterSplit.Keys) { if ($Folder.Replacements.ReplaceCaseInsenstive -and $Folder.Replacements.ReplaceSkipRegex) { $Name = $Name -ireplace $Replace, $Folder.Replacements.AfterSplit[$Replace] } elseif ($Folder.Replacements.ReplaceCaseInsenstive) { $Name = $Name -ireplace [regex]::Escape($Replace), $Folder.Replacements.AfterSplit[$Replace] } else { $Name = $Name.Replace($Replace, $Folder.Replacements.AfterSplit[$Replace]) } } $ResultsFile['MenuNameFinal'] = $Name $Type = 'folder replacements' $ResultsFile['Type'] = $Type if ($DebugPreference) { [PSCustomObject] $ResultsFile | Out-String | Write-Debug } } else { $Name = $MenuName $Type = 'no replacements applied' } if ($Name) { [PSCustomObject] @{ Name = $Name Href = $Href FileName = "$($Folder.Url)_$($File.Name)" Menu = $FolderName MenuLink = $Folder.Url Date = $File.LastWriteTime FullPath = $File.FullName Include = $false SkipGeneration = $false Debug = [PSCustomObject] $ResultsFile } } else { Write-Color -Text "[e]", "[TheDashboard] ", "Creating Menu ", "[error] ", "Couldn't create menu item for $($File.FullName). Problem with $Type" -Color Red, DarkGray, Red, DarkGray, Red } } } } function Convert-MultipleReplacements { [CmdletBinding()] param( [System.Collections.IDictionary] $Replacements, [Array] $ReplacementConfiguration ) Write-Color -Text '[i]', "[TheDashboard] ", 'Converting Multiple Replacements', ' [Informative] ' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta if (-not $Replacements) { $ReplacementSetting = [ordered] @{ SplitOn = $null BeforeSplit = [ordered] @{} AfterSplit = [ordered] @{} AddSpaceToName = $null AfterSplitPositionName = $null AfterRemoveChars = $null AfterUpperChars = $null AfterRemoveDoubleSpaces = $null BeforeRemoveChars = $null } } else { $ReplacementSetting = $Replacements } foreach ($Replacement in $ReplacementConfiguration) { Remove-EmptyValue -Hashtable $Replacement foreach ($R in $Replacement.Keys) { if ($R -eq 'SplitOn') { if ($null -ne $Replacement[$R]) { $ReplacementSetting['SplitOn'] = $Replacement[$R] } } elseif ($R -eq 'beforeSplit') { foreach ($Before in $Replacement[$R]) { foreach ($Key in $Before.Keys) { $ReplacementSetting['BeforeSplit'][$Key] = $Before[$Key] } } } elseif ($R -eq 'afterSplit') { foreach ($After in $Replacement[$R]) { foreach ($Key in $After.Keys) { $ReplacementSetting['AfterSplit'][$Key] = $After[$Key] } } } elseif ($R -eq 'addSpaceToName') { if ($null -ne $Replacement[$R]) { $ReplacementSetting['AddSpaceToName'] = $Replacement[$R] } } elseif ($R -eq 'AfterSplitPositionName') { if ($null -ne $Replacement[$R]) { $ReplacementSetting['AfterSplitPositionName'] = $Replacement[$R] } } else { if ($null -ne $Replacement[$R]) { if ($Replacement[$R].GetType().Name -eq 'SwitchParameter') { $ReplacementSetting[$R] = $Replacement[$R].IsPresent } else { $ReplacementSetting[$R] = $Replacement[$R] } } } } } $ReplacementSetting } function Convert-NestedElements { [CmdletBinding()] param( [ScriptBlock] $Elements, [System.Collections.Generic.List[System.Collections.IDictionary]] $GageConfiguration, [System.Collections.Generic.List[System.Collections.IDictionary]] $FoldersConfiguration, [System.Collections.Generic.List[System.Collections.IDictionary]] $ReplacementConfiguration, [System.Collections.IDictionary] $FolderLimit ) if ($Elements) { $TimeLogElements = Start-TimeLog Write-Color -Text '[i]', "[TheDashboard] ", 'Executing nested elements (data gathering/conversions)', ' [Informative] ' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta try { $OutputElements = & $Elements } catch { Write-Color -Text '[e]', "[TheDashboard] ", 'Failed to execute nested elements', ' [Error] ', $_.Exception.Message -Color Yellow, DarkGray, Yellow, DarkGray, Red return } foreach ($E in $OutputElements) { if ($E.Type -eq 'Gage') { $GageConfiguration.Add($E.Settings) } elseif ($E.Type -eq 'Folder') { $FoldersConfiguration.Add($E.Settings) } elseif ($E.Type -eq 'Replacement') { $ReplacementConfiguration.Add($E.Settings) } elseif ($E.Type -eq 'FolderLimit') { foreach ($Setting in $E.Settings.Keys) { $FolderLimit[$Setting] = $E.Settings[$Setting] } } } $TimeLogElements = Stop-TimeLog -Time $TimeLogElements -Option OneLiner Write-Color -Text '[i]', "[TheDashboard] ", 'Executing nested elements (data gathering/conversions)', ' [Time to execute: ', $TimeLogElements, ']' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta } } function Copy-HTMLFiles { [cmdletbinding()] param( [System.Collections.IDictionary] $Folders, [string] $Extension ) Write-Color -Text '[i]', "[TheDashboard] ", 'Copying or moving HTML files', ' [Informative] ' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta foreach ($FolderName in $Folders.Keys) { if ($Folders[$FolderName].CopyFrom) { foreach ($Path in $Folders[$FolderName].CopyFrom) { Write-Color -Text '[i]', "[TheDashboard] ", "Copying from $Path to $($Folders[$FolderName].Path) using '$Extension'", ' [Informative] ' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta foreach ($File in Get-ChildItem -LiteralPath $Path -Filter "*$Extension" -Recurse) { $Destination = $File.FullName.Replace($Path, $Folders[$FolderName].Path) $DIrectoryName = [io.path]::GetDirectoryName($Destination) $null = New-Item -Path $DIrectoryName -ItemType Directory -Force Copy-Item -LiteralPath $File.FullName -Destination $Destination -Force -ErrorAction Stop } foreach ($Ext in $Folders[$FolderName].Extension) { Write-Color -Text '[i]', "[TheDashboard] ", "Copying from $Path to $($Folders[$FolderName].Path) using '$Ext'", ' [Informative] ' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta foreach ($File in Get-ChildItem -LiteralPath $Path -Filter "*$Ext" -Recurse) { $Destination = $File.FullName.Replace($Path, $Folders[$FolderName].Path) $DIrectoryName = [io.path]::GetDirectoryName($Destination) $null = New-Item -Path $DIrectoryName -ItemType Directory -Force Copy-Item -LiteralPath $File.FullName -Destination $Destination -Force -ErrorAction Stop } } } } if ($Folders[$FolderName].MoveFrom) { foreach ($Path in $Folders[$FolderName].MoveFrom) { Write-Color -Text '[i]', "[TheDashboard] ", "Moving from $Path to $($Folders[$FolderName].Path) using '$Extension'", ' [Informative] ' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta foreach ($File in Get-ChildItem -LiteralPath $Path -Filter "*$Extension" -Recurse) { $Destination = $File.FullName.Replace($Path, $Folders[$FolderName].Path) $DIrectoryName = [io.path]::GetDirectoryName($Destination) $null = New-Item -Path $DIrectoryName -ItemType Directory -Force Move-Item -LiteralPath $File.FullName -Destination $Destination -Force -ErrorAction Stop } foreach ($Ext in $Folders[$FolderName].Extension) { Write-Color -Text '[i]', "[TheDashboard] ", "Moving from $Path to $($Folders[$FolderName].Path) using '$Ext'", ' [Informative] ' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta foreach ($File in Get-ChildItem -LiteralPath $Path -Filter "*$Ext" -Recurse) { $Destination = $File.FullName.Replace($Path, $Folders[$FolderName].Path) $DIrectoryName = [io.path]::GetDirectoryName($Destination) $null = New-Item -Path $DIrectoryName -ItemType Directory -Force Move-Item -LiteralPath $File.FullName -Destination $Destination -Force -ErrorAction Stop } } } } } } function Get-GitHubVersion { [cmdletBinding()] param( [Parameter(Mandatory)][string] $Cmdlet, [Parameter(Mandatory)][string] $RepositoryOwner, [Parameter(Mandatory)][string] $RepositoryName ) $App = Get-Command -Name $Cmdlet -ErrorAction SilentlyContinue if ($App) { [Array] $GitHubReleases = (Get-GitHubLatestRelease -Url "https://api.github.com/repos/$RepositoryOwner/$RepositoryName/releases" -Verbose:$false) $LatestVersion = $GitHubReleases[0] if (-not $LatestVersion.Errors) { if ($App.Version -eq $LatestVersion.Version) { "Current/Latest: $($LatestVersion.Version) at $($LatestVersion.PublishDate)" } elseif ($App.Version -lt $LatestVersion.Version) { "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Update?" } elseif ($App.Version -gt $LatestVersion.Version) { "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Lucky you!" } } else { "Current: $($App.Version)" } } } function Import-DashboardStatistics { [CmdletBinding()] param( [string] $StatisticsPath ) $ExportData = [ordered] @{ MenuBuilder = [ordered] @{} Statistics = [ordered] @{} } if ($StatisticsPath -and (Test-Path -LiteralPath $StatisticsPath)) { Write-Color -Text '[i]', "[TheDashboard] ", 'Importing Statistics', ' [Informative] ', $StatisticsPath -Color Yellow, DarkGray, Yellow, DarkGray, Magenta $ImportedData = Import-Clixml -LiteralPath $StatisticsPath if (-not $ImportedData.Statistics) { $ExportData.Statistics = $ImportedData } else { $ExportData = $ImportedData } } foreach ($E in $GageConfiguration) { $ExportData['Statistics'][$E.Date] = [ordered] @{} $ExportData['Statistics'][$E.Date].Date = $E.Date } $ExportData } function Limit-FilesHistory { <# .SYNOPSIS Controls how files are moved into history based on specified limits. .DESCRIPTION Checks and applies IncludeHistory settings, filtering by date or count. .PARAMETER MenuBuilder Contains the menu structure to which entries will be added. .PARAMETER Entry Represents the current file entry under evaluation. .PARAMETER Limits Holds data about various limits for item, days, and date filtering. .PARAMETER CurrentDate Specifies the current date for time-based comparisons. .EXAMPLE Limit-FilesHistory -MenuBuilder $Builder -Entry $File -Limits $Limits -CurrentDate (Get-Date) .NOTES Part of TheDashboard module, handles history-limit logic for file entries. #> [CmdletBinding()] param( [System.Collections.IDictionary] $MenuBuilder, [PSCustomObject] $Entry, [System.Collections.IDictionary] $Limits, [DateTime] $CurrentDate ) if ($Limits.IncludeHistory) { if ($null -ne $Limits.IncludeHistoryLimit) { if ($MenuBuilder[$Entry.Menu][$Entry.Name]['History'].Count -ge $Limits.IncludeHistoryLimit) { $Entry.Include = $false return } } if ($null -ne $Limits.IncludeHistoryLimitDate) { if ($Entry.Date -lt $Limits.IncludeHistoryLimitDate) { $Entry.Include = $false return } } if ($null -ne $Limits.IncludeHistoryLimitDays) { if ($Entry.Date -lt ($CurrentDate).AddDays(-$Limits.IncludeHistoryLimitDays)) { $Entry.Include = $false return } } $Entry.Include = $true $MenuBuilder[$Entry.Menu][$Entry.Name]['History'].Add($Entry) } } function New-HTMLReport { [cmdletBinding()] param( [Array] $OutputElements, [string] $Logo, [string] $Extension, [System.Collections.IDictionary] $MenuBuilder, [System.Collections.IDictionary] $Configuration, [System.Collections.IDictionary] $ExportData, [Array] $Files, [string] $HTMLPath, [switch] $ShowHTML, [switch] $Online, [Uri] $UrlPath, [switch] $Force, [switch] $Pretend ) $TimeLogHTML = Start-TimeLog Write-Color -Text '[i]', '[HTML ] ', "Generating HTML report ($HTMLPath)" -Color Yellow, DarkGray, Yellow $FilePathsGenerated = [System.Collections.Generic.List[string]]::new() $FilePathsGenerated.Add($HTMLPath) if ($Pretend) { foreach ($Menu in $MenuBuilder.Keys) { Write-Color -Text '[i]', '[HTML ] ', "Building Menu for ", $Menu -Color Yellow, DarkGray, Yellow, DarkCyan $TopMenuSplat = @{ Name = $Menu } if ($Configuration.Folders.$Menu.IconType) { $TopMenuSplat[$Configuration.Folders.$Menu.IconType] = $Configuration.Folders.$Menu.Icon } foreach ($MenuReport in $MenuBuilder[$Menu].Keys | Sort-Object) { $MenuLink = $MenuBuilder[$Menu][$MenuReport]['Current'].MenuLink $PathToSubReports = [io.path]::GetDirectoryName($HTMLPath) $PageName = ($MenuBuilder[$Menu][$MenuReport]['Current'].Name).Replace(":", "_").Replace(" ", "_") $FullPath = [io.path]::Combine($PathToSubReports, "$($MenuLink)_$PageName$($Extension)") $CurrentReport = $MenuBuilder[$Menu][$MenuReport]['Current'] [Array] $FullReports = $MenuBuilder[$Menu][$MenuReport]['Full'] [Array] $HistoryReports = $MenuBuilder[$Menu][$MenuReport]['History'] $Name = $CurrentReport.Name $FilePathsGenerated.Add($FullPath) foreach ($Report in $FullReports) { $FullPathOther = [io.path]::Combine($PathToSubReports, $Report.FileName) $Name = $Report.Name + ' - ' + $Report.Date $FilePathsGenerated.Add($FullPathOther) } } Write-Color -Text '[i]', '[HTML ] ', "Ending Menu for ", $Menu -Color Yellow, DarkGray, Yellow, DarkCyan } } else { New-HTML { $newHTMLNavTopSplat = @{ Logo = $Logo MenuItemsWidth = '250px' NavigationLinks = { foreach ($Menu in $MenuBuilder.Keys) { $TopMenuSplat = @{ Name = $Menu } if ($Configuration.Folders.$Menu.IconType) { $TopMenuSplat[$Configuration.Folders.$Menu.IconType] = $Configuration.Folders.$Menu.Icon } New-NavTopMenu @TopMenuSplat { foreach ($MenuReport in $MenuBuilder[$Menu].Keys | Sort-Object) { $MenuLink = $MenuBuilder[$Menu][$MenuReport]['Current'].MenuLink $PageName = (( -join ($MenuBuilder[$Menu][$MenuReport]['Current'].Name)).Replace(":", "_").Replace(" ", "_")) if ($UrlPath) { New-NavLink -IconRegular calendar-check -Name $MenuBuilder[$Menu][$MenuReport]['Current'].Name -Href "$UrlPath/$($MenuLink)_$($PageName)$($Extension)" } else { New-NavLink -IconRegular calendar-check -Name $MenuBuilder[$Menu][$MenuReport]['Current'].Name -Href "$($MenuLink)_$($PageName)$($Extension)" } } } } } } if ($UrlPath) { $FileNameHome = [System.IO.Path]::GetFileName($HTMLPath) $newHTMLNavTopSplat['HomeLink'] = "$UrlPath/$FileNameHome" } else { $newHTMLNavTopSplat['HomeLinkHome'] = $true } New-HTMLNavTop @newHTMLNavTopSplat New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow New-HTMLPanelStyle -BorderRadius 0px New-HTMLSection -Invisible { foreach ($E in $OutputElements) { New-HTMLPanel { New-HTMLGage -Label $E.Label -MinValue $E.MinValue -MaxValue $E.MaxValue -Value $E.Value -Counter $ExportData['Statistics'][$E.Date][$($E.Label)] = $E.Value } } } New-HTMLSection -Invisible { New-HTMLPanel { $StatisticsKeys = $ExportData['Statistics'].Keys | Sort-Object | Select-Object -Last 50 [Array] $Dates = foreach ($Day in $StatisticsKeys) { $ExportData['Statistics'][$Day].Date } foreach ($UserInput in $Dates) { $ExportData['Statistics'][$Day].$($UserInput.Label) = $UserInput.Value } New-HTMLChart -Title 'Domain Summary' -TitleAlignment center { New-ChartAxisX -Type datetime -Names $Dates New-ChartAxisY -TitleText 'Numbers' -Show foreach ($UserInput in $OutputElements) { $Values = foreach ($Day in $StatisticsKeys) { $ExportData['Statistics'][$Day].$($UserInput.Label) } New-ChartLine -Name $UserInput.Label -Value $Values } } } New-HTMLPanel { New-HTMLCalendar { foreach ($Menu in $MenuBuilder.Keys) { foreach ($MenuReport in $MenuBuilder[$Menu].Keys) { [Array] $FullReports = $MenuBuilder[$Menu][$MenuReport]['Full'] foreach ($CalendarEntry in $FullReports) { if ($CalendarEntry.Include) { if ($($CalendarEntry.Date).Day -eq $($($CalendarEntry.Date).AddMinutes(30)).Day) { New-CalendarEvent -Title $CalendarEntry.Name -StartDate $CalendarEntry.Date -EndDate $($CalendarEntry.Date).AddMinutes(30) -Url $CalendarEntry.FileName } else { New-CalendarEvent -Title $CalendarEntry.Name -StartDate $CalendarEntry.Date.AddMinutes(-30) -EndDate $($CalendarEntry.Date) -Url $CalendarEntry.FileName } } } } } } -HeaderRight @('dayGridMonth', 'timeGridWeek', 'timeGridDay', 'listMonth', 'listYear') } } foreach ($Menu in $MenuBuilder.Keys) { Write-Color -Text '[i]', '[HTML ] ', "Building Menu for ", $Menu -Color Yellow, DarkGray, Yellow, DarkCyan $TopMenuSplat = @{ Name = $Menu } if ($Configuration.Folders.$Menu.IconType) { $TopMenuSplat[$Configuration.Folders.$Menu.IconType] = $Configuration.Folders.$Menu.Icon } foreach ($MenuReport in $MenuBuilder[$Menu].Keys | Sort-Object) { $MenuLink = $MenuBuilder[$Menu][$MenuReport]['Current'].MenuLink $PathToSubReports = [io.path]::GetDirectoryName($HTMLPath) $PageName = ($MenuBuilder[$Menu][$MenuReport]['Current'].Name).Replace(":", "_").Replace(" ", "_") $FullPath = [io.path]::Combine($PathToSubReports, "$($MenuLink)_$PageName$($Extension)") $CurrentReport = $MenuBuilder[$Menu][$MenuReport]['Current'] [Array] $FullReports = $MenuBuilder[$Menu][$MenuReport]['Full'] [Array] $HistoryReports = $MenuBuilder[$Menu][$MenuReport]['History'] $Name = $CurrentReport.Name if (-not $CurrentReport.SkipGeneration) { New-HTMLReportPage -Report $CurrentReport -FullReports $FullReports -HistoryReports $HistoryReports -FilePath $FullPath -PathToSubReports $PathToSubReports -Name $Name } else { Write-Color -Text '[i]', '[HTML ] ', "Skipping generation of ", $FullPath, ". generation not required..." -Color Yellow, DarkGray, Yellow } $FilePathsGenerated.Add($FullPath) foreach ($Report in $FullReports) { if ($Report.Include) { $FullPathOther = [io.path]::Combine($PathToSubReports, $Report.FileName) $Name = $Report.Name + ' - ' + $Report.Date $FilePathsGenerated.Add($FullPathOther) if (-not $Report.SkipGeneration) { New-HTMLReportPage -SubReport -Report $Report -FullReports $FullReports -FilePath $FullPathOther -PathToSubReports $PathToSubReports -Name $Name -HistoryReports $HistoryReports } else { Write-Color -Text '[i]', '[HTML ] ', "Skipping generation of ", $FullPathOther, ". generation not required..." -Color Yellow, DarkGray, Yellow } } } } Write-Color -Text '[i]', '[HTML ] ', "Ending Menu for ", $Menu -Color Yellow, DarkGray, Yellow, DarkCyan } Write-Color -Text '[i]', '[HTML ] ', "Saving HTML reports (this may take a while...)" -Color Yellow, DarkGray, Yellow } -FilePath $HTMLPath -ShowHTML:$ShowHTML.IsPresent -TitleText 'The Dashboard' -Online:$Online.IsPresent -Author "Przemyslaw Klys @ Evotec" } $TimeLogEndHTML = Stop-TimeLog -Time $TimeLogHTML -Option OneLiner Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report', " [Time to execute: $TimeLogEndHTML]" -Color Yellow, DarkGray, Yellow, DarkGray $FilePathsGenerated } function New-HTMLReportPage { [cmdletBinding()] param( [PSCustomObject] $Report, [Array] $FullReports, [Array] $HistoryReports, [string] $FilePath, [string] $PathToSubReports, [string] $Name, [switch] $SubReport ) if ($SubReport) { Write-Color -Text '[i]', '[HTML ] ', "Generating HTML page ($MenuReport) sub report ($FilePath)" -Color Yellow, DarkGray, Yellow } else { Write-Color -Text '[i]', '[HTML ] ', "Generating HTML page ($MenuReport) report ($FilePath)" -Color Yellow, DarkGray, Yellow } New-HTMLPage -Name $Name -Title "The Dashboard - $Name" { New-HTMLSection -HeaderText "Summary for $($Report.Name)" -HeaderBackGroundColor Black { New-HTMLSection -Invisible { New-HTMLPanel { New-HTMLText -Text "Report name: ", $CurrentReport.Name -FontSize 12px New-HTMLText -Text "Report date: ", $CurrentReport.Date -FontSize 12px New-HTMLText -Text "All reports in this catagory: ", $FullReports.Count -FontSize 12px New-HTMLList { if ($FullReports.Count -eq 1) { New-HTMLListItem -Text "Date in report: ", $FullReports[0].Date -FontSize 12px -FontWeight normal, bold -TextDecoration none, underline } else { New-HTMLListItem -Text "Date ranges from: ", $FullReports[$FullReports.Count - 1].Date, " to ", $FullReports[0].Date -FontSize 12px -FontWeight normal, bold, normal, bold -TextDecoration none, underline, none, underline } } if ($HistoryReports.Count) { New-HTMLText -Text "History reports in this catagory: ", $HistoryReports.Count -FontSize 12px New-HTMLList { if ($HistoryReports.Count -gt 1) { New-HTMLListItem -Text "Date ranges from: ", $HistoryReports[$FullReports.Count - 1].Date, " to ", $HistoryReports[0].Date -FontSize 12px -FontWeight normal, bold, normal, bold -TextDecoration none, underline, none, underline } else { New-HTMLListItem -Text "Date in report: ", $HistoryReports[0].Date -FontSize 12px -FontWeight normal, bold -TextDecoration none, underline } } } } -Invisible } New-HTMLSection -Invisible { New-HTMLCalendar { foreach ($CalendarEntry in $FullReports) { if ($($CalendarEntry.Date).Day -eq $($($CalendarEntry.Date).AddMinutes(30)).Day) { New-CalendarEvent -Title $CalendarEntry.Name -StartDate $CalendarEntry.Date -EndDate $($CalendarEntry.Date).AddMinutes(30) -Url $CalendarEntry.FileName } else { New-CalendarEvent -Title $CalendarEntry.Name -StartDate $CalendarEntry.Date.AddMinutes(-30) -EndDate $($CalendarEntry.Date) -Url $CalendarEntry.FileName } } foreach ($CalendarEntry in $HistoryReports) { if ($($CalendarEntry.Date).Day -eq $($($CalendarEntry.Date).AddMinutes(30)).Day) { New-CalendarEvent -Title $CalendarEntry.Name -StartDate $CalendarEntry.Date -EndDate $($CalendarEntry.Date).AddMinutes(30) -Url $CalendarEntry.Href -TargetName 'iFrameWithContent' } else { New-CalendarEvent -Title $CalendarEntry.Name -StartDate $CalendarEntry.Date.AddMinutes(-30) -EndDate $($CalendarEntry.Date) -Url $CalendarEntry.Href -TargetName 'iFrameWithContent' } } } -HeaderRight @('dayGridMonth', 'timeGridWeek', 'timeGridDay', 'listMonth', 'listYear') } } -Height 300px New-HTMLSection -Invisible { } -Height 15px New-HTMLFrame -SourcePath $Report.Href -Scrolling Auto -Height 2000px -Name 'iFrameWithContent' } -FilePath $FilePath } $Global:HTMLIcons = [ordered] @{ FontAwesomeBrands = [ordered] @{ '500px' = 'f26e' 'accessible-icon' = 'f368' 'accusoft' = 'f369' 'acquisitions-incorporated' = 'f6af' 'adn' = 'f170' 'adversal' = 'f36a' 'affiliatetheme' = 'f36b' 'airbnb' = 'f834' 'algolia' = 'f36c' 'alipay' = 'f642' 'amazon' = 'f270' 'amazon-pay' = 'f42c' 'amilia' = 'f36d' 'android' = 'f17b' 'angellist' = 'f209' 'angrycreative' = 'f36e' 'angular' = 'f420' 'app-store' = 'f36f' 'app-store-ios' = 'f370' 'apper' = 'f371' 'apple' = 'f179' 'apple-pay' = 'f415' 'artstation' = 'f77a' 'asymmetrik' = 'f372' 'atlassian' = 'f77b' 'audible' = 'f373' 'autoprefixer' = 'f41c' 'avianex' = 'f374' 'aviato' = 'f421' 'aws' = 'f375' 'bandcamp' = 'f2d5' 'battle-net' = 'f835' 'behance' = 'f1b4' 'behance-square' = 'f1b5' 'bimobject' = 'f378' 'bitbucket' = 'f171' 'bitcoin' = 'f379' 'bity' = 'f37a' 'black-tie' = 'f27e' 'blackberry' = 'f37b' 'blogger' = 'f37c' 'blogger-b' = 'f37d' 'bluetooth' = 'f293' 'bluetooth-b' = 'f294' 'bootstrap' = 'f836' 'btc' = 'f15a' 'buffer' = 'f837' 'buromobelexperte' = 'f37f' 'buy-n-large' = 'f8a6' 'canadian-maple-leaf' = 'f785' 'cc-amazon-pay' = 'f42d' 'cc-amex' = 'f1f3' 'cc-apple-pay' = 'f416' 'cc-diners-club' = 'f24c' 'cc-discover' = 'f1f2' 'cc-jcb' = 'f24b' 'cc-mastercard' = 'f1f1' 'cc-paypal' = 'f1f4' 'cc-stripe' = 'f1f5' 'cc-visa' = 'f1f0' 'centercode' = 'f380' 'centos' = 'f789' 'chrome' = 'f268' 'chromecast' = 'f838' 'cloudflare' = 'e07d' 'cloudscale' = 'f383' 'cloudsmith' = 'f384' 'cloudversify' = 'f385' 'codepen' = 'f1cb' 'codiepie' = 'f284' 'confluence' = 'f78d' 'connectdevelop' = 'f20e' 'contao' = 'f26d' 'cotton-bureau' = 'f89e' 'cpanel' = 'f388' 'creative-commons' = 'f25e' 'creative-commons-by' = 'f4e7' 'creative-commons-nc' = 'f4e8' 'creative-commons-nc-eu' = 'f4e9' 'creative-commons-nc-jp' = 'f4ea' 'creative-commons-nd' = 'f4eb' 'creative-commons-pd' = 'f4ec' 'creative-commons-pd-alt' = 'f4ed' 'creative-commons-remix' = 'f4ee' 'creative-commons-sa' = 'f4ef' 'creative-commons-sampling' = 'f4f0' 'creative-commons-sampling-plus' = 'f4f1' 'creative-commons-share' = 'f4f2' 'creative-commons-zero' = 'f4f3' 'critical-role' = 'f6c9' 'css3' = 'f13c' 'css3-alt' = 'f38b' 'cuttlefish' = 'f38c' 'd-and-d' = 'f38d' 'd-and-d-beyond' = 'f6ca' 'dailymotion' = 'e052' 'dashcube' = 'f210' 'deezer' = 'e077' 'delicious' = 'f1a5' 'deploydog' = 'f38e' 'deskpro' = 'f38f' 'dev' = 'f6cc' 'deviantart' = 'f1bd' 'dhl' = 'f790' 'diaspora' = 'f791' 'digg' = 'f1a6' 'digital-ocean' = 'f391' 'discord' = 'f392' 'discourse' = 'f393' 'dochub' = 'f394' 'docker' = 'f395' 'draft2digital' = 'f396' 'dribbble' = 'f17d' 'dribbble-square' = 'f397' 'dropbox' = 'f16b' 'drupal' = 'f1a9' 'dyalog' = 'f399' 'earlybirds' = 'f39a' 'ebay' = 'f4f4' 'edge' = 'f282' 'edge-legacy' = 'e078' 'elementor' = 'f430' 'ello' = 'f5f1' 'ember' = 'f423' 'empire' = 'f1d1' 'envira' = 'f299' 'erlang' = 'f39d' 'ethereum' = 'f42e' 'etsy' = 'f2d7' 'evernote' = 'f839' 'expeditedssl' = 'f23e' 'facebook' = 'f09a' 'facebook-f' = 'f39e' 'facebook-messenger' = 'f39f' 'facebook-square' = 'f082' 'fantasy-flight-games' = 'f6dc' 'fedex' = 'f797' 'fedora' = 'f798' 'figma' = 'f799' 'firefox' = 'f269' 'firefox-browser' = 'e007' 'first-order' = 'f2b0' 'first-order-alt' = 'f50a' 'firstdraft' = 'f3a1' 'flickr' = 'f16e' 'flipboard' = 'f44d' 'fly' = 'f417' 'font-awesome' = 'f2b4' 'font-awesome-alt' = 'f35c' 'font-awesome-flag' = 'f425' 'fonticons' = 'f280' 'fonticons-fi' = 'f3a2' 'fort-awesome' = 'f286' 'fort-awesome-alt' = 'f3a3' 'forumbee' = 'f211' 'foursquare' = 'f180' 'free-code-camp' = 'f2c5' 'freebsd' = 'f3a4' 'fulcrum' = 'f50b' 'galactic-republic' = 'f50c' 'galactic-senate' = 'f50d' 'get-pocket' = 'f265' 'gg' = 'f260' 'gg-circle' = 'f261' 'git' = 'f1d3' 'git-alt' = 'f841' 'git-square' = 'f1d2' 'github' = 'f09b' 'github-alt' = 'f113' 'github-square' = 'f092' 'gitkraken' = 'f3a6' 'gitlab' = 'f296' 'gitter' = 'f426' 'glide' = 'f2a5' 'glide-g' = 'f2a6' 'gofore' = 'f3a7' 'goodreads' = 'f3a8' 'goodreads-g' = 'f3a9' 'google' = 'f1a0' 'google-drive' = 'f3aa' 'google-pay' = 'e079' 'google-play' = 'f3ab' 'google-plus' = 'f2b3' 'google-plus-g' = 'f0d5' 'google-plus-square' = 'f0d4' 'google-wallet' = 'f1ee' 'gratipay' = 'f184' 'grav' = 'f2d6' 'gripfire' = 'f3ac' 'grunt' = 'f3ad' 'guilded' = 'e07e' 'gulp' = 'f3ae' 'hacker-news' = 'f1d4' 'hacker-news-square' = 'f3af' 'hackerrank' = 'f5f7' 'hips' = 'f452' 'hire-a-helper' = 'f3b0' 'hive' = 'e07f' 'hooli' = 'f427' 'hornbill' = 'f592' 'hotjar' = 'f3b1' 'houzz' = 'f27c' 'html5' = 'f13b' 'hubspot' = 'f3b2' 'ideal' = 'e013' 'imdb' = 'f2d8' 'innosoft' = 'e080' 'instagram' = 'f16d' 'instagram-square' = 'e055' 'instalod' = 'e081' 'intercom' = 'f7af' 'internet-explorer' = 'f26b' 'invision' = 'f7b0' 'ioxhost' = 'f208' 'itch-io' = 'f83a' 'itunes' = 'f3b4' 'itunes-note' = 'f3b5' 'java' = 'f4e4' 'jedi-order' = 'f50e' 'jenkins' = 'f3b6' 'jira' = 'f7b1' 'joget' = 'f3b7' 'joomla' = 'f1aa' 'js' = 'f3b8' 'js-square' = 'f3b9' 'jsfiddle' = 'f1cc' 'kaggle' = 'f5fa' 'keybase' = 'f4f5' 'keycdn' = 'f3ba' 'kickstarter' = 'f3bb' 'kickstarter-k' = 'f3bc' 'korvue' = 'f42f' 'laravel' = 'f3bd' 'lastfm' = 'f202' 'lastfm-square' = 'f203' 'leanpub' = 'f212' 'less' = 'f41d' 'line' = 'f3c0' 'linkedin' = 'f08c' 'linkedin-in' = 'f0e1' 'linode' = 'f2b8' 'linux' = 'f17c' 'lyft' = 'f3c3' 'magento' = 'f3c4' 'mailchimp' = 'f59e' 'mandalorian' = 'f50f' 'markdown' = 'f60f' 'mastodon' = 'f4f6' 'maxcdn' = 'f136' 'mdb' = 'f8ca' 'medapps' = 'f3c6' 'medium' = 'f23a' 'medium-m' = 'f3c7' 'medrt' = 'f3c8' 'meetup' = 'f2e0' 'megaport' = 'f5a3' 'mendeley' = 'f7b3' 'microblog' = 'e01a' 'microsoft' = 'f3ca' 'mix' = 'f3cb' 'mixcloud' = 'f289' 'mixer' = 'e056' 'mizuni' = 'f3cc' 'modx' = 'f285' 'monero' = 'f3d0' 'napster' = 'f3d2' 'neos' = 'f612' 'nimblr' = 'f5a8' 'node' = 'f419' 'node-js' = 'f3d3' 'npm' = 'f3d4' 'ns8' = 'f3d5' 'nutritionix' = 'f3d6' 'octopus-deploy' = 'e082' 'odnoklassniki' = 'f263' 'odnoklassniki-square' = 'f264' 'old-republic' = 'f510' 'opencart' = 'f23d' 'openid' = 'f19b' 'opera' = 'f26a' 'optin-monster' = 'f23c' 'orcid' = 'f8d2' 'osi' = 'f41a' 'page4' = 'f3d7' 'pagelines' = 'f18c' 'palfed' = 'f3d8' 'patreon' = 'f3d9' 'paypal' = 'f1ed' 'penny-arcade' = 'f704' 'perbyte' = 'e083' 'periscope' = 'f3da' 'phabricator' = 'f3db' 'phoenix-framework' = 'f3dc' 'phoenix-squadron' = 'f511' 'php' = 'f457' 'pied-piper' = 'f2ae' 'pied-piper-alt' = 'f1a8' 'pied-piper-hat' = 'f4e5' 'pied-piper-pp' = 'f1a7' 'pied-piper-square' = 'e01e' 'pinterest' = 'f0d2' 'pinterest-p' = 'f231' 'pinterest-square' = 'f0d3' 'playstation' = 'f3df' 'product-hunt' = 'f288' 'pushed' = 'f3e1' 'python' = 'f3e2' 'qq' = 'f1d6' 'quinscape' = 'f459' 'quora' = 'f2c4' 'r-project' = 'f4f7' 'raspberry-pi' = 'f7bb' 'ravelry' = 'f2d9' 'react' = 'f41b' 'reacteurope' = 'f75d' 'readme' = 'f4d5' 'rebel' = 'f1d0' 'red-river' = 'f3e3' 'reddit' = 'f1a1' 'reddit-alien' = 'f281' 'reddit-square' = 'f1a2' 'redhat' = 'f7bc' 'renren' = 'f18b' 'replyd' = 'f3e6' 'researchgate' = 'f4f8' 'resolving' = 'f3e7' 'rev' = 'f5b2' 'rocketchat' = 'f3e8' 'rockrms' = 'f3e9' 'rust' = 'e07a' 'safari' = 'f267' 'salesforce' = 'f83b' 'sass' = 'f41e' 'schlix' = 'f3ea' 'scribd' = 'f28a' 'searchengin' = 'f3eb' 'sellcast' = 'f2da' 'sellsy' = 'f213' 'servicestack' = 'f3ec' 'shirtsinbulk' = 'f214' 'shopify' = 'e057' 'shopware' = 'f5b5' 'simplybuilt' = 'f215' 'sistrix' = 'f3ee' 'sith' = 'f512' 'sketch' = 'f7c6' 'skyatlas' = 'f216' 'skype' = 'f17e' 'slack' = 'f198' 'slack-hash' = 'f3ef' 'slideshare' = 'f1e7' 'snapchat' = 'f2ab' 'snapchat-ghost' = 'f2ac' 'snapchat-square' = 'f2ad' 'soundcloud' = 'f1be' 'sourcetree' = 'f7d3' 'speakap' = 'f3f3' 'speaker-deck' = 'f83c' 'spotify' = 'f1bc' 'squarespace' = 'f5be' 'stack-exchange' = 'f18d' 'stack-overflow' = 'f16c' 'stackpath' = 'f842' 'staylinked' = 'f3f5' 'steam' = 'f1b6' 'steam-square' = 'f1b7' 'steam-symbol' = 'f3f6' 'sticker-mule' = 'f3f7' 'strava' = 'f428' 'stripe' = 'f429' 'stripe-s' = 'f42a' 'studiovinari' = 'f3f8' 'stumbleupon' = 'f1a4' 'stumbleupon-circle' = 'f1a3' 'superpowers' = 'f2dd' 'supple' = 'f3f9' 'suse' = 'f7d6' 'swift' = 'f8e1' 'symfony' = 'f83d' 'teamspeak' = 'f4f9' 'telegram' = 'f2c6' 'telegram-plane' = 'f3fe' 'tencent-weibo' = 'f1d5' 'the-red-yeti' = 'f69d' 'themeco' = 'f5c6' 'themeisle' = 'f2b2' 'think-peaks' = 'f731' 'tiktok' = 'e07b' 'trade-federation' = 'f513' 'trello' = 'f181' 'tripadvisor' = 'f262' 'tumblr' = 'f173' 'tumblr-square' = 'f174' 'twitch' = 'f1e8' 'twitter' = 'f099' 'twitter-square' = 'f081' 'typo3' = 'f42b' 'uber' = 'f402' 'ubuntu' = 'f7df' 'uikit' = 'f403' 'umbraco' = 'f8e8' 'uncharted' = 'e084' 'uniregistry' = 'f404' 'unity' = 'e049' 'unsplash' = 'e07c' 'untappd' = 'f405' 'ups' = 'f7e0' 'usb' = 'f287' 'usps' = 'f7e1' 'ussunnah' = 'f407' 'vaadin' = 'f408' 'viacoin' = 'f237' 'viadeo' = 'f2a9' 'viadeo-square' = 'f2aa' 'viber' = 'f409' 'vimeo' = 'f40a' 'vimeo-square' = 'f194' 'vimeo-v' = 'f27d' 'vine' = 'f1ca' 'vk' = 'f189' 'vnv' = 'f40b' 'vuejs' = 'f41f' 'watchman-monitoring' = 'e087' 'waze' = 'f83f' 'weebly' = 'f5cc' 'weibo' = 'f18a' 'weixin' = 'f1d7' 'whatsapp' = 'f232' 'whatsapp-square' = 'f40c' 'whmcs' = 'f40d' 'wikipedia-w' = 'f266' 'windows' = 'f17a' 'wix' = 'f5cf' 'wizards-of-the-coast' = 'f730' 'wodu' = 'e088' 'wolf-pack-battalion' = 'f514' 'wordpress' = 'f19a' 'wordpress-simple' = 'f411' 'wpbeginner' = 'f297' 'wpexplorer' = 'f2de' 'wpforms' = 'f298' 'wpressr' = 'f3e4' 'xbox' = 'f412' 'xing' = 'f168' 'xing-square' = 'f169' 'y-combinator' = 'f23b' 'yahoo' = 'f19e' 'yammer' = 'f840' 'yandex' = 'f413' 'yandex-international' = 'f414' 'yarn' = 'f7e3' 'yelp' = 'f1e9' 'yoast' = 'f2b1' 'youtube' = 'f167' 'youtube-square' = 'f431' 'zhihu' = 'f63f' } FontAwesomeRegular = [ordered] @{ 'address-book' = 'f2b9' 'address-card' = 'f2bb' 'angry' = 'f556' 'arrow-alt-circle-down' = 'f358' 'arrow-alt-circle-left' = 'f359' 'arrow-alt-circle-right' = 'f35a' 'arrow-alt-circle-up' = 'f35b' 'bell' = 'f0f3' 'bell-slash' = 'f1f6' 'bookmark' = 'f02e' 'building' = 'f1ad' 'calendar' = 'f133' 'calendar-alt' = 'f073' 'calendar-check' = 'f274' 'calendar-minus' = 'f272' 'calendar-plus' = 'f271' 'calendar-times' = 'f273' 'caret-square-down' = 'f150' 'caret-square-left' = 'f191' 'caret-square-right' = 'f152' 'caret-square-up' = 'f151' 'chart-bar' = 'f080' 'check-circle' = 'f058' 'check-square' = 'f14a' 'circle' = 'f111' 'clipboard' = 'f328' 'clock' = 'f017' 'clone' = 'f24d' 'closed-captioning' = 'f20a' 'comment' = 'f075' 'comment-alt' = 'f27a' 'comment-dots' = 'f4ad' 'comments' = 'f086' 'compass' = 'f14e' 'copy' = 'f0c5' 'copyright' = 'f1f9' 'credit-card' = 'f09d' 'dizzy' = 'f567' 'dot-circle' = 'f192' 'edit' = 'f044' 'envelope' = 'f0e0' 'envelope-open' = 'f2b6' 'eye' = 'f06e' 'eye-slash' = 'f070' 'file' = 'f15b' 'file-alt' = 'f15c' 'file-archive' = 'f1c6' 'file-audio' = 'f1c7' 'file-code' = 'f1c9' 'file-excel' = 'f1c3' 'file-image' = 'f1c5' 'file-pdf' = 'f1c1' 'file-powerpoint' = 'f1c4' 'file-video' = 'f1c8' 'file-word' = 'f1c2' 'flag' = 'f024' 'flushed' = 'f579' 'folder' = 'f07b' 'folder-open' = 'f07c' 'frown' = 'f119' 'frown-open' = 'f57a' 'futbol' = 'f1e3' 'gem' = 'f3a5' 'grimace' = 'f57f' 'grin' = 'f580' 'grin-alt' = 'f581' 'grin-beam' = 'f582' 'grin-beam-sweat' = 'f583' 'grin-hearts' = 'f584' 'grin-squint' = 'f585' 'grin-squint-tears' = 'f586' 'grin-stars' = 'f587' 'grin-tears' = 'f588' 'grin-tongue' = 'f589' 'grin-tongue-squint' = 'f58a' 'grin-tongue-wink' = 'f58b' 'grin-wink' = 'f58c' 'hand-lizard' = 'f258' 'hand-paper' = 'f256' 'hand-peace' = 'f25b' 'hand-point-down' = 'f0a7' 'hand-point-left' = 'f0a5' 'hand-point-right' = 'f0a4' 'hand-point-up' = 'f0a6' 'hand-pointer' = 'f25a' 'hand-rock' = 'f255' 'hand-scissors' = 'f257' 'hand-spock' = 'f259' 'handshake' = 'f2b5' 'hdd' = 'f0a0' 'heart' = 'f004' 'hospital' = 'f0f8' 'hourglass' = 'f254' 'id-badge' = 'f2c1' 'id-card' = 'f2c2' 'image' = 'f03e' 'images' = 'f302' 'keyboard' = 'f11c' 'kiss' = 'f596' 'kiss-beam' = 'f597' 'kiss-wink-heart' = 'f598' 'laugh' = 'f599' 'laugh-beam' = 'f59a' 'laugh-squint' = 'f59b' 'laugh-wink' = 'f59c' 'lemon' = 'f094' 'life-ring' = 'f1cd' 'lightbulb' = 'f0eb' 'list-alt' = 'f022' 'map' = 'f279' 'meh' = 'f11a' 'meh-blank' = 'f5a4' 'meh-rolling-eyes' = 'f5a5' 'minus-square' = 'f146' 'money-bill-alt' = 'f3d1' 'moon' = 'f186' 'newspaper' = 'f1ea' 'object-group' = 'f247' 'object-ungroup' = 'f248' 'paper-plane' = 'f1d8' 'pause-circle' = 'f28b' 'play-circle' = 'f144' 'plus-square' = 'f0fe' 'question-circle' = 'f059' 'registered' = 'f25d' 'sad-cry' = 'f5b3' 'sad-tear' = 'f5b4' 'save' = 'f0c7' 'share-square' = 'f14d' 'smile' = 'f118' 'smile-beam' = 'f5b8' 'smile-wink' = 'f4da' 'snowflake' = 'f2dc' 'square' = 'f0c8' 'star' = 'f005' 'star-half' = 'f089' 'sticky-note' = 'f249' 'stop-circle' = 'f28d' 'sun' = 'f185' 'surprise' = 'f5c2' 'thumbs-down' = 'f165' 'thumbs-up' = 'f164' 'times-circle' = 'f057' 'tired' = 'f5c8' 'trash-alt' = 'f2ed' 'user' = 'f007' 'user-circle' = 'f2bd' 'window-close' = 'f410' 'window-maximize' = 'f2d0' 'window-minimize' = 'f2d1' 'window-restore' = 'f2d2' } FontAwesomeSolid = [ordered] @{ 'ad' = 'f641' 'address-book' = 'f2b9' 'address-card' = 'f2bb' 'adjust' = 'f042' 'air-freshener' = 'f5d0' 'align-center' = 'f037' 'align-justify' = 'f039' 'align-left' = 'f036' 'align-right' = 'f038' 'allergies' = 'f461' 'ambulance' = 'f0f9' 'american-sign-language-interpreting' = 'f2a3' 'anchor' = 'f13d' 'angle-double-down' = 'f103' 'angle-double-left' = 'f100' 'angle-double-right' = 'f101' 'angle-double-up' = 'f102' 'angle-down' = 'f107' 'angle-left' = 'f104' 'angle-right' = 'f105' 'angle-up' = 'f106' 'angry' = 'f556' 'ankh' = 'f644' 'apple-alt' = 'f5d1' 'archive' = 'f187' 'archway' = 'f557' 'arrow-alt-circle-down' = 'f358' 'arrow-alt-circle-left' = 'f359' 'arrow-alt-circle-right' = 'f35a' 'arrow-alt-circle-up' = 'f35b' 'arrow-circle-down' = 'f0ab' 'arrow-circle-left' = 'f0a8' 'arrow-circle-right' = 'f0a9' 'arrow-circle-up' = 'f0aa' 'arrow-down' = 'f063' 'arrow-left' = 'f060' 'arrow-right' = 'f061' 'arrow-up' = 'f062' 'arrows-alt' = 'f0b2' 'arrows-alt-h' = 'f337' 'arrows-alt-v' = 'f338' 'assistive-listening-systems' = 'f2a2' 'asterisk' = 'f069' 'at' = 'f1fa' 'atlas' = 'f558' 'atom' = 'f5d2' 'audio-description' = 'f29e' 'award' = 'f559' 'baby' = 'f77c' 'baby-carriage' = 'f77d' 'backspace' = 'f55a' 'backward' = 'f04a' 'bacon' = 'f7e5' 'bacteria' = 'e059' 'bacterium' = 'e05a' 'bahai' = 'f666' 'balance-scale' = 'f24e' 'balance-scale-left' = 'f515' 'balance-scale-right' = 'f516' 'ban' = 'f05e' 'band-aid' = 'f462' 'barcode' = 'f02a' 'bars' = 'f0c9' 'baseball-ball' = 'f433' 'basketball-ball' = 'f434' 'bath' = 'f2cd' 'battery-empty' = 'f244' 'battery-full' = 'f240' 'battery-half' = 'f242' 'battery-quarter' = 'f243' 'battery-three-quarters' = 'f241' 'bed' = 'f236' 'beer' = 'f0fc' 'bell' = 'f0f3' 'bell-slash' = 'f1f6' 'bezier-curve' = 'f55b' 'bible' = 'f647' 'bicycle' = 'f206' 'biking' = 'f84a' 'binoculars' = 'f1e5' 'biohazard' = 'f780' 'birthday-cake' = 'f1fd' 'blender' = 'f517' 'blender-phone' = 'f6b6' 'blind' = 'f29d' 'blog' = 'f781' 'bold' = 'f032' 'bolt' = 'f0e7' 'bomb' = 'f1e2' 'bone' = 'f5d7' 'bong' = 'f55c' 'book' = 'f02d' 'book-dead' = 'f6b7' 'book-medical' = 'f7e6' 'book-open' = 'f518' 'book-reader' = 'f5da' 'bookmark' = 'f02e' 'border-all' = 'f84c' 'border-none' = 'f850' 'border-style' = 'f853' 'bowling-ball' = 'f436' 'box' = 'f466' 'box-open' = 'f49e' 'box-tissue' = 'e05b' 'boxes' = 'f468' 'braille' = 'f2a1' 'brain' = 'f5dc' 'bread-slice' = 'f7ec' 'briefcase' = 'f0b1' 'briefcase-medical' = 'f469' 'broadcast-tower' = 'f519' 'broom' = 'f51a' 'brush' = 'f55d' 'bug' = 'f188' 'building' = 'f1ad' 'bullhorn' = 'f0a1' 'bullseye' = 'f140' 'burn' = 'f46a' 'bus' = 'f207' 'bus-alt' = 'f55e' 'business-time' = 'f64a' 'calculator' = 'f1ec' 'calendar' = 'f133' 'calendar-alt' = 'f073' 'calendar-check' = 'f274' 'calendar-day' = 'f783' 'calendar-minus' = 'f272' 'calendar-plus' = 'f271' 'calendar-times' = 'f273' 'calendar-week' = 'f784' 'camera' = 'f030' 'camera-retro' = 'f083' 'campground' = 'f6bb' 'candy-cane' = 'f786' 'cannabis' = 'f55f' 'capsules' = 'f46b' 'car' = 'f1b9' 'car-alt' = 'f5de' 'car-battery' = 'f5df' 'car-crash' = 'f5e1' 'car-side' = 'f5e4' 'caravan' = 'f8ff' 'caret-down' = 'f0d7' 'caret-left' = 'f0d9' 'caret-right' = 'f0da' 'caret-square-down' = 'f150' 'caret-square-left' = 'f191' 'caret-square-right' = 'f152' 'caret-square-up' = 'f151' 'caret-up' = 'f0d8' 'carrot' = 'f787' 'cart-arrow-down' = 'f218' 'cart-plus' = 'f217' 'cash-register' = 'f788' 'cat' = 'f6be' 'certificate' = 'f0a3' 'chair' = 'f6c0' 'chalkboard' = 'f51b' 'chalkboard-teacher' = 'f51c' 'charging-station' = 'f5e7' 'chart-area' = 'f1fe' 'chart-bar' = 'f080' 'chart-line' = 'f201' 'chart-pie' = 'f200' 'check' = 'f00c' 'check-circle' = 'f058' 'check-double' = 'f560' 'check-square' = 'f14a' 'cheese' = 'f7ef' 'chess' = 'f439' 'chess-bishop' = 'f43a' 'chess-board' = 'f43c' 'chess-king' = 'f43f' 'chess-knight' = 'f441' 'chess-pawn' = 'f443' 'chess-queen' = 'f445' 'chess-rook' = 'f447' 'chevron-circle-down' = 'f13a' 'chevron-circle-left' = 'f137' 'chevron-circle-right' = 'f138' 'chevron-circle-up' = 'f139' 'chevron-down' = 'f078' 'chevron-left' = 'f053' 'chevron-right' = 'f054' 'chevron-up' = 'f077' 'child' = 'f1ae' 'church' = 'f51d' 'circle' = 'f111' 'circle-notch' = 'f1ce' 'city' = 'f64f' 'clinic-medical' = 'f7f2' 'clipboard' = 'f328' 'clipboard-check' = 'f46c' 'clipboard-list' = 'f46d' 'clock' = 'f017' 'clone' = 'f24d' 'closed-captioning' = 'f20a' 'cloud' = 'f0c2' 'cloud-download-alt' = 'f381' 'cloud-meatball' = 'f73b' 'cloud-moon' = 'f6c3' 'cloud-moon-rain' = 'f73c' 'cloud-rain' = 'f73d' 'cloud-showers-heavy' = 'f740' 'cloud-sun' = 'f6c4' 'cloud-sun-rain' = 'f743' 'cloud-upload-alt' = 'f382' 'cocktail' = 'f561' 'code' = 'f121' 'code-branch' = 'f126' 'coffee' = 'f0f4' 'cog' = 'f013' 'cogs' = 'f085' 'coins' = 'f51e' 'columns' = 'f0db' 'comment' = 'f075' 'comment-alt' = 'f27a' 'comment-dollar' = 'f651' 'comment-dots' = 'f4ad' 'comment-medical' = 'f7f5' 'comment-slash' = 'f4b3' 'comments' = 'f086' 'comments-dollar' = 'f653' 'compact-disc' = 'f51f' 'compass' = 'f14e' 'compress' = 'f066' 'compress-alt' = 'f422' 'compress-arrows-alt' = 'f78c' 'concierge-bell' = 'f562' 'cookie' = 'f563' 'cookie-bite' = 'f564' 'copy' = 'f0c5' 'copyright' = 'f1f9' 'couch' = 'f4b8' 'credit-card' = 'f09d' 'crop' = 'f125' 'crop-alt' = 'f565' 'cross' = 'f654' 'crosshairs' = 'f05b' 'crow' = 'f520' 'crown' = 'f521' 'crutch' = 'f7f7' 'cube' = 'f1b2' 'cubes' = 'f1b3' 'cut' = 'f0c4' 'database' = 'f1c0' 'deaf' = 'f2a4' 'democrat' = 'f747' 'desktop' = 'f108' 'dharmachakra' = 'f655' 'diagnoses' = 'f470' 'dice' = 'f522' 'dice-d20' = 'f6cf' 'dice-d6' = 'f6d1' 'dice-five' = 'f523' 'dice-four' = 'f524' 'dice-one' = 'f525' 'dice-six' = 'f526' 'dice-three' = 'f527' 'dice-two' = 'f528' 'digital-tachograph' = 'f566' 'directions' = 'f5eb' 'disease' = 'f7fa' 'divide' = 'f529' 'dizzy' = 'f567' 'dna' = 'f471' 'dog' = 'f6d3' 'dollar-sign' = 'f155' 'dolly' = 'f472' 'dolly-flatbed' = 'f474' 'donate' = 'f4b9' 'door-closed' = 'f52a' 'door-open' = 'f52b' 'dot-circle' = 'f192' 'dove' = 'f4ba' 'download' = 'f019' 'drafting-compass' = 'f568' 'dragon' = 'f6d5' 'draw-polygon' = 'f5ee' 'drum' = 'f569' 'drum-steelpan' = 'f56a' 'drumstick-bite' = 'f6d7' 'dumbbell' = 'f44b' 'dumpster' = 'f793' 'dumpster-fire' = 'f794' 'dungeon' = 'f6d9' 'edit' = 'f044' 'egg' = 'f7fb' 'eject' = 'f052' 'ellipsis-h' = 'f141' 'ellipsis-v' = 'f142' 'envelope' = 'f0e0' 'envelope-open' = 'f2b6' 'envelope-open-text' = 'f658' 'envelope-square' = 'f199' 'equals' = 'f52c' 'eraser' = 'f12d' 'ethernet' = 'f796' 'euro-sign' = 'f153' 'exchange-alt' = 'f362' 'exclamation' = 'f12a' 'exclamation-circle' = 'f06a' 'exclamation-triangle' = 'f071' 'expand' = 'f065' 'expand-alt' = 'f424' 'expand-arrows-alt' = 'f31e' 'external-link-alt' = 'f35d' 'external-link-square-alt' = 'f360' 'eye' = 'f06e' 'eye-dropper' = 'f1fb' 'eye-slash' = 'f070' 'fan' = 'f863' 'fast-backward' = 'f049' 'fast-forward' = 'f050' 'faucet' = 'e005' 'fax' = 'f1ac' 'feather' = 'f52d' 'feather-alt' = 'f56b' 'female' = 'f182' 'fighter-jet' = 'f0fb' 'file' = 'f15b' 'file-alt' = 'f15c' 'file-archive' = 'f1c6' 'file-audio' = 'f1c7' 'file-code' = 'f1c9' 'file-contract' = 'f56c' 'file-csv' = 'f6dd' 'file-download' = 'f56d' 'file-excel' = 'f1c3' 'file-export' = 'f56e' 'file-image' = 'f1c5' 'file-import' = 'f56f' 'file-invoice' = 'f570' 'file-invoice-dollar' = 'f571' 'file-medical' = 'f477' 'file-medical-alt' = 'f478' 'file-pdf' = 'f1c1' 'file-powerpoint' = 'f1c4' 'file-prescription' = 'f572' 'file-signature' = 'f573' 'file-upload' = 'f574' 'file-video' = 'f1c8' 'file-word' = 'f1c2' 'fill' = 'f575' 'fill-drip' = 'f576' 'film' = 'f008' 'filter' = 'f0b0' 'fingerprint' = 'f577' 'fire' = 'f06d' 'fire-alt' = 'f7e4' 'fire-extinguisher' = 'f134' 'first-aid' = 'f479' 'fish' = 'f578' 'fist-raised' = 'f6de' 'flag' = 'f024' 'flag-checkered' = 'f11e' 'flag-usa' = 'f74d' 'flask' = 'f0c3' 'flushed' = 'f579' 'folder' = 'f07b' 'folder-minus' = 'f65d' 'folder-open' = 'f07c' 'folder-plus' = 'f65e' 'font' = 'f031' 'football-ball' = 'f44e' 'forward' = 'f04e' 'frog' = 'f52e' 'frown' = 'f119' 'frown-open' = 'f57a' 'funnel-dollar' = 'f662' 'futbol' = 'f1e3' 'gamepad' = 'f11b' 'gas-pump' = 'f52f' 'gavel' = 'f0e3' 'gem' = 'f3a5' 'genderless' = 'f22d' 'ghost' = 'f6e2' 'gift' = 'f06b' 'gifts' = 'f79c' 'glass-cheers' = 'f79f' 'glass-martini' = 'f000' 'glass-martini-alt' = 'f57b' 'glass-whiskey' = 'f7a0' 'glasses' = 'f530' 'globe' = 'f0ac' 'globe-africa' = 'f57c' 'globe-americas' = 'f57d' 'globe-asia' = 'f57e' 'globe-europe' = 'f7a2' 'golf-ball' = 'f450' 'gopuram' = 'f664' 'graduation-cap' = 'f19d' 'greater-than' = 'f531' 'greater-than-equal' = 'f532' 'grimace' = 'f57f' 'grin' = 'f580' 'grin-alt' = 'f581' 'grin-beam' = 'f582' 'grin-beam-sweat' = 'f583' 'grin-hearts' = 'f584' 'grin-squint' = 'f585' 'grin-squint-tears' = 'f586' 'grin-stars' = 'f587' 'grin-tears' = 'f588' 'grin-tongue' = 'f589' 'grin-tongue-squint' = 'f58a' 'grin-tongue-wink' = 'f58b' 'grin-wink' = 'f58c' 'grip-horizontal' = 'f58d' 'grip-lines' = 'f7a4' 'grip-lines-vertical' = 'f7a5' 'grip-vertical' = 'f58e' 'guitar' = 'f7a6' 'h-square' = 'f0fd' 'hamburger' = 'f805' 'hammer' = 'f6e3' 'hamsa' = 'f665' 'hand-holding' = 'f4bd' 'hand-holding-heart' = 'f4be' 'hand-holding-medical' = 'e05c' 'hand-holding-usd' = 'f4c0' 'hand-holding-water' = 'f4c1' 'hand-lizard' = 'f258' 'hand-middle-finger' = 'f806' 'hand-paper' = 'f256' 'hand-peace' = 'f25b' 'hand-point-down' = 'f0a7' 'hand-point-left' = 'f0a5' 'hand-point-right' = 'f0a4' 'hand-point-up' = 'f0a6' 'hand-pointer' = 'f25a' 'hand-rock' = 'f255' 'hand-scissors' = 'f257' 'hand-sparkles' = 'e05d' 'hand-spock' = 'f259' 'hands' = 'f4c2' 'hands-helping' = 'f4c4' 'hands-wash' = 'e05e' 'handshake' = 'f2b5' 'handshake-alt-slash' = 'e05f' 'handshake-slash' = 'e060' 'hanukiah' = 'f6e6' 'hard-hat' = 'f807' 'hashtag' = 'f292' 'hat-cowboy' = 'f8c0' 'hat-cowboy-side' = 'f8c1' 'hat-wizard' = 'f6e8' 'hdd' = 'f0a0' 'head-side-cough' = 'e061' 'head-side-cough-slash' = 'e062' 'head-side-mask' = 'e063' 'head-side-virus' = 'e064' 'heading' = 'f1dc' 'headphones' = 'f025' 'headphones-alt' = 'f58f' 'headset' = 'f590' 'heart' = 'f004' 'heart-broken' = 'f7a9' 'heartbeat' = 'f21e' 'helicopter' = 'f533' 'highlighter' = 'f591' 'hiking' = 'f6ec' 'hippo' = 'f6ed' 'history' = 'f1da' 'hockey-puck' = 'f453' 'holly-berry' = 'f7aa' 'home' = 'f015' 'horse' = 'f6f0' 'horse-head' = 'f7ab' 'hospital' = 'f0f8' 'hospital-alt' = 'f47d' 'hospital-symbol' = 'f47e' 'hospital-user' = 'f80d' 'hot-tub' = 'f593' 'hotdog' = 'f80f' 'hotel' = 'f594' 'hourglass' = 'f254' 'hourglass-end' = 'f253' 'hourglass-half' = 'f252' 'hourglass-start' = 'f251' 'house-damage' = 'f6f1' 'house-user' = 'e065' 'hryvnia' = 'f6f2' 'i-cursor' = 'f246' 'ice-cream' = 'f810' 'icicles' = 'f7ad' 'icons' = 'f86d' 'id-badge' = 'f2c1' 'id-card' = 'f2c2' 'id-card-alt' = 'f47f' 'igloo' = 'f7ae' 'image' = 'f03e' 'images' = 'f302' 'inbox' = 'f01c' 'indent' = 'f03c' 'industry' = 'f275' 'infinity' = 'f534' 'info' = 'f129' 'info-circle' = 'f05a' 'italic' = 'f033' 'jedi' = 'f669' 'joint' = 'f595' 'journal-whills' = 'f66a' 'kaaba' = 'f66b' 'key' = 'f084' 'keyboard' = 'f11c' 'khanda' = 'f66d' 'kiss' = 'f596' 'kiss-beam' = 'f597' 'kiss-wink-heart' = 'f598' 'kiwi-bird' = 'f535' 'landmark' = 'f66f' 'language' = 'f1ab' 'laptop' = 'f109' 'laptop-code' = 'f5fc' 'laptop-house' = 'e066' 'laptop-medical' = 'f812' 'laugh' = 'f599' 'laugh-beam' = 'f59a' 'laugh-squint' = 'f59b' 'laugh-wink' = 'f59c' 'layer-group' = 'f5fd' 'leaf' = 'f06c' 'lemon' = 'f094' 'less-than' = 'f536' 'less-than-equal' = 'f537' 'level-down-alt' = 'f3be' 'level-up-alt' = 'f3bf' 'life-ring' = 'f1cd' 'lightbulb' = 'f0eb' 'link' = 'f0c1' 'lira-sign' = 'f195' 'list' = 'f03a' 'list-alt' = 'f022' 'list-ol' = 'f0cb' 'list-ul' = 'f0ca' 'location-arrow' = 'f124' 'lock' = 'f023' 'lock-open' = 'f3c1' 'long-arrow-alt-down' = 'f309' 'long-arrow-alt-left' = 'f30a' 'long-arrow-alt-right' = 'f30b' 'long-arrow-alt-up' = 'f30c' 'low-vision' = 'f2a8' 'luggage-cart' = 'f59d' 'lungs' = 'f604' 'lungs-virus' = 'e067' 'magic' = 'f0d0' 'magnet' = 'f076' 'mail-bulk' = 'f674' 'male' = 'f183' 'map' = 'f279' 'map-marked' = 'f59f' 'map-marked-alt' = 'f5a0' 'map-marker' = 'f041' 'map-marker-alt' = 'f3c5' 'map-pin' = 'f276' 'map-signs' = 'f277' 'marker' = 'f5a1' 'mars' = 'f222' 'mars-double' = 'f227' 'mars-stroke' = 'f229' 'mars-stroke-h' = 'f22b' 'mars-stroke-v' = 'f22a' 'mask' = 'f6fa' 'medal' = 'f5a2' 'medkit' = 'f0fa' 'meh' = 'f11a' 'meh-blank' = 'f5a4' 'meh-rolling-eyes' = 'f5a5' 'memory' = 'f538' 'menorah' = 'f676' 'mercury' = 'f223' 'meteor' = 'f753' 'microchip' = 'f2db' 'microphone' = 'f130' 'microphone-alt' = 'f3c9' 'microphone-alt-slash' = 'f539' 'microphone-slash' = 'f131' 'microscope' = 'f610' 'minus' = 'f068' 'minus-circle' = 'f056' 'minus-square' = 'f146' 'mitten' = 'f7b5' 'mobile' = 'f10b' 'mobile-alt' = 'f3cd' 'money-bill' = 'f0d6' 'money-bill-alt' = 'f3d1' 'money-bill-wave' = 'f53a' 'money-bill-wave-alt' = 'f53b' 'money-check' = 'f53c' 'money-check-alt' = 'f53d' 'monument' = 'f5a6' 'moon' = 'f186' 'mortar-pestle' = 'f5a7' 'mosque' = 'f678' 'motorcycle' = 'f21c' 'mountain' = 'f6fc' 'mouse' = 'f8cc' 'mouse-pointer' = 'f245' 'mug-hot' = 'f7b6' 'music' = 'f001' 'network-wired' = 'f6ff' 'neuter' = 'f22c' 'newspaper' = 'f1ea' 'not-equal' = 'f53e' 'notes-medical' = 'f481' 'object-group' = 'f247' 'object-ungroup' = 'f248' 'oil-can' = 'f613' 'om' = 'f679' 'otter' = 'f700' 'outdent' = 'f03b' 'pager' = 'f815' 'paint-brush' = 'f1fc' 'paint-roller' = 'f5aa' 'palette' = 'f53f' 'pallet' = 'f482' 'paper-plane' = 'f1d8' 'paperclip' = 'f0c6' 'parachute-box' = 'f4cd' 'paragraph' = 'f1dd' 'parking' = 'f540' 'passport' = 'f5ab' 'pastafarianism' = 'f67b' 'paste' = 'f0ea' 'pause' = 'f04c' 'pause-circle' = 'f28b' 'paw' = 'f1b0' 'peace' = 'f67c' 'pen' = 'f304' 'pen-alt' = 'f305' 'pen-fancy' = 'f5ac' 'pen-nib' = 'f5ad' 'pen-square' = 'f14b' 'pencil-alt' = 'f303' 'pencil-ruler' = 'f5ae' 'people-arrows' = 'e068' 'people-carry' = 'f4ce' 'pepper-hot' = 'f816' 'percent' = 'f295' 'percentage' = 'f541' 'person-booth' = 'f756' 'phone' = 'f095' 'phone-alt' = 'f879' 'phone-slash' = 'f3dd' 'phone-square' = 'f098' 'phone-square-alt' = 'f87b' 'phone-volume' = 'f2a0' 'photo-video' = 'f87c' 'piggy-bank' = 'f4d3' 'pills' = 'f484' 'pizza-slice' = 'f818' 'place-of-worship' = 'f67f' 'plane' = 'f072' 'plane-arrival' = 'f5af' 'plane-departure' = 'f5b0' 'plane-slash' = 'e069' 'play' = 'f04b' 'play-circle' = 'f144' 'plug' = 'f1e6' 'plus' = 'f067' 'plus-circle' = 'f055' 'plus-square' = 'f0fe' 'podcast' = 'f2ce' 'poll' = 'f681' 'poll-h' = 'f682' 'poo' = 'f2fe' 'poo-storm' = 'f75a' 'poop' = 'f619' 'portrait' = 'f3e0' 'pound-sign' = 'f154' 'power-off' = 'f011' 'pray' = 'f683' 'praying-hands' = 'f684' 'prescription' = 'f5b1' 'prescription-bottle' = 'f485' 'prescription-bottle-alt' = 'f486' 'print' = 'f02f' 'procedures' = 'f487' 'project-diagram' = 'f542' 'pump-medical' = 'e06a' 'pump-soap' = 'e06b' 'puzzle-piece' = 'f12e' 'qrcode' = 'f029' 'question' = 'f128' 'question-circle' = 'f059' 'quidditch' = 'f458' 'quote-left' = 'f10d' 'quote-right' = 'f10e' 'quran' = 'f687' 'radiation' = 'f7b9' 'radiation-alt' = 'f7ba' 'rainbow' = 'f75b' 'random' = 'f074' 'receipt' = 'f543' 'record-vinyl' = 'f8d9' 'recycle' = 'f1b8' 'redo' = 'f01e' 'redo-alt' = 'f2f9' 'registered' = 'f25d' 'remove-format' = 'f87d' 'reply' = 'f3e5' 'reply-all' = 'f122' 'republican' = 'f75e' 'restroom' = 'f7bd' 'retweet' = 'f079' 'ribbon' = 'f4d6' 'ring' = 'f70b' 'road' = 'f018' 'robot' = 'f544' 'rocket' = 'f135' 'route' = 'f4d7' 'rss' = 'f09e' 'rss-square' = 'f143' 'ruble-sign' = 'f158' 'ruler' = 'f545' 'ruler-combined' = 'f546' 'ruler-horizontal' = 'f547' 'ruler-vertical' = 'f548' 'running' = 'f70c' 'rupee-sign' = 'f156' 'sad-cry' = 'f5b3' 'sad-tear' = 'f5b4' 'satellite' = 'f7bf' 'satellite-dish' = 'f7c0' 'save' = 'f0c7' 'school' = 'f549' 'screwdriver' = 'f54a' 'scroll' = 'f70e' 'sd-card' = 'f7c2' 'search' = 'f002' 'search-dollar' = 'f688' 'search-location' = 'f689' 'search-minus' = 'f010' 'search-plus' = 'f00e' 'seedling' = 'f4d8' 'server' = 'f233' 'shapes' = 'f61f' 'share' = 'f064' 'share-alt' = 'f1e0' 'share-alt-square' = 'f1e1' 'share-square' = 'f14d' 'shekel-sign' = 'f20b' 'shield-alt' = 'f3ed' 'shield-virus' = 'e06c' 'ship' = 'f21a' 'shipping-fast' = 'f48b' 'shoe-prints' = 'f54b' 'shopping-bag' = 'f290' 'shopping-basket' = 'f291' 'shopping-cart' = 'f07a' 'shower' = 'f2cc' 'shuttle-van' = 'f5b6' 'sign' = 'f4d9' 'sign-in-alt' = 'f2f6' 'sign-language' = 'f2a7' 'sign-out-alt' = 'f2f5' 'signal' = 'f012' 'signature' = 'f5b7' 'sim-card' = 'f7c4' 'sink' = 'e06d' 'sitemap' = 'f0e8' 'skating' = 'f7c5' 'skiing' = 'f7c9' 'skiing-nordic' = 'f7ca' 'skull' = 'f54c' 'skull-crossbones' = 'f714' 'slash' = 'f715' 'sleigh' = 'f7cc' 'sliders-h' = 'f1de' 'smile' = 'f118' 'smile-beam' = 'f5b8' 'smile-wink' = 'f4da' 'smog' = 'f75f' 'smoking' = 'f48d' 'smoking-ban' = 'f54d' 'sms' = 'f7cd' 'snowboarding' = 'f7ce' 'snowflake' = 'f2dc' 'snowman' = 'f7d0' 'snowplow' = 'f7d2' 'soap' = 'e06e' 'socks' = 'f696' 'solar-panel' = 'f5ba' 'sort' = 'f0dc' 'sort-alpha-down' = 'f15d' 'sort-alpha-down-alt' = 'f881' 'sort-alpha-up' = 'f15e' 'sort-alpha-up-alt' = 'f882' 'sort-amount-down' = 'f160' 'sort-amount-down-alt' = 'f884' 'sort-amount-up' = 'f161' 'sort-amount-up-alt' = 'f885' 'sort-down' = 'f0dd' 'sort-numeric-down' = 'f162' 'sort-numeric-down-alt' = 'f886' 'sort-numeric-up' = 'f163' 'sort-numeric-up-alt' = 'f887' 'sort-up' = 'f0de' 'spa' = 'f5bb' 'space-shuttle' = 'f197' 'spell-check' = 'f891' 'spider' = 'f717' 'spinner' = 'f110' 'splotch' = 'f5bc' 'spray-can' = 'f5bd' 'square' = 'f0c8' 'square-full' = 'f45c' 'square-root-alt' = 'f698' 'stamp' = 'f5bf' 'star' = 'f005' 'star-and-crescent' = 'f699' 'star-half' = 'f089' 'star-half-alt' = 'f5c0' 'star-of-david' = 'f69a' 'star-of-life' = 'f621' 'step-backward' = 'f048' 'step-forward' = 'f051' 'stethoscope' = 'f0f1' 'sticky-note' = 'f249' 'stop' = 'f04d' 'stop-circle' = 'f28d' 'stopwatch' = 'f2f2' 'stopwatch-20' = 'e06f' 'store' = 'f54e' 'store-alt' = 'f54f' 'store-alt-slash' = 'e070' 'store-slash' = 'e071' 'stream' = 'f550' 'street-view' = 'f21d' 'strikethrough' = 'f0cc' 'stroopwafel' = 'f551' 'subscript' = 'f12c' 'subway' = 'f239' 'suitcase' = 'f0f2' 'suitcase-rolling' = 'f5c1' 'sun' = 'f185' 'superscript' = 'f12b' 'surprise' = 'f5c2' 'swatchbook' = 'f5c3' 'swimmer' = 'f5c4' 'swimming-pool' = 'f5c5' 'synagogue' = 'f69b' 'sync' = 'f021' 'sync-alt' = 'f2f1' 'syringe' = 'f48e' 'table' = 'f0ce' 'table-tennis' = 'f45d' 'tablet' = 'f10a' 'tablet-alt' = 'f3fa' 'tablets' = 'f490' 'tachometer-alt' = 'f3fd' 'tag' = 'f02b' 'tags' = 'f02c' 'tape' = 'f4db' 'tasks' = 'f0ae' 'taxi' = 'f1ba' 'teeth' = 'f62e' 'teeth-open' = 'f62f' 'temperature-high' = 'f769' 'temperature-low' = 'f76b' 'tenge' = 'f7d7' 'terminal' = 'f120' 'text-height' = 'f034' 'text-width' = 'f035' 'th' = 'f00a' 'th-large' = 'f009' 'th-list' = 'f00b' 'theater-masks' = 'f630' 'thermometer' = 'f491' 'thermometer-empty' = 'f2cb' 'thermometer-full' = 'f2c7' 'thermometer-half' = 'f2c9' 'thermometer-quarter' = 'f2ca' 'thermometer-three-quarters' = 'f2c8' 'thumbs-down' = 'f165' 'thumbs-up' = 'f164' 'thumbtack' = 'f08d' 'ticket-alt' = 'f3ff' 'times' = 'f00d' 'times-circle' = 'f057' 'tint' = 'f043' 'tint-slash' = 'f5c7' 'tired' = 'f5c8' 'toggle-off' = 'f204' 'toggle-on' = 'f205' 'toilet' = 'f7d8' 'toilet-paper' = 'f71e' 'toilet-paper-slash' = 'e072' 'toolbox' = 'f552' 'tools' = 'f7d9' 'tooth' = 'f5c9' 'torah' = 'f6a0' 'torii-gate' = 'f6a1' 'tractor' = 'f722' 'trademark' = 'f25c' 'traffic-light' = 'f637' 'trailer' = 'e041' 'train' = 'f238' 'tram' = 'f7da' 'transgender' = 'f224' 'transgender-alt' = 'f225' 'trash' = 'f1f8' 'trash-alt' = 'f2ed' 'trash-restore' = 'f829' 'trash-restore-alt' = 'f82a' 'tree' = 'f1bb' 'trophy' = 'f091' 'truck' = 'f0d1' 'truck-loading' = 'f4de' 'truck-monster' = 'f63b' 'truck-moving' = 'f4df' 'truck-pickup' = 'f63c' 'tshirt' = 'f553' 'tty' = 'f1e4' 'tv' = 'f26c' 'umbrella' = 'f0e9' 'umbrella-beach' = 'f5ca' 'underline' = 'f0cd' 'undo' = 'f0e2' 'undo-alt' = 'f2ea' 'universal-access' = 'f29a' 'university' = 'f19c' 'unlink' = 'f127' 'unlock' = 'f09c' 'unlock-alt' = 'f13e' 'upload' = 'f093' 'user' = 'f007' 'user-alt' = 'f406' 'user-alt-slash' = 'f4fa' 'user-astronaut' = 'f4fb' 'user-check' = 'f4fc' 'user-circle' = 'f2bd' 'user-clock' = 'f4fd' 'user-cog' = 'f4fe' 'user-edit' = 'f4ff' 'user-friends' = 'f500' 'user-graduate' = 'f501' 'user-injured' = 'f728' 'user-lock' = 'f502' 'user-md' = 'f0f0' 'user-minus' = 'f503' 'user-ninja' = 'f504' 'user-nurse' = 'f82f' 'user-plus' = 'f234' 'user-secret' = 'f21b' 'user-shield' = 'f505' 'user-slash' = 'f506' 'user-tag' = 'f507' 'user-tie' = 'f508' 'user-times' = 'f235' 'users' = 'f0c0' 'users-cog' = 'f509' 'users-slash' = 'e073' 'utensil-spoon' = 'f2e5' 'utensils' = 'f2e7' 'vector-square' = 'f5cb' 'venus' = 'f221' 'venus-double' = 'f226' 'venus-mars' = 'f228' 'vest' = 'e085' 'vest-patches' = 'e086' 'vial' = 'f492' 'vials' = 'f493' 'video' = 'f03d' 'video-slash' = 'f4e2' 'vihara' = 'f6a7' 'virus' = 'e074' 'virus-slash' = 'e075' 'viruses' = 'e076' 'voicemail' = 'f897' 'volleyball-ball' = 'f45f' 'volume-down' = 'f027' 'volume-mute' = 'f6a9' 'volume-off' = 'f026' 'volume-up' = 'f028' 'vote-yea' = 'f772' 'vr-cardboard' = 'f729' 'walking' = 'f554' 'wallet' = 'f555' 'warehouse' = 'f494' 'water' = 'f773' 'wave-square' = 'f83e' 'weight' = 'f496' 'weight-hanging' = 'f5cd' 'wheelchair' = 'f193' 'wifi' = 'f1eb' 'wind' = 'f72e' 'window-close' = 'f410' 'window-maximize' = 'f2d0' 'window-minimize' = 'f2d1' 'window-restore' = 'f2d2' 'wine-bottle' = 'f72f' 'wine-glass' = 'f4e3' 'wine-glass-alt' = 'f5ce' 'won-sign' = 'f159' 'wrench' = 'f0ad' 'x-ray' = 'f497' 'yen-sign' = 'f157' 'yin-yang' = 'f6ad' } FontsMaterialIcon = @( '3d-rotation' '500px' '8tracks' 'account' 'account-add' 'account-box' 'account-box-mail' 'account-box-o' 'account-box-phone' 'account-calendar' 'account-circle' 'account-o' 'accounts' 'accounts-add' 'accounts-alt' 'accounts-list' 'accounts-list-alt' 'accounts-outline' 'airline-seat-flat' 'airline-seat-flat-angled' 'airline-seat-individual-suite' 'airline-seat-legroom-extra' 'airline-seat-legroom-normal' 'airline-seat-legroom-reduced' 'airline-seat-recline-extra' 'airline-seat-recline-normal' 'airplane' 'airplane-off' 'airplay' 'alarm' 'alarm-check' 'alarm-off' 'alarm-plus' 'alarm-snooze' 'album' 'alert-circle' 'alert-circle-o' 'alert-octagon' 'alert-polygon' 'alert-triangle' 'amazon' 'android' 'android-alt' 'apple' 'apps' 'archive' 'arrow-left' 'arrow-left-bottom' 'arrow-merge' 'arrow-missed' 'arrow-right' 'arrow-right-top' 'arrows' 'arrow-split' 'aspect-ratio' 'aspect-ratio-alt' 'assignment' 'assignment-account' 'assignment-alert' 'assignment-check' 'assignment-o' 'assignment-return' 'assignment-returned' 'attachment' 'attachment-alt' 'audio' 'badge-check' 'balance' 'balance-wallet' 'battery' 'battery-alert' 'battery-flash' 'battery-unknown' 'behance' 'bike' 'block' 'block-alt' 'blogger' 'bluetooth' 'bluetooth-connected' 'bluetooth-off' 'bluetooth-search' 'bluetooth-setting' 'blur' 'blur-circular' 'blur-linear' 'blur-off' 'boat' 'book' 'book-image' 'bookmark' 'bookmark-outline' 'border-all' 'border-bottom' 'border-clear' 'border-color' 'border-horizontal' 'border-inner' 'border-left' 'border-outer' 'border-right' 'border-style' 'border-top' 'border-vertical' 'brightness-2' 'brightness-3' 'brightness-4' 'brightness-5' 'brightness-6' 'brightness-7' 'brightness-auto' 'brightness-setting' 'broken-image' 'brush' 'bug' 'bus' 'cake' 'calendar' 'calendar-alt' 'calendar-check' 'calendar-close' 'calendar-note' 'camera' 'camera-add' 'camera-alt' 'camera-bw' 'camera-front' 'camera-mic' 'camera-party-mode' 'camera-rear' 'camera-roll' 'camera-switch' 'car' 'card' 'card-alert' 'card-giftcard' 'card-membership' 'card-off' 'card-sd' 'card-sim' 'card-travel' 'caret-down' 'caret-down-circle' 'caret-left' 'caret-left-circle' 'caret-right' 'caret-right-circle' 'caret-up' 'caret-up-circle' 'car-taxi' 'car-wash' 'case' 'case-check' 'case-download' 'case-play' 'cast' 'cast-connected' 'center-focus-strong' 'center-focus-weak' 'chart' 'chart-donut' 'check' 'check-all' 'check-circle' 'check-circle-u' 'check-square' 'chevron-down' 'chevron-left' 'chevron-right' 'chevron-up' 'circle' 'circle-o' 'city' 'city-alt' 'close' 'close-circle' 'close-circle-o' 'closed-caption' 'cloud' 'cloud-box' 'cloud-circle' 'cloud-done' 'cloud-download' 'cloud-off' 'cloud-outline' 'cloud-outline-alt' 'cloud-upload' 'cocktail' 'code' 'codepen' 'code-setting' 'code-smartphone' 'coffee' 'collection-bookmark' 'collection-case-play' 'collection-folder-image' 'collection-image' 'collection-image-o' 'collection-item' 'collection-item-1' 'collection-item-2' 'collection-item-3' 'collection-item-4' 'collection-item-5' 'collection-item-6' 'collection-item-7' 'collection-item-8' 'collection-item-9' 'collection-item-9-plus' 'collection-music' 'collection-pdf' 'collection-plus' 'collection-speaker' 'collection-text' 'collection-video' 'comment' 'comment-alert' 'comment-alt' 'comment-alt-text' 'comment-edit' 'comment-image' 'comment-list' 'comment-more' 'comment-outline' 'comments' 'comment-text' 'comment-text-alt' 'comment-video' 'compare' 'compass' 'confirmation-number' 'copy' 'crop' 'crop-16-9' 'crop-3-2' 'crop-5-4' 'crop-7-5' 'crop-din' 'crop-free' 'crop-landscape' 'crop-portrait' 'crop-square' 'cutlery' 'delete' 'delicious' 'desktop-mac' 'desktop-windows' 'developer-board' 'device-hub' 'devices' 'devices-off' 'dialpad' 'disc-full' 'disqus' 'dns' 'dock' 'dot-circle' 'dot-circle-alt' 'download' 'dribbble' 'drink' 'dropbox' 'edit' 'eject' 'eject-alt' 'email' 'email-open' 'equalizer' 'evernote' 'explicit' 'exposure' 'exposure-alt' 'eye' 'eyedropper' 'eye-off' 'face' 'facebook' 'facebook-box' 'fast-forward' 'fast-rewind' 'favorite' 'favorite-outline' 'female' 'file' 'file-plus' 'file-text' 'filter-b-and-w' 'filter-center-focus' 'filter-frames' 'filter-list' 'filter-tilt-shift' 'fire' 'flag' 'flare' 'flash' 'flash-auto' 'flash-off' 'flattr' 'flickr' 'flight-land' 'flight-takeoff' 'flip' 'flip-to-back' 'flip-to-front' 'floppy' 'flower' 'flower-alt' 'folder' 'folder-outline' 'folder-person' 'folder-star' 'folder-star-alt' 'font' 'format-align-center' 'format-align-justify' 'format-align-left' 'format-align-right' 'format-bold' 'format-clear' 'format-clear-all' 'format-color-fill' 'format-color-reset' 'format-color-text' 'format-indent-decrease' 'format-indent-increase' 'format-italic' 'format-line-spacing' 'format-list-bulleted' 'format-list-numbered' 'format-ltr' 'format-rtl' 'format-size' 'format-strikethrough' 'format-strikethrough-s' 'format-subject' 'format-underlined' 'format-valign-bottom' 'format-valign-center' 'format-valign-top' 'forward' 'forward-10' 'forward-30' 'forward-5' 'fullscreen' 'fullscreen-alt' 'fullscreen-exit' 'functions' 'gamepad' 'gas-station' 'gesture' 'gif' 'github' 'github-alt' 'github-box' 'globe' 'globe-alt' 'globe-lock' 'google' 'google-drive' 'google-earth' 'google-glass' 'google-maps' 'google-old' 'google-pages' 'google-play' 'google-plus' 'google-plus-box' 'gps' 'gps-dot' 'gps-off' 'gradient' 'graduation-cap' 'grain' 'graphic-eq' 'grid' 'grid-off' 'group' 'group-work' 'hd' 'hdr' 'hdr-off' 'hdr-strong' 'hdr-weak' 'headset' 'headset-mic' 'hearing' 'help' 'help-outline' 'home' 'hospital' 'hospital-alt' 'hotel' 'hourglass' 'hourglass-alt' 'hourglass-outline' 'hq' 'http' 'image' 'image-alt' 'image-o' 'inbox' 'info' 'info-outline' 'input-antenna' 'input-composite' 'input-hdmi' 'input-power' 'input-svideo' 'instagram' 'invert-colors' 'invert-colors-off' 'iridescent' 'key' 'keyboard' 'keyboard-hide' 'label' 'label-alt' 'label-alt-outline' 'label-heart' 'labels' 'lamp' 'landscape' 'language-css3' 'language-html5' 'language-javascript' 'language-python' 'language-python-alt' 'laptop' 'laptop-chromebook' 'laptop-mac' 'lastfm' 'layers' 'layers-off' 'leak' 'leak-off' 'library' 'link' 'linkedin' 'linkedin-box' 'lock' 'lock-open' 'lock-outline' 'long-arrow-down' 'long-arrow-left' 'long-arrow-return' 'long-arrow-right' 'long-arrow-tab' 'long-arrow-up' 'looks' 'loupe' 'mail-reply' 'mail-reply-all' 'mail-send' 'male' 'male-alt' 'male-female' 'mall' 'map' 'markunread-mailbox' 'memory' 'menu' 'mic' 'mic-off' 'mic-outline' 'mic-setting' 'minus' 'minus-circle' 'minus-circle-outline' 'minus-square' 'money' 'money-box' 'money-off' 'mood' 'mood-bad' 'more' 'more-vert' 'mouse' 'movie' 'movie-alt' 'n-1-square' 'n-2-square' 'n-3-square' 'n-4-square' 'n-5-square' 'n-6-square' 'nature' 'nature-people' 'navigation' 'neg-1' 'neg-2' 'network' 'network-alert' 'network-locked' 'network-off' 'network-outline' 'network-setting' 'nfc' 'notifications' 'notifications-active' 'notifications-add' 'notifications-none' 'notifications-off' 'notifications-paused' 'odnoklassniki' 'open-in-browser' 'open-in-new' 'outlook' 'palette' 'panorama-horizontal' 'panorama-vertical' 'panorama-wide-angle' 'parking' 'pause' 'pause-circle' 'pause-circle-outline' 'paypal' 'paypal-alt' 'phone' 'phone-bluetooth' 'phone-end' 'phone-forwarded' 'phone-in-talk' 'phone-locked' 'phone-missed' 'phone-msg' 'phone-paused' 'phone-ring' 'phone-setting' 'phone-sip' 'photo-size-select-large' 'photo-size-select-small' 'picture-in-picture' 'pin' 'pin-account' 'pin-assistant' 'pin-drop' 'pin-help' 'pin-off' 'pinterest' 'pinterest-box' 'pizza' 'plaster' 'play' 'play-circle' 'play-circle-outline' 'play-for-work' 'playlist-audio' 'playlist-plus' 'playstation' 'plus' 'plus-1' 'plus-2' 'plus-circle' 'plus-circle-o' 'plus-circle-o-duplicate' 'plus-square' 'pocket' 'polymer' 'portable-wifi' 'portable-wifi-changes' 'portable-wifi-off' 'power' 'power-input' 'power-setting' 'present-to-all' 'print' 'puzzle-piece' 'quote' 'radio' 'railway' 'reader' 'receipt' 'reddit' 'redo' 'refresh' 'refresh-alt' 'refresh-sync' 'refresh-sync-alert' 'refresh-sync-off' 'remote-control' 'remote-control-alt' 'repeat' 'repeat-one' 'replay' 'replay-10' 'replay-30' 'replay-5' 'roller' 'rotate-ccw' 'rotate-cw' 'rotate-left' 'rotate-right' 'router' 'rss' 'ruler' 'run' 'satellite' 'scanner' 'scissors' 'screen-rotation' 'screen-rotation-lock' 'search' 'search-for' 'search-in-file' 'search-in-page' 'search-replace' 'seat' 'sec-10' 'sec-3' 'select-all' 'settings' 'settings-square' 'shape' 'share' 'shield-check' 'shield-security' 'shopping-basket' 'shopping-cart' 'shopping-cart-plus' 'shuffle' 'sign-in' 'skip-next' 'skip-previous' 'skype' 'slideshare' 'slideshow' 'smartphone' 'smartphone-android' 'smartphone-download' 'smartphone-erase' 'smartphone-info' 'smartphone-iphone' 'smartphone-landscape' 'smartphone-landscape-lock' 'smartphone-lock' 'smartphone-portrait-lock' 'smartphone-ring' 'smartphone-setting' 'smartphone-setup' 'sort-amount-asc' 'sort-amount-desc' 'sort-asc' 'sort-desc' 'soundcloud' 'space-bar' 'speaker' 'spellcheck' 'spinner' 'square-down' 'square-o' 'square-right' 'stackoverflow' 'star' 'star-circle' 'star-half' 'star-outline' 'steam' 'steam-square' 'stop' 'storage' 'store' 'store-24' 'subway' 'sun' 'surround-sound' 'swap' 'swap-alt' 'swap-vertical' 'swap-vertical-circle' 'tab' 'tablet' 'tablet-android' 'tablet-mac' 'tab-unselected' 'tag' 'tag-close' 'tag-more' 'tap-and-play' 'text-format' 'texture' 'thumb-down' 'thumb-up' 'thumb-up-down' 'ticket-star' 'time' 'time-countdown' 'time-interval' 'timer' 'time-restore' 'time-restore-setting' 'timer-off' 'toll' 'tonality' 'toys' 'traffic' 'transform' 'translate' 'trending-down' 'trending-flat' 'trending-up' 'triangle-down' 'triangle-up' 'truck' 'tumblr' 'tune' 'turning-sign' 'tv' 'tv-alt-play' 'tv-list' 'tv-play' 'twitch' 'twitter' 'twitter-box' 'undo' 'unfold-less' 'unfold-more' 'ungroup' 'upload' 'usb' 'vibration' 'videocam' 'videocam-off' 'videocam-switch' 'view-agenda' 'view-array' 'view-carousel' 'view-column' 'view-comfy' 'view-compact' 'view-dashboard' 'view-day' 'view-headline' 'view-list' 'view-list-alt' 'view-module' 'view-quilt' 'view-stream' 'view-subtitles' 'view-toc' 'view-web' 'view-week' 'vignette' 'vimeo' 'vk' 'voicemail' 'volume-down' 'volume-mute' 'volume-off' 'volume-up' 'walk' 'wallpaper' 'washing-machine' 'watch' 'wb-auto' 'whatsapp' 'widgets' 'wifi' 'wifi-alt' 'wifi-alt-2' 'wifi-info' 'wifi-lock' 'wifi-off' 'wifi-outline' 'wikipedia' 'window-maximize' 'window-minimize' 'window-restore' 'windows' 'wrap-text' 'wrench' 'xbox' 'yahoo' 'youtube' 'youtube-play' 'zero' 'zoom-in' 'zoom-out' ) FontsSimple = @( '1001tracklists' '1password' '3m' '42' '4d' '500px' 'a-frame' 'abbrobotstudio' 'abbvie' 'abletonlive' 'about-dot-me' 'abstract' 'academia' 'accenture' 'acclaim' 'accusoft' 'acer' 'acm' 'actigraph' 'activision' 'adafruit' 'adblock' 'adblockplus' 'addthis' 'adguard' 'adobe' 'adobeacrobatreader' 'adobeaftereffects' 'adobeaudition' 'adobecreativecloud' 'adobedreamweaver' 'adobefonts' 'adobeillustrator' 'adobeindesign' 'adobelightroom' 'adobelightroomclassic' 'adobephonegap' 'adobephotoshop' 'adobepremierepro' 'adobexd' 'adonisjs' 'aerlingus' 'aeroflot' 'aeromexico' 'aerospike' 'affinity' 'affinitydesigner' 'affinityphoto' 'affinitypublisher' 'aidungeon' 'aiohttp' 'aiqfome' 'airasia' 'airbnb' 'airbus' 'aircall' 'aircanada' 'airchina' 'airfrance' 'airplayaudio' 'airplayvideo' 'airtable' 'alacritty' 'alfaromeo' 'algolia' 'alibaba-dot-com' 'alibabacloud' 'aliexpress' 'alipay' 'alitalia' 'alliedmodders' 'allocine' 'alltrails' 'alpinedotjs' 'alpinelinux' 'altiumdesigner' 'amazon' 'amazonalexa' 'amazonaws' 'amazondynamodb' 'amazonfiretv' 'amazonlumberyard' 'amazonpay' 'amazonprime' 'amazons3' 'amd' 'americanairlines' 'americanexpress' 'amp' 'amul' 'ana' 'anaconda' 'analogue' 'anchor' 'andela' 'android' 'androidauto' 'androidstudio' 'angellist' 'angular' 'angularjs' 'angularuniversal' 'anilist' 'ansible' 'ansys' 'antdesign' 'antena3' 'anydesk' 'aol' 'apache' 'apacheairflow' 'apacheant' 'apachecassandra' 'apachecloudstack' 'apachecordova' 'apachedruid' 'apacheecharts' 'apacheflink' 'apachegroovy' 'apachehive' 'apachejmeter' 'apachekafka' 'apachekylin' 'apachemaven' 'apachenetbeanside' 'apacheopenoffice' 'apachepulsar' 'apacherocketmq' 'apachesolr' 'apachespark' 'apachetomcat' 'aparat' 'apollographql' 'apostrophe' 'apple' 'applearcade' 'applemusic' 'applepay' 'applepodcasts' 'appletv' 'appsignal' 'appstore' 'appveyor' 'aral' 'arangodb' 'archicad' 'archiveofourown' 'archlinux' 'ardour' 'arduino' 'arkecosystem' 'arlo' 'artixlinux' 'artstation' 'arxiv' 'asana' 'asciidoctor' 'asciinema' 'aseprite' 'askfm' 'askubuntu' 'assemblyscript' 'asus' 'at-and-t' 'atari' 'atlassian' 'atom' 'audacity' 'audi' 'audible' 'audio-technica' 'audioboom' 'audiomack' 'aurelia' 'auth0' 'authy' 'autodesk' 'autohotkey' 'automatic' 'automattic' 'autotask' 'aventrix' 'awesomelists' 'awesomewm' 'awsamplify' 'azureartifacts' 'azuredataexplorer' 'azuredevops' 'azurefunctions' 'azurepipelines' 'b-and-rautomation' 'babel' 'badgr' 'badoo' 'baidu' 'bamboo' 'bancontact' 'bandcamp' 'bandlab' 'bandsintown' 'bankofamerica' 'barclays' 'baremetrics' 'basecamp' 'bata' 'bathasu' 'battle-dot-net' 'bbc' 'bbciplayer' 'beatport' 'beats' 'beatsbydre' 'behance' 'beijingsubway' 'bentley' 'betfair' 'bigbasket' 'bigcartel' 'bigcommerce' 'bilibili' 'bing' 'bit' 'bitbucket' 'bitcoin' 'bitcoincash' 'bitcoinsv' 'bitdefender' 'bitly' 'bitrise' 'bitwarden' 'bitwig' 'blackberry' 'blazemeter' 'blazor' 'blender' 'blockchain-dot-com' 'blogger' 'bloglovin' 'blueprint' 'bluetooth' 'bmcsoftware' 'bmw' 'boeing' 'bookbub' 'bookmeter' 'bookstack' 'boost' 'bootstrap' 'bosch' 'bose' 'bower' 'box' 'brand-dot-ai' 'brandfolder' 'brave' 'breaker' 'britishairways' 'broadcom' 'bt' 'buddy' 'buefy' 'buffer' 'bugatti' 'bugcrowd' 'bugsnag' 'buildkite' 'bulma' 'bunq' 'buymeacoffee' 'buzzfeed' 'byte' 'c' 'cachet' 'cairometro' 'cakephp' 'campaignmonitor' 'canonical' 'canva' 'capacitor' 'carthrottle' 'carto' 'cashapp' 'castbox' 'castorama' 'castro' 'caterpillar' 'cbs' 'cdprojekt' 'celery' 'centos' 'ceph' 'cesium' 'cevo' 'chai' 'chainlink' 'chakraui' 'chart-dot-js' 'chartmogul' 'chase' 'chatbot' 'checkio' 'checkmarx' 'chef' 'chevrolet' 'chinaeasternairlines' 'chinasouthernairlines' 'chocolatey' 'chrysler' 'chupachups' 'cinema4d' 'circle' 'circleci' 'cirrusci' 'cisco' 'citrix' 'citroen' 'civicrm' 'ckeditor4' 'claris' 'clickup' 'clion' 'cliqz' 'clockify' 'clojure' 'cloud66' 'cloudbees' 'cloudcannon' 'cloudera' 'cloudflare' 'cloudsmith' 'cloudways' 'clubhouse' 'clyp' 'cmake' 'cnn' 'co-op' 'cockroachlabs' 'cocoapods' 'cocos' 'coda' 'codacy' 'codeberg' 'codecademy' 'codeceptjs' 'codechef' 'codeclimate' 'codecov' 'codefactor' 'codeforces' 'codeigniter' 'codemagic' 'codemirror' 'codenewbie' 'codepen' 'codeproject' 'codersrank' 'coderwall' 'codesandbox' 'codeship' 'codewars' 'codingame' 'codingninjas' 'codio' 'coffeescript' 'cognizant' 'coinbase' 'commerzbank' 'commonworkflowlanguage' 'composer' 'compropago' 'concourse' 'conda-forge' 'conekta' 'confluence' 'consul' 'contactlesspayment' 'contentful' 'convertio' 'cookiecutter' 'coronaengine' 'coronarenderer' 'corsair' 'couchbase' 'counter-strike' 'countingworkspro' 'coursera' 'coveralls' 'cpanel' 'cplusplus' 'craftcms' 'creativecommons' 'crehana' 'crowdin' 'crowdsource' 'crunchbase' 'crunchyroll' 'cryengine' 'crystal' 'csharp' 'css3' 'csswizardry' 'cucumber' 'curl' 'curseforge' 'cycling74' 'cypress' 'd-wavesystems' 'd3-dot-js' 'dacia' 'daf' 'dailymotion' 'daimler' 'darkreader' 'dart' 'daserste' 'dash' 'dashlane' 'dassaultsystemes' 'databricks' 'datacamp' 'datadog' 'datastax' 'dataversioncontrol' 'datocms' 'datto' 'dazn' 'dblp' 'dcentertainment' 'debian' 'deepin' 'deepnote' 'deezer' 'delicious' 'deliveroo' 'dell' 'delonghi' 'delphi' 'delta' 'deno' 'dependabot' 'derspiegel' 'designernews' 'deutschebahn' 'deutschebank' 'dev-dot-to' 'deviantart' 'devpost' 'devrant' 'dgraph' 'dhl' 'diagrams-dot-net' 'dialogflow' 'diaspora' 'digg' 'digi-keyelectronics' 'digitalocean' 'dior' 'directus' 'discogs' 'discord' 'discourse' 'discover' 'disqus' 'disroot' 'django' 'dlna' 'docker' 'docusign' 'dogecoin' 'dolby' 'doordash' 'dot-net' 'douban' 'draugiem-dot-lv' 'dribbble' 'drone' 'drooble' 'dropbox' 'drupal' 'dsautomobiles' 'dtube' 'duckduckgo' 'dunked' 'duolingo' 'dwm' 'dynamics365' 'dynatrace' 'ea' 'eagle' 'easyjet' 'ebay' 'eclipseche' 'eclipseide' 'eclipsejetty' 'eclipsemosquitto' 'eclipsevert-dot-x' 'editorconfig' 'edx' 'egghead' 'egnyte' 'eightsleep' 'elastic' 'elasticcloud' 'elasticsearch' 'elasticstack' 'electron' 'element' 'elementary' 'eleventy' 'elixir' 'eljueves' 'ello' 'elm' 'elsevier' 'embarcadero' 'ember-dot-js' 'emby' 'emirates' 'emlakjet' 'empirekred' 'enpass' 'envato' 'epel' 'epicgames' 'epson' 'erlang' 'esea' 'eslgaming' 'eslint' 'esphome' 'espressif' 'ethereum' 'ethiopianairlines' 'etihadairways' 'etsy' 'eventbrite' 'eventstore' 'evernote' 'everplaces' 'evry' 'exercism' 'expensify' 'expertsexchange' 'expo' 'express' 'eyeem' 'f-droid' 'f-secure' 'facebook' 'facebookgaming' 'facebooklive' 'faceit' 'facepunch' 'falcon' 'fampay' 'fandango' 'fandom' 'farfetch' 'fastapi' 'fastify' 'fastlane' 'fastly' 'fathom' 'favro' 'feathub' 'fedex' 'fedora' 'fedramp' 'feedly' 'ferrari' 'ferrarin-dot-v-dot' 'ffmpeg' 'fiat' 'fidoalliance' 'fifa' 'figma' 'figshare' 'fila' 'files' 'filezilla' 'fing' 'firebase' 'firefox' 'firefoxbrowser' 'first' 'fitbit' 'fite' 'fiverr' 'flask' 'flathub' 'flattr' 'flickr' 'flipboard' 'flipkart' 'floatplane' 'flood' 'fluentd' 'flutter' 'fmod' 'fnac' 'folium' 'fontawesome' 'fontbase' 'foodpanda' 'ford' 'forestry' 'formstack' 'fortinet' 'fortran' 'fossa' 'fossilscm' 'foursquare' 'foxtel' 'fozzy' 'framer' 'fraunhofer-gesellschaft' 'freebsd' 'freecodecamp' 'freedesktop-dot-org' 'freelancer' 'freenas' 'fujifilm' 'fujitsu' 'furaffinity' 'furrynetwork' 'futurelearn' 'g2a' 'gamejolt' 'garmin' 'gatling' 'gatsby' 'gauges' 'geeksforgeeks' 'generalelectric' 'generalmotors' 'genius' 'gentoo' 'geocaching' 'gerrit' 'ghost' 'ghostery' 'gimp' 'giphy' 'git' 'gitbook' 'gitea' 'gitee' 'gitextensions' 'github' 'githubactions' 'githubsponsors' 'gitkraken' 'gitlab' 'gitlfs' 'gitpod' 'gitter' 'glassdoor' 'glitch' 'gmail' 'gnome' 'gnu' 'gnubash' 'gnuemacs' 'gnuicecat' 'gnuprivacyguard' 'gnusocial' 'go' 'godotengine' 'gofundme' 'gog-dot-com' 'goldenline' 'goodreads' 'google' 'googleads' 'googleadsense' 'googleanalytics' 'googleassistant' 'googlecalendar' 'googlecardboard' 'googlecast' 'googlechat' 'googlechrome' 'googleclassroom' 'googlecloud' 'googlecolab' 'googledomains' 'googledrive' 'googleearth' 'googlefit' 'googlefonts' 'googlehangouts' 'googlekeep' 'googlelens' 'googlemaps' 'googlemeet' 'googlemessages' 'googlemybusiness' 'googlenearby' 'googlenews' 'googleoptimize' 'googlepay' 'googlephotos' 'googleplay' 'googlepodcasts' 'googlescholar' 'googlesearchconsole' 'googlesheets' 'googlestreetview' 'googletagmanager' 'googletranslate' 'gotomeeting' 'gov-dot-uk' 'gradle' 'grafana' 'grammarly' 'graphcool' 'graphql' 'grav' 'gravatar' 'graylog' 'greensock' 'griddotai' 'gridsome' 'groupon' 'grubhub' 'grunt' 'guangzhoumetro' 'gulp' 'gumroad' 'gumtree' 'gutenberg' 'habr' 'hackaday' 'hackclub' 'hackerearth' 'hackerone' 'hackerrank' 'hackhands' 'hackster' 'hackthebox' 'handshake' 'handshake_protocol' 'happycow' 'harbor' 'hashnode' 'haskell' 'hasura' 'hatenabookmark' 'haveibeenpwned' 'haxe' 'hbo' 'hcl' 'headspace' 'hellofresh' 'hellyhansen' 'helm' 'helpdesk' 'here' 'heroku' 'hexo' 'hey' 'hibernate' 'highly' 'hilton' 'hipchat' 'hitachi' 'hive' 'hive_blockchain' 'hockeyapp' 'homeadvisor' 'homeassistant' 'homeassistantcommunitystore' 'homebrew' 'homebridge' 'homify' 'honda' 'hootsuite' 'hoppscotch' 'hotels-dot-com' 'hotjar' 'houdini' 'houzz' 'hp' 'html5' 'htmlacademy' 'huawei' 'hubspot' 'hugo' 'hulu' 'humblebundle' 'hungryjacks' 'hurriyetemlak' 'husqvarna' 'hyper' 'hyperledger' 'hypothesis' 'hyundai' 'iata' 'ibeacon' 'ibm' 'ibmwatson' 'icinga' 'icloud' 'icomoon' 'icon' 'iconfinder' 'iconify' 'iconjar' 'icq' 'ideal' 'ieee' 'ifixit' 'ifood' 'ifttt' 'iheartradio' 'ikea' 'imagej' 'imdb' 'imgur' 'immer' 'imou' 'indeed' 'infiniti' 'influxdb' 'informatica' 'infosys' 'ingress' 'inkscape' 'insomnia' 'instacart' 'instagram' 'instapaper' 'instructables' 'integromat' 'intel' 'intellijidea' 'intercom' 'internetarchive' 'internetexplorer' 'intigriti' 'invision' 'invoiceninja' 'iobroker' 'ionic' 'ios' 'iota' 'ipfs' 'issuu' 'itch-dot-io' 'itunes' 'iveco' 'jabber' 'jaguar' 'jamboard' 'jameson' 'jamstack' 'jasmine' 'java' 'javascript' 'jbl' 'jcb' 'jeep' 'jekyll' 'jellyfin' 'jenkins' 'jenkinsx' 'jest' 'jet' 'jetbrains' 'jfrog' 'jfrogbintray' 'jinja' 'jira' 'jirasoftware' 'jitsi' 'johndeere' 'joomla' 'jpeg' 'jquery' 'jrgroup' 'jsdelivr' 'jsfiddle' 'json' 'jsonwebtokens' 'jss' 'julia' 'junipernetworks' 'junit5' 'jupyter' 'justeat' 'justgiving' 'kaggle' 'kahoot' 'kaios' 'kakao' 'kakaotalk' 'kalilinux' 'karlsruherverkehrsverbund' 'kasasmart' 'kashflow' 'kaspersky' 'katacoda' 'katana' 'kde' 'kdenlive' 'keepassxc' 'kentico' 'keras' 'keybase' 'keycdn' 'khanacademy' 'khronosgroup' 'kia' 'kibana' 'kickstarter' 'kik' 'kirby' 'kitsu' 'klarna' 'klm' 'klook' 'klout' 'knowledgebase' 'known' 'ko-fi' 'kodi' 'koding' 'kofax' 'komoot' 'kongregate' 'kotlin' 'krita' 'ktm' 'kubernetes' 'kubuntu' 'kyocera' 'labview' 'lada' 'lamborghini' 'landrover' 'laragon' 'laravel' 'laravelhorizon' 'laravelnova' 'last-dot-fm' 'lastpass' 'latex' 'launchpad' 'lbry' 'leaflet' 'leanpub' 'leetcode' 'lenovo' 'less' 'letsencrypt' 'letterboxd' 'lg' 'lgtm' 'liberapay' 'libraries-dot-io' 'librarything' 'libreoffice' 'libuv' 'lichess' 'lifx' 'lighthouse' 'line' 'lineageos' 'linewebtoon' 'linkedin' 'linktree' 'linode' 'linux' 'linuxcontainers' 'linuxfoundation' 'linuxmint' 'lionair' 'lit' 'litecoin' 'livechat' 'livejournal' 'livestream' 'llvm' 'lmms' 'logitech' 'logmein' 'logstash' 'looker' 'loom' 'loop' 'lospec' 'lotpolishairlines' 'lua' 'lubuntu' 'lufthansa' 'lumen' 'lydia' 'lyft' 'maas' 'macos' 'macys' 'magento' 'magisk' 'mail-dot-ru' 'mailchimp' 'majorleaguehacking' 'makerbot' 'man' 'manageiq' 'manjaro' 'mapbox' 'mariadb' 'mariadbfoundation' 'markdown' 'marketo' 'marriott' 'maserati' 'mastercard' 'mastercomfig' 'mastodon' 'material-ui' 'materialdesign' 'materialdesignicons' 'mathworks' 'matomo' 'matrix' 'mattermost' 'matternet' 'max-planck-gesellschaft' 'max' 'maytag' 'mazda' 'mcafee' 'mcdonalds' 'mclaren' 'mdnwebdocs' 'mediafire' 'mediatek' 'mediatemple' 'medium' 'meetup' 'mega' 'mendeley' 'mercedes' 'mercurial' 'messenger' 'metabase' 'metafilter' 'meteor' 'metro' 'metrodelaciudaddemexico' 'metrodemadrid' 'metrodeparis' 'mewe' 'micro-dot-blog' 'microbit' 'microgenetics' 'micropython' 'microsoft' 'microsoftacademic' 'microsoftaccess' 'microsoftazure' 'microsoftedge' 'microsoftexcel' 'microsoftexchange' 'microsoftoffice' 'microsoftonedrive' 'microsoftonenote' 'microsoftoutlook' 'microsoftpowerpoint' 'microsoftsharepoint' 'microsoftsqlserver' 'microsoftteams' 'microsoftvisio' 'microsoftword' 'microstrategy' 'midi' 'minds' 'minecraft' 'minetest' 'mini' 'minutemailer' 'miro' 'mitsubishi' 'mix' 'mixcloud' 'mobx-state-tree' 'mobx' 'mocha' 'modx' 'mojangstudios' 'moleculer' 'momenteo' 'monero' 'mongodb' 'monkeytie' 'monogram' 'monster' 'monzo' 'moo' 'moscowmetro' 'motorola' 'mozilla' 'mta' 'mtr' 'mumble' 'musescore' 'musicbrainz' 'mxlinux' 'myanimelist' 'myob' 'myspace' 'mysql' 'n26' 'namebase' 'namecheap' 'nano' 'nasa' 'nationalgrid' 'nativescript' 'naver' 'nba' 'nbb' 'ndr' 'nec' 'neo4j' 'neovim' 'nestjs' 'netapp' 'netflix' 'netlify' 'newjapanpro-wrestling' 'newrelic' 'newyorktimes' 'next-dot-js' 'nextcloud' 'nextdoor' 'nfc' 'nginx' 'ngrok' 'niconico' 'nim' 'nintendo' 'nintendo3ds' 'nintendogamecube' 'nintendonetwork' 'nintendoswitch' 'nissan' 'nixos' 'node-dot-js' 'node-red' 'nodemon' 'nokia' 'norwegian' 'notepadplusplus' 'notion' 'notist' 'nounproject' 'npm' 'nrwl' 'nubank' 'nucleo' 'nuget' 'nuke' 'numba' 'numpy' 'nutanix' 'nuxt-dot-js' 'nvidia' 'nx' 'observable' 'obsstudio' 'ocaml' 'octave' 'octopusdeploy' 'oculus' 'odnoklassniki' 'odysee' 'okcupid' 'okta' 'oneplus' 'onlyfans' 'onnx' 'onstar' 'opel' 'openaccess' 'openai' 'openaigym' 'openapiinitiative' 'openbadges' 'openbsd' 'openbugbounty' 'opencollective' 'opencontainersinitiative' 'opencv' 'openfaas' 'opengl' 'openid' 'openlayers' 'opennebula' 'opensourceinitiative' 'openssl' 'openstack' 'openstreetmap' 'opensuse' 'openvpn' 'openwrt' 'opera' 'opnsense' 'opsgenie' 'opslevel' 'oracle' 'orcid' 'org' 'origin' 'osano' 'oshkosh' 'osmc' 'osu' 'overcast' 'overleaf' 'ovh' 'owasp' 'oxygen' 'oyo' 'p5-dot-js' 'packagist' 'pagekit' 'pagerduty' 'pagespeedinsights' 'pagseguro' 'palantir' 'paloaltosoftware' 'pandas' 'pandora' 'pantheon' 'paritysubstrate' 'parse-dot-ly' 'passport' 'pastebin' 'patreon' 'payoneer' 'paypal' 'paytm' 'pcgamingwiki' 'peertube' 'pegasusairlines' 'pelican' 'peloton' 'pepsi' 'perforce' 'periscope' 'perl' 'peugeot' 'pexels' 'pfsense' 'phabricator' 'philipshue' 'phonepe' 'photobucket' 'photocrowd' 'photopea' 'php' 'phpstorm' 'pi-hole' 'picarto-dot-tv' 'picnic' 'picpay' 'pimcore' 'pinboard' 'pingdom' 'pingup' 'pinterest' 'pioneerdj' 'pivotaltracker' 'piwigo' 'pixabay' 'pixiv' 'pjsip' 'planet' 'plangrid' 'platzi' 'playcanvas' 'player-dot-me' 'playerfm' 'playstation' 'playstation2' 'playstation3' 'playstation4' 'playstation5' 'playstationvita' 'pleroma' 'plesk' 'plex' 'plotly' 'pluralsight' 'plurk' 'pluscodes' 'pm2' 'pnpm' 'pocket' 'pocketcasts' 'podcastaddict' 'podman' 'pointy' 'pokemon' 'poly' 'polymerproject' 'pop_os' 'porsche' 'postcss' 'postgresql' 'postman' 'postmates' 'powerbi' 'powers' 'powershell' 'pr-dot-co' 'pre-commit' 'premierleague' 'prestashop' 'presto' 'prettier' 'prezi' 'prime' 'primevideo' 'prisma' 'prismic' 'privateinternetaccess' 'probot' 'processingfoundation' 'processwire' 'producthunt' 'progate' 'progress' 'prometheus' 'prosieben' 'proto-dot-io' 'protocols-dot-io' 'protondb' 'protonmail' 'protonvpn' 'protools' 'proxmox' 'publons' 'pubmed' 'pug' 'puppet' 'puppeteer' 'purescript' 'purgecss' 'purism' 'pusher' 'pycharm' 'pypi' 'pypy' 'python' 'pytorch' 'pytorchlightning' 'pyup' 'qantas' 'qatarairways' 'qemu' 'qgis' 'qi' 'qiita' 'qiskit' 'qiwi' 'qt' 'qualcomm' 'qualtrics' 'quantcast' 'quantconnect' 'quantopian' 'quarkus' 'quasar' 'qubesos' 'quest' 'quickbooks' 'quicktime' 'quip' 'quora' 'qwiklabs' 'qzone' 'r' 'r3' 'rabbitmq' 'racket' 'radar' 'radiopublic' 'rainmeter' 'rakuten' 'ram' 'rancher' 'raspberrypi' 'razer' 'react' 'reactivex' 'reactos' 'reactrouter' 'readthedocs' 'realm' 'reason' 'reasonstudios' 'redbubble' 'reddit' 'redhat' 'redhatopenshift' 'redis' 'redux-saga' 'redux' 'redwoodjs' 'relianceindustrieslimited' 'renault' 'renovatebot' 'renpy' 'renren' 'repl-dot-it' 'researchgate' 'resurrectionremixos' 'retroarch' 'retropie' 'reveal-dot-js' 'reverbnation' 'revolut' 'revue' 'rewe' 'rezgo' 'rhinoceros' 'rider' 'ring' 'riotgames' 'ripple' 'riseup' 'roamresearch' 'roblox' 'robotframework' 'rocket-dot-chat' 'roku' 'rolls-royce' 'rollup-dot-js' 'roots' 'rootsbedrock' 'rootssage' 'ros' 'rottentomatoes' 'roundcube' 'rss' 'rstudio' 'rte' 'rtl' 'rtlzwei' 'ruby' 'rubygems' 'rubyonrails' 'runkeeper' 'runkit' 'rust' 'rxdb' 'ryanair' 's7airlines' 'safari' 'sahibinden' 'salesforce' 'saltstack' 'samsung' 'samsungpay' 'sanfranciscomunicipalrailway' 'saopaulometro' 'sap' 'sass' 'sat-dot-1' 'saucelabs' 'scala' 'scaleway' 'scania' 'scikit-learn' 'scipy' 'scopus' 'scratch' 'screencastify' 'scribd' 'scrimba' 'scrollreveal' 'scrumalliance' 'scrutinizerci' 'seagate' 'seat' 'sefaria' 'sega' 'selenium' 'sellfy' 'semantic-release' 'semanticuireact' 'semanticweb' 'semaphoreci' 'semver' 'sencha' 'sennheiser' 'sensu' 'sentry' 'sepa' 'serverfault' 'serverless' 'sfml' 'shanghaimetro' 'sharp' 'shazam' 'shell' 'shelly' 'shenzhenmetro' 'shields-dot-io' 'shikimori' 'shopify' 'shopware' 'shotcut' 'showpad' 'showtime' 'shutterstock' 'siemens' 'signal' 'simkl' 'simpleanalytics' 'simpleicons' 'sinaweibo' 'singlestore' 'sitepoint' 'sketch' 'sketchfab' 'sketchup' 'skillshare' 'skoda' 'sky' 'skyliner' 'skype' 'skypeforbusiness' 'slack' 'slackware' 'slashdot' 'slickpic' 'slides' 'slideshare' 'smart' 'smartthings' 'smashingmagazine' 'smrt' 'smugmug' 'snapchat' 'snapcraft' 'snowflake' 'snyk' 'society6' 'socket-dot-io' 'sogou' 'solidity' 'sololearn' 'solus' 'sonarcloud' 'sonarlint' 'sonarqube' 'sonarsource' 'songkick' 'songoda' 'sonicwall' 'sonos' 'soundcloud' 'sourceengine' 'sourceforge' 'sourcegraph' 'southwestairlines' 'spacemacs' 'spacex' 'sparkar' 'sparkasse' 'sparkfun' 'sparkpost' 'spdx' 'speakerdeck' 'spectrum' 'speedtest' 'spinnaker' 'spinrilla' 'splunk' 'spotify' 'spotlight' 'spreaker' 'spring' 'springboot' 'sprint' 'spyderide' 'sqlite' 'square' 'squareenix' 'squarespace' 'ssrn' 'stackbit' 'stackexchange' 'stackoverflow' 'stackpath' 'stackshare' 'stadia' 'staffbase' 'starlingbank' 'starship' 'startrek' 'starz' 'statamic' 'staticman' 'statuspage' 'statuspal' 'steam' 'steamdb' 'steamworks' 'steem' 'steemit' 'steinberg' 'stellar' 'stencyl' 'stimulus' 'stitcher' 'stmicroelectronics' 'storify' 'storyblok' 'storybook' 'strapi' 'strava' 'streamlit' 'stripe' 'strongswan' 'stubhub' 'styled-components' 'stylelint' 'styleshare' 'stylus' 'subaru' 'sublimetext' 'substack' 'subversion' 'suckless' 'sumologic' 'supabase' 'superuser' 'surveymonkey' 'suse' 'suzuki' 'svelte' 'svg' 'svgo' 'swagger' 'swarm' 'swift' 'swiggy' 'swiper' 'symantec' 'symfony' 'symphony' 'sympy' 'synology' 't-mobile' 'tableau' 'tado' 'tails' 'tailwindcss' 'talend' 'tampermonkey' 'taobao' 'tapas' 'tasmota' 'tata' 'taxbuzz' 'teamcity' 'teamspeak' 'teamviewer' 'ted' 'teespring' 'tekton' 'tele5' 'telegram' 'telegraph' 'tencentqq' 'tencentweibo' 'tensorflow' 'teradata' 'teratail' 'terraform' 'tesla' 'testin' 'testinglibrary' 'textpattern' 'theconversation' 'theirishtimes' 'themighty' 'themodelsresource' 'themoviedatabase' 'theregister' 'thesoundsresource' 'thespritersresource' 'thewashingtonpost' 'thingiverse' 'thinkpad' 'three-dot-js' 'threema' 'thumbtack' 'thunderbird' 'thymeleaf' 'ticketmaster' 'tidal' 'tide' 'tiktok' 'tile' 'timescale' 'tinder' 'tinyletter' 'tmux' 'todoist' 'toggl' 'tokyometro' 'tomorrowland' 'topcoder' 'toptal' 'torbrowser' 'torproject' 'toshiba' 'toyota' 'tp-link' 'tqdm' 'trainerroad' 'trakt' 'transferwise' 'transportforireland' 'transportforlondon' 'travisci' 'treehouse' 'trello' 'trendmicro' 'treyarch' 'triller' 'trino' 'trip-dot-com' 'tripadvisor' 'trove' 'truenas' 'trulia' 'trustedshops' 'trustpilot' 'tryhackme' 'tryitonline' 'ts-node' 'tui' 'tumblr' 'tunein' 'turbosquid' 'turkishairlines' 'tutanota' 'tvtime' 'twilio' 'twitch' 'twitter' 'twoo' 'typescript' 'typo3' 'uber' 'ubereats' 'ubiquiti' 'ubisoft' 'ublockorigin' 'ubuntu' 'udacity' 'udemy' 'ufc' 'uikit' 'ulule' 'umbraco' 'unacademy' 'undertale' 'unicode' 'unilever' 'unitedairlines' 'unity' 'unraid' 'unrealengine' 'unsplash' 'untangle' 'untappd' 'upcloud' 'uplabs' 'uploaded' 'ups' 'upwork' 'usps' 'v' 'v8' 'vaadin' 'vagrant' 'valve' 'vapor' 'vault' 'vauxhall' 'vbulletin' 'vectorlogozone' 'vectorworks' 'veeam' 'veepee' 'venmo' 'vercel' 'verdaccio' 'veritas' 'verizon' 'vfairs' 'viadeo' 'viber' 'vim' 'vimeo' 'vimeolivestream' 'vine' 'virb' 'virtualbox' 'virustotal' 'visa' 'visualstudio' 'visualstudiocode' 'vite' 'vivaldi' 'vivino' 'vk' 'vlcmediaplayer' 'vmware' 'vodafone' 'volkswagen' 'volvo' 'vonage' 'vox' 'vsco' 'vue-dot-js' 'vuetify' 'vulkan' 'vultr' 'w3c' 'wagtail' 'wakatime' 'walkman' 'wappalyzer' 'warnerbros-dot' 'wattpad' 'waze' 'wearos' 'weasyl' 'webassembly' 'webauthn' 'webcomponents-dot-org' 'webdriverio' 'webflow' 'webgl' 'webhint' 'weblate' 'webmin' 'webmoney' 'webpack' 'webrtc' 'webstorm' 'wechat' 'weights-and-biases' 'wemo' 'wetransfer' 'whatsapp' 'wheniwork' 'whitesource' 'wii' 'wiiu' 'wikidata' 'wikimediacommons' 'wikipedia' 'wikiquote' 'wikivoyage' 'windows' 'windows95' 'windowsterminal' 'windowsxp' 'winmate' 'wipro' 'wire' 'wireguard' 'wireshark' 'wish' 'wistia' 'wix' 'wizzair' 'wolfram' 'wolframlanguage' 'wolframmathematica' 'woo' 'woocommerce' 'wordpress' 'workplace' 'worldhealthorganization' 'wpengine' 'wprocket' 'write-dot-as' 'wwe' 'wwise' 'x-dot-org' 'x-pack' 'xamarin' 'xaml' 'xampp' 'xbox' 'xcode' 'xdadevelopers' 'xero' 'xfce' 'xiaomi' 'xilinx' 'xing' 'xmpp' 'xrp' 'xsplit' 'xstate' 'yahoo' 'yale' 'yamahacorporation' 'yamahamotorcorporation' 'yammer' 'yandex' 'yarn' 'ycombinator' 'yelp' 'yoast' 'yourtravel-dot-tv' 'youtube' 'youtubegaming' 'youtubemusic' 'youtubestudio' 'youtubetv' 'z-wave' 'zalando' 'zapier' 'zdf' 'zelle' 'zend' 'zendesk' 'zendframework' 'zenn' 'zerodha' 'zeromq' 'zerply' 'zhihu' 'zigbee' 'zillow' 'zingat' 'zoho' 'zoiper' 'zomato' 'zoom' 'zorin' 'zotero' 'zulip' ) } function Remove-DiscardedReports { [CmdletBinding()] param( [Array] $FilePathsGenerated, [string] $FolderPath, [string] $Extension ) $FilePathsCurrent = Get-ChildItem -LiteralPath $FolderPath -File -Filter "*$($Extension)" | Select-Object -ExpandProperty FullName foreach ($File in $FilePathsCurrent) { if ($File -notin $FilePathsGenerated) { Write-Color -Text '[i]', '[FILE ] ', "Removing discarded report: $File" -Color Yellow, DarkGray, Yellow Remove-Item -LiteralPath $File -Force } } } function Set-FolderConfiguration { [CmdletBinding()] param( [System.Collections.IDictionary]$Folders, [Array] $FoldersConfiguration, [System.Collections.IDictionary]$FolderLimit ) Write-Color -Text '[i]', "[TheDashboard] ", 'Setting Folder Configuration', ' [Informative] ' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta foreach ($Folder in $FoldersConfiguration) { if (-not $Folder.Name -and $Folder.Url) { $Folder.Name = $Folder.Url } elseif (-not $Folder.Url -and $Folder.Name) { $Folder.Url = $Folder.Name } if ($FolderLimit -and $FolderLimit.Name -and -not $Folder.DisableGlobalLimits) { $Folder.LimitsConfiguration[$FolderLimit.Name] = $FolderLimit } $Folders[$Folder.Name] = $Folder } } function New-DashboardEmailParameter { [cmdletBinding(DefaultParameterSetName = 'Compatibility', SupportsShouldProcess)] param( [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [alias('SmtpServer')][string] $Server, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [int] $Port, [Parameter(Mandatory, ParameterSetName = 'SecureString')] [Parameter(Mandatory, ParameterSetName = 'oAuth')] [Parameter(Mandatory, ParameterSetName = 'Graph')] [Parameter(Mandatory, ParameterSetName = 'MgGraphRequest')] [Parameter(Mandatory, ParameterSetName = 'Compatibility')] [Parameter(Mandatory, ParameterSetName = 'SendGrid')] [object] $From, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [Parameter(ParameterSetName = 'Compatibility')] [Parameter(ParameterSetName = 'SendGrid')] [string] $ReplyTo, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [Parameter(ParameterSetName = 'Compatibility')] [Parameter(ParameterSetName = 'SendGrid')] [alias('Importance')][ValidateSet('Low', 'Normal', 'High')][string] $Priority, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [ValidateSet('None', 'OnSuccess', 'OnFailure', 'Delay', 'Never')][string[]] $DeliveryNotificationOption, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [MailKit.Net.Smtp.DeliveryStatusNotificationType] $DeliveryStatusNotificationType, [Parameter(ParameterSetName = 'oAuth')] [Parameter(Mandatory, ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'Compatibility')] [Parameter(Mandatory, ParameterSetName = 'SendGrid')] [pscredential] $Credential, [Parameter(ParameterSetName = 'SecureString')] [string] $Username, [Parameter(ParameterSetName = 'SecureString')] [string] $Password, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [MailKit.Security.SecureSocketOptions] $SecureSocketOptions, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [switch] $UseSsl, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [switch] $SkipCertificateRevocation, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [alias('SkipCertificateValidatation')][switch] $SkipCertificateValidation, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [int] $Timeout, [Parameter(ParameterSetName = 'oAuth')] [alias('oAuth')][switch] $oAuth2, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $RequestReadReceipt, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $RequestDeliveryReceipt, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $Graph, [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $MgGraphRequest, [Parameter(ParameterSetName = 'SecureString')] [switch] $AsSecureString, [Parameter(ParameterSetName = 'SendGrid')] [switch] $SendGrid, [Parameter(ParameterSetName = 'SendGrid')] [switch] $SeparateTo, [Parameter(ParameterSetName = 'Graph')] [Parameter(ParameterSetName = 'MgGraphRequest')] [switch] $DoNotSaveToSentItems, [Parameter(ParameterSetName = 'SecureString')] [Parameter(ParameterSetName = 'oAuth')] [Parameter(ParameterSetName = 'Compatibility')] [string] $LocalDomain ) $Output = [ordered] @{ Type = 'DashboardConfigurationEmail' Settings = [ordered] @{ Server = if ($PSBoundParameters.ContainsKey('Server')) { $Server } else { $null } Port = if ($PSBoundParameters.ContainsKey('Port')) { $Port } else { $null } From = if ($PSBoundParameters.ContainsKey('From')) { $From } else { $null } ReplyTo = if ($PSBoundParameters.ContainsKey('ReplyTo')) { $ReplyTo } else { $null } Priority = if ($PSBoundParameters.ContainsKey('Priority')) { $Priority } else { $null } DeliveryNotificationOption = if ($PSBoundParameters.ContainsKey('DeliveryNotificationOption')) { $DeliveryNotificationOption } else { $null } DeliveryStatusNotificationType = if ($PSBoundParameters.ContainsKey('DeliveryStatusNotificationType')) { $DeliveryStatusNotificationType } else { $null } Credential = if ($PSBoundParameters.ContainsKey('Credential')) { $Credential } else { $null } Username = if ($PSBoundParameters.ContainsKey('Username')) { $Username } else { $null } Password = if ($PSBoundParameters.ContainsKey('Password')) { $Password } else { $null } SecureSocketOptions = if ($PSBoundParameters.ContainsKey('SecureSocketOptions')) { $SecureSocketOptions } else { $null } UseSsl = if ($PSBoundParameters.ContainsKey('UseSsl')) { $UseSsl } else { $null } SkipCertificateRevocation = if ($PSBoundParameters.ContainsKey('SkipCertificateRevocation')) { $SkipCertificateRevocation } else { $null } SkipCertificateValidation = if ($PSBoundParameters.ContainsKey('SkipCertificateValidatation')) { $SkipCertificateValidation } else { $null } Timeout = if ($PSBoundParameters.ContainsKey('Timeout')) { $Timeout } else { $null } oAuth2 = if ($PSBoundParameters.ContainsKey('oAuth2')) { $oAuth2 } else { $null } RequestReadReceipt = if ($PSBoundParameters.ContainsKey('RequestReadReceipt')) { $RequestReadReceipt } else { $null } RequestDeliveryReceipt = if ($PSBoundParameters.ContainsKey('RequestDeliveryReceipt')) { $RequestDeliveryReceipt } else { $null } Graph = if ($PSBoundParameters.ContainsKey('Graph')) { $Graph } else { $null } MgGraphRequest = if ($PSBoundParameters.ContainsKey('MgGraphRequest')) { $MgGraphRequest } else { $null } AsSecureString = if ($PSBoundParameters.ContainsKey('AsSecureString')) { $AsSecureString } else { $null } SendGrid = if ($PSBoundParameters.ContainsKey('SendGrid')) { $SendGrid } else { $null } SeparateTo = if ($PSBoundParameters.ContainsKey('SeparateTo')) { $SeparateTo } else { $null } DoNotSaveToSentItems = if ($PSBoundParameters.ContainsKey('DoNotSaveToSentItems')) { $DoNotSaveToSentItems } else { $null } WhatIf = $WhatIfPreference } } Remove-EmptyValue -Hashtable $Output.Settings $Output } function New-DashboardFolder { <# .SYNOPSIS This function is used to define a new folder for the dashboard that will be displayed as a TopLevelMenu. .DESCRIPTION This function is used to define a new folder for the dashboard that will be displayed as a TopLevelMenu. It allows you to define the folder name, path, and URL name. You can also specify the icon that will be used for the folder. .PARAMETER Entries This parameter allows you to define the content of the folder. It should be a script block that contains the content of the folder. The content of the folder can be defined using the New-DashboardReplacement, New-DashboardLimit functions .PARAMETER Name The name of the folder that will be displayed in the dashboard. .PARAMETER Path Path to Reports folder where the reports are stored. .PARAMETER UrlName The URL name for the dashboard folder. This is used to create the URL for the folder. .PARAMETER CopyFrom Copy files from this location to the existing location that is specified by the Path parameter. This is useful for copying reports from different locations to the expected location. .PARAMETER MoveFrom Move files from this location to the existing location that is specified by the Path parameter. This is useful for moving reports from different locations to the expected location. .PARAMETER Extension This parameter specifies the file extensions to include when processing files. It will by default use the same extension as the root of the dashboard, but this one can include additional extensions. .PARAMETER DisableGlobalReplacement This parameter, when set, will disable global replacements for the dashboard for this particular folder. This means that the replacements defined in the root of the dashboard will not be applied to the content of this folder. .PARAMETER DisableGlobalLimits This parameter, when set, will disable global limits for the dashboard for this particular folder. This means that the limits defined in the root of the dashboard will not be applied to the content of this folder. .PARAMETER IconBrands Specifies the icon that will be used for the folder. This is a brand icon from FontAwesome. .PARAMETER IconRegular Specifies the icon that will be used for the folder. This is a regular icon from FontAwesome. .PARAMETER IconSolid Specifies the icon that will be used for the folder. This is a solid icon from FontAwesome. .EXAMPLE New-DashboardFolder -Name 'Group Policies' -IconBrands android -UrlName 'GPO' -Path $PSScriptRoot\Reports\TestPolicies .EXAMPLE New-DashboardFolder -Name 'Windows Updates' -IconBrands android -UrlName 'WindowsUpdates' -Path $PSScriptRoot\Reports\WindowsUpdates { $newDashboardReplacementSplat = @{ BeforeSplit = @{ 'GroupMembership-' = '' '_Regional' = ' Regional' 'GPOZaurr' = '' 'Testimo' = '' } #AfterSplitPositionName = 1, 2 AfterSplitPositionName = [ordered] @{ 'PingCastle-Domain-*' = 0, 2 '*' = 0 # '*' = 1, 2 } AfterRemoveDoubleSpaces = $true AfterUpperChars = "evotec.com", "test.com" AfterSplit = @{ 'G P O' = 'GPO' 'L D A P' = 'LDAP' 'D H C P' = 'DHCP' 'A D' = 'AD' 'I T R X X' = 'ITRXX' 'I N S' = 'INS' 'L A P S' = 'LAPS' 'K R B G T' = 'KRBGT' } SplitOn = "[_-]" AddSpaceToName = $true } New-DashboardReplacement @newDashboardReplacementSplat New-DashboardLimit -LimitItem 2 -IncludeHistory } -DisableGlobalReplacement .NOTES General notes #> [alias('New-TheDashboardFolder')] [CmdletBinding()] param( [Parameter(Position = 0)][scriptblock] $Entries, [Parameter(Mandatory)][string] $Name, [Parameter(Mandatory)][string] $Path, [Parameter(Mandatory)][string] $UrlName, [string[]] $CopyFrom, [string[]] $MoveFrom, [string[]] $Extension, [switch] $DisableGlobalReplacement, [switch] $DisableGlobalLimits, # ICON BRANDS [ArgumentCompleter( { param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters) ($Global:HTMLIcons.FontAwesomeBrands.Keys) } )] [ValidateScript( { $_ -in (($Global:HTMLIcons.FontAwesomeBrands.Keys)) } )] [parameter(ParameterSetName = "FontAwesomeBrands")][string] $IconBrands, # ICON REGULAR [ArgumentCompleter( { param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters) ($Global:HTMLIcons.FontAwesomeRegular.Keys) } )] [ValidateScript( { $_ -in (($Global:HTMLIcons.FontAwesomeRegular.Keys)) } )] [parameter(ParameterSetName = "FontAwesomeRegular")][string] $IconRegular, # ICON SOLID [ArgumentCompleter( { param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters) ($Global:HTMLIcons.FontAwesomeSolid.Keys) } )] [ValidateScript( { $_ -in (($Global:HTMLIcons.FontAwesomeSolid.Keys)) } )] [parameter(ParameterSetName = "FontAwesomeSolid")][string] $IconSolid ) if ($IconBrands) { $IconType = 'IconBrands' $Icon = $IconBrands } elseif ($IconRegular) { $IconType = 'IconRegular' $Icon = $IconRegular } elseif ($IconSolid) { $IconType = 'IconSolid' $Icon = $IconSolid } else { $IconType = 'IconSolid' $Icon = 'folder' } if ($Entries) { $ReplacementConfiguration = [System.Collections.Generic.List[System.Collections.IDictionary]]::new() $LimitsConfiguration = [ordered] @{} $OutputElements = & $Entries foreach ($E in $OutputElements) { if ($E.Type -eq 'Replacement') { $ReplacementConfiguration.Add($E.Settings) } elseif ($E.Type -eq 'FolderLimit') { $LimitsConfiguration[$E.Settings.Name] = $E.Settings } } $ReplacementsConfiguration = Convert-MultipleReplacements -ReplacementConfiguration $ReplacementConfiguration } else { $LimitsConfiguration = [ordered] @{} $ReplacementsConfiguration = $null } $Folder = [ordered] @{ Type = 'Folder' Settings = [ordered] @{ Name = $Name IconType = $IconType Icon = $Icon Path = $Path Url = $UrlName CopyFrom = $CopyFrom MoveFrom = $MoveFrom Extension = $Extension ReplacementsGlobal = -not $DisableGlobalReplacement.IsPresent Replacements = $ReplacementsConfiguration LimitsConfiguration = $LimitsConfiguration DisableGlobalLimits = $DisableGlobalLimits.IsPresent } } Remove-EmptyValue -Hashtable $Folder -Recursive -ExcludeParameter 'LimitsConfiguration', 'Replacements' -DoNotRemoveEmptyDictionary $Folder } function New-DashboardGage { [alias('New-TheDashboardGage')] [cmdletBinding()] param( [Parameter(Mandatory)][string] $Label, [Parameter()][int] $MinValue, [Parameter(Mandatory)][int] $MaxValue, [Parameter(Mandatory)][int] $Value, [Parameter(Mandatory)][DateTime] $Date ) [ordered] @{ Type = 'Gage' Settings = [ordered] @{ Label = $Label Value = $Value Date = $Date MinValue = $MinValue MaxValue = $MaxValue } } } function New-DashboardLimit { <# .SYNOPSIS Sets limit rules (date, days, item count) for dashboard reports. .DESCRIPTION Creates a structured object defining limits for how many reports to keep, how many to store in history, and date-based handling when displaying files. .PARAMETER Name Specifies which reports the limit applies to. .PARAMETER LimitItem Limits how many newest files are kept before moving to history. .PARAMETER LimitDate Specifies date threshold for including files in the menu. .PARAMETER LimitDays Specifies how many days back reports are displayed. .PARAMETER IncludeHistory Indicates whether older files are added to a history list instead of excluded. .PARAMETER IncludeHistoryLimitDate Date threshold after which history entries should not be included. .PARAMETER IncludeHistoryLimitDays Number of days to preserve history entries. .PARAMETER IncludeHistoryLimit Limits how many history entries are kept. .EXAMPLE New-DashboardLimit -LimitItem 3 -LimitDays 10 .NOTES Part of TheDashboard module, used to configure file retention rules. #> [CmdletBinding()] param( [string] $Name, [nullable[int]] $LimitItem, [nullable[DateTime]] $LimitDate, [nullable[int]] $LimitDays, [switch] $IncludeHistory, [nullable[DateTime]] $IncludeHistoryLimitDate, [nullable[int]] $IncludeHistoryLimitDays, [nullable[int]] $IncludeHistoryLimit ) if (-not $Name) { $Name = '*' } if ($IncludeHistoryLimitDate -or $IncludeHistoryLimitDays -or $IncludeHistoryLimit) { $IncludeHistory = $true } $Limit = [ordered] @{ Type = 'FolderLimit' Settings = [ordered] @{ Name = $Name LimitItem = $LimitItem LimitDate = $LimitDate LimitDays = $LimitDays IncludeHistory = $IncludeHistory IncludeHistoryLimit = $IncludeHistoryLimit IncludeHistoryLimitDate = $IncludeHistoryLimitDate IncludeHistoryLimitDays = $IncludeHistoryLimitDays } } Remove-EmptyValue -Hashtable $Limit.Settings $Limit } function New-DashboardNotification { [CmdletBinding(DefaultParameterSetName = 'Report')] param( [Parameter(ParameterSetName = 'Report')][string] $Name, [Parameter(ParameterSetName = 'Report')][string] $Category, [string] $Type, [string] $Filter, [Parameter(Mandatory)][string] $EmailTo, [Parameter()][string] $EmailCC, [Parameter(Mandatory)][string] $EmailSubject ) $Notification = [ordered] @{ Type = 'Notification' Settings = [ordered] @{ Id = [Guid]::NewGuid().ToString() Name = $Name Category = $Category Type = $Type Filter = $Filter EmailTo = $EmailTo EmailCC = $EmailCC EmailSubject = $EmailSubject } } Remove-EmptyValue -Hashtable $Notification $Notification } function New-DashboardReplacement { <# .SYNOPSIS Helps preparing Menu Names and Menu Entries for the dashboard from the file names .DESCRIPTION Helps preparing Menu Names and Menu Entries for the dashboard from the file names .PARAMETER SplitOn Character to split on, for example on '_' or in most cases [_-] which means either _ or - .PARAMETER BeforeSplit Dictionary of replacements to be done before splitting .PARAMETER BeforeRemoveChars Array of characters to remove from the name before splitting, such as specific prefixes or suffixes. .PARAMETER AfterSplit Dictionary of replacements to be done after splitting the file name. .PARAMETER AddSpaceToName Switch indicating whether to add spaces between words in the menu name. .PARAMETER AfterSplitPositionName Array indicating which positions to select from the split file name after splitting. .PARAMETER AfterRemoveChars Array of characters to remove from the name after splitting, such as specific unwanted characters. .PARAMETER AfterUpperChars Array of strings to convert to upper case after splitting. .PARAMETER AfterRemoveDoubleSpaces Switch indicating whether to remove double spaces from the formatted name. .PARAMETER ReplaceCaseInsenstive Switch indicating whether replacements should be case insensitive. .PARAMETER ReplaceSkipRegex Array of regex patterns to skip during the replacement process. .EXAMPLE An example of how to use the New-DashboardReplacement function. For example: ```powershell New-DashboardReplacement -SplitOn '_' -BeforeSplit @{ 'GPO' = '' } -AfterSplit @{ 'BlockedInheritance' = 'Blocked Inheritance' } -AddSpaceToName ``` .NOTES The process of replacement is done in a specific order. - Before Split Replacements => to fix the name before splitting, say remove some parts we don't like - SplitOn => split on specific character or characters, for example on '_' or in most cases [_-] which means either _ or - - PositionName => Pick specific parts of the split based on the full file name after first replacements - FormatString => Format the string based on settings provided - After Split Replacements => Replacements after split The way it works is: - We take file 'GPOBlockedInheritance_2023-10-05_092149.aspx' - We first do BeforeSplit replacements: - It could be for example removing 'GPO' from the name or changing Inheritance to something else - We then split on specific character, for example on '_' or in most cases [_-] which means either _ or - - After splitting, we will select specific parts of the file name based on the split results, to create a formatted name for the output. - If we split on [_-] we get 'GPOBlockedInheritance', '2023', '10', '05', '092149.aspx' - However keep in mind we removed 'GPO' in BeforeSplit, so we would get 'BlockedInheritance', '2023', '10', '05', '092149.aspx' - So now we can select specific parts of the split, for example 0, 1, 2, 3, 4, 5 - If we select 0, 1 we would get 'BlockedInheritance', '2023' - We then merge the selected parts into a single string, for example 'BlockedInheritance 2023' which will be our menu name - However at this point we also can do 'AddSpaceToName' which basically adds space between words, so 'BlockedInheritance 2023' becomes 'Blocked Inheritance 2023' - This step is necessary to improve readability and user experience. - However the way it works to add a space is to search for all high case letters and add space before them, so 'BlockedInheritance 2023' becomes 'Blocked Inheritance 2023' - This works fine for most cases, but sometimes we need to do additional replacements, for example 'GPO' becomes 'G P O', 'PingCastle' becomes 'Ping Castle' - We then do AfterSplit replacements, for example replacing 'G P O' with 'GPO' or 'Ping Castle' back to 'PingCastle' #> [alias('New-TheDashboardReplacement')] [CmdletBinding()] param( [string] $SplitOn, [Array] $BeforeSplit, [alias('RemoveCharsBefore')][string[]] $BeforeRemoveChars, [Array] $AfterSplit, [alias('AfterAddSpaceToName')][switch] $AddSpaceToName, [object] $AfterSplitPositionName, [alias('RemoveCharsAfter')][string[]] $AfterRemoveChars, [alias('ConvertToUpperChars')][string[]] $AfterUpperChars, [alias('RemoveDoubleSpaces')][switch] $AfterRemoveDoubleSpaces, [switch] $ReplaceCaseInsenstive, [switch] $ReplaceSkipRegex ) $BeforeEntry = [ordered] @{} foreach ($Before in $BeforeSplit) { foreach ($Key in $Before.Keys) { $BeforeEntry[$Key] = $Before[$Key] } } $AfterEntry = [ordered] @{} foreach ($After in $AfterSplit) { foreach ($Key in $After.Keys) { $AfterEntry[$Key] = $After[$Key] } } $Replacements = [ordered] @{ Type = 'Replacement' Settings = @{ SplitOn = if ($PSBoundParameters.ContainsKey('SplitOn')) { $SplitOn } else { $null } BeforeSplit = if ($PSBoundParameters.ContainsKey('BeforeSplit')) { $BeforeEntry } else { $null } AfterSplit = if ($PSBoundParameters.ContainsKey('AfterSplit')) { $AfterEntry } else { $null } AddSpaceToName = if ($PSBoundParameters.ContainsKey('AddSpaceToName')) { $AddSpaceToName } else { $null } AfterSplitPositionName = if ($PSBoundParameters.ContainsKey('AfterSplitPositionName')) { $AfterSplitPositionName } else { $null } AfterRemoveChars = if ($PSBoundParameters.ContainsKey('AfterRemoveChars')) { $AfterRemoveChars } else { $null } AfterUpperChars = if ($PSBoundParameters.ContainsKey('AfterUpperChars')) { $AfterUpperChars } else { $null } AfterRemoveDoubleSpaces = if ($PSBoundParameters.ContainsKey('AfterRemoveDoubleSpaces')) { $AfterRemoveDoubleSpaces } else { $null } BeforeRemoveChars = if ($PSBoundParameters.ContainsKey('BeforeRemoveChars')) { $BeforeRemoveChars } else { $null } ReplaceCaseInsenstive = if ($PSBoundParameters.ContainsKey('ReplaceCaseInsenstive')) { $ReplaceCaseInsenstive } else { $null } ReplaceSkipRegex = if ($PSBoundParameters.ContainsKey('ReplaceSkipRegex')) { $ReplaceSkipRegex } else { $null } } } $Replacements } function Repair-DashboardContent { <# .SYNOPSIS This function helps to replace content in files with a specific extension in a specific directory with a new content. .DESCRIPTION This function helps to replace content in files with a specific extension in a specific directory with a new content. .PARAMETER Directory Directory where files are located .PARAMETER ExtensionFrom File extension to look for. Default is .aspx .EXAMPLE $SearchString = "_|:|@|#|<-|←|<:|<%|=|=>|⇒|>:" $ReplaceString = "_|:|@|#|<-|←|<:|<-%|=|=>|⇒|>:" $Directory = "C:\Users\przemyslaw.klys\Downloads" Repair-ReportContent -Directory $Directory -Search $SearchString -Replace $ReplaceString -EscapeRegex .EXAMPLE $SearchString = '<meta http-equiv="Content-Security-Policy".*?/>' $ReplaceString = '' $Directory = "C:\Support\GitHub\TheDashboard\Examples\Reports\Security" Repair-ReportContent -Directory $Directory -Search $SearchString -Replace $ReplaceString .NOTES This is a fix for a SharePoint error that occurs when a string contains "<%" - Sorry, something went wrong. An error occurred during the processing of /sites/TheDashboard/Shared Documents/Gpozaurr_GPOBrokenPartially_2024-03-27_025637.aspx. The server block is not well formed. The reports created by several scripts contain "<%" in the string, which is not allowed in SharePoint. This is used by the Enlighter.JS library and shows up when not using -Online switch. Since the enlighter.js library is put inside HTML directly it causes issues with SharePoint. The script will replace "<%" with "<-%" in all files with the .aspx extension in the Reports folder. This is also a fix for PingCastle reports that contain characters that block it from hosting on Sharepoint or IIS (<meta http-equiv="Content-Security-Policy".*?/>) #> [CmdletBinding(SupportsShouldProcess)] param( [parameter(Mandatory)][string] $Directory, [string] $ExtensionFrom = '.aspx', [string] $Search, [string] $Replace, [switch] $EscapeRegex, [int] $OnlyNewerThan, [switch] $AddOneMinute ) if ($EscapeRegex) { $SearchString = [regex]::Escape($Search) $ReplaceString = [regex]::Escape($Replace) } else { $SearchString = $Search $ReplaceString = $Replace } if (-not (Test-Path -Path $Directory)) { Write-Color -Text '[i]', "[TheDashboard] ", "Directory $Directory does not exist" -Color Yellow, DarkGray, Yellow, DarkGray, Magenta return } $Files = Get-ChildItem -Path $Directory -File -Recurse -Include "*$ExtensionFrom" foreach ($File in $Files) { if ($File.Extension -eq $ExtensionFrom) { if ($OnlyNewerThan -and $File.LastWriteTime -lt (Get-Date).AddDays(-$OnlyNewerThan)) { continue } Write-Color -Text '[i]', "[TheDashboard] ", "Processing fixes $($File.FullName) / $($File.LastWriteTime)" -Color Yellow, DarkGray, Yellow, DarkGray, Magenta $originalCreationTime = $File.CreationTime $originalLastWriteTime = $File.LastWriteTime $Encoding = Get-FileEncoding -Path $File.FullName if ($PSVersionTable.PSVersion.Major -ge 7) { $FileContent = Get-Content -Raw -Path $File.FullName -Encoding $Encoding } else { if ($Encoding -eq 'UTF8BOM') { $Encoding = 'UTF8' } $FileContent = Get-Content -Raw -Path $File.FullName -Encoding $Encoding } if ($FileContent -match $SearchString) { Write-Color -Text '[i]', "[TheDashboard] ", "Processing fixes $($File.FullName) for ($SearchString)" -Color Green $NewContent = $FileContent -replace $SearchString, $ReplaceString $NewContent | Set-Content -Path $File.FullName -Encoding $Encoding $item = Get-Item $File.FullName $item.CreationTime = $originalCreationTime if ($AddOneMinute) { $item.LastWriteTime = $originalLastWriteTime.AddMinutes(1) } else { $item.LastWriteTime = $originalLastWriteTime } } } } } function Repair-DashboardExtension { <# .SYNOPSIS This function renames the extension of files in a given directory. .DESCRIPTION The Repair-DashboardExtension function is used to rename the extension of files in a specified directory from one extension to another. It's a part of the process of converting the reports to a SharePoint compatible format. .PARAMETER Path The path to the directory containing the files whose extensions are to be renamed. .PARAMETER ExtensionFrom The current extension of the files to be renamed. .PARAMETER ExtensionTo The new extension to be given to the files. .EXAMPLE Repair-DashboardExtension -Path "C:\Reports" -ExtensionFrom ".html" -ExtensionTo ".aspx" This command renames the extension of all .html files in the C:\Reports directory to .aspx. .NOTES If a file with the new name already exists in the directory, the function will skip renaming that file. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)][string] $Path, [Parameter(Mandatory)][string] $ExtensionFrom, [Parameter(Mandatory)][string] $ExtensionTo ) if (-not (Test-Path -Path $Path)) { Write-Color -Text '[i]', "[TheDashboard] ", "Directory $Path does not exist" -Color Yellow, DarkGray, Yellow, DarkGray, Magenta return } $Files = Get-ChildItem -Path $Path -File -Recurse -Include "*$ExtensionFrom" foreach ($File in $Files) { if ($File.Extension -eq $ExtensionFrom) { Write-Color -Text '[i]', "[TheDashboard] ", "Processing rename $($File.FullName) / $($File.LastWriteTime)" -Color Yellow, DarkGray, Yellow, DarkGray, Magenta $NewName = $File.FullName -replace "$($ExtensionFrom)$", $ExtensionTo $ExpectedName = [io.path]::Combine($File.DirectoryName, $NewName) if (Test-Path -LiteralPath $ExpectedName) { Write-Color -Text "[i]", "[TheDashboard] ", "File already exists: $($ExpectedName). Skipping" -Color Yellow, DarkGray, Yellow, DarkGray, Magenta continue } else { if ($PSCmdlet.ShouldProcess($File.FullName, "Rename to $NewName")) { Rename-Item -Path $File.FullName -NewName $NewName -Force } } } } } function Start-TheDashboard { <# .SYNOPSIS Generates TheDashboard from multiple provided reports in form of HTML files. .DESCRIPTION Generates TheDashboard from multiple provided reports in form of HTML files. It also generates statistics and charts based on the data provided by additional data. .PARAMETER Elements ScriptBlock that accepts New-Dashboard* functions, allowing for configuration of TheDashboard. .PARAMETER HTMLPath Path to HTML files that will be generated. .PARAMETER StatisticsPath Path to XML file that will save some of the data that is used to generate charts .PARAMETER Logo Path to logo that will be used in the header. .PARAMETER Folders Folders that will be used to generate TheDashboard. Can co-exist with Elements parameter, and configuration using both will be merged. .PARAMETER UrlPath URL that will be used as a starting point for TheDashboard, and used by all the links. By default the URL uses relative path, but it can be changed to absolute path. This is useful for example when you want to use TheDashboard on a SharePoint site, where the files are stored in a different place. .PARAMETER Replacements Replacements that will be used to replace names within TheDashboard. .PARAMETER RemoveNotIncluded Removes files that are not included in the menu. This is useful to clean up files that are not displayed, but be careful during testing. .PARAMETER DeleteMethod Specifies how files are removed when they are not included in the menu. By default, files are moved to the Recycle Bin. All options are: RemoveItem, DotNetDelete, RecycleBin. .PARAMETER TestMode Pretends to generate TheDashboard, but does not actually generate it. Other processes are executed, but no HTML files are created. This is useful for testing along with PassThru parameter, which will return all the data that would be used to generate TheDashboard. .PARAMETER ShowHTML Show TheDashboard in browser after generating it. .PARAMETER Online Tells Dashboard to use CSS/JS from CDN instead of local files. .PARAMETER PassThru Returns the object that was generated by the script. .PARAMETER LogPath Path to log file where all actions will be logged .PARAMETER LogMaximum Maximum number of log files to keep. If 0 then unlimited. Default unlimited. Please keep in mind that this will only work if the logs are in the dedicated folder. If you use the same folder as the script, then logging deletion will be disabled. .PARAMETER LogShowTime Show time in console output. Default $false. Logs will always have time. .PARAMETER LogTimeFormat Time format to use in log file. Default "yyyy-MM-dd HH:mm:ss" .PARAMETER Force Forces the script to regenerate existing files regardless if they need to be updated or not. .EXAMPLE An example .NOTES General notes #> [cmdletBinding(SupportsShouldProcess)] param( [Parameter(Position = 0)][ScriptBlock] $Elements, [Parameter(Position = 1, Mandatory)][alias('FilePath')][string] $HTMLPath, [string] $StatisticsPath, [string] $Logo, [System.Collections.IDictionary] $Folders, [System.Collections.IDictionary] $Replacements, [switch] $RemoveNotIncluded, [ValidateSet("RemoveItem", "DotNetDelete", "RecycleBin")] [string] $DeleteMethod = "RecycleBin", [Uri] $UrlPath, [switch] $ShowHTML, [switch] $Online, [switch] $PassThru, [switch] $TestMode, [string] $LogPath, [int] $LogMaximum, [switch] $LogShowTime, [string] $LogTimeFormat, [switch] $Force ) $Script:Reporting = [ordered] @{} $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Start-TheDashboard' -RepositoryOwner 'evotecit' -RepositoryName 'TheDashboard' Set-LoggingCapabilities -LogPath $LogPath -LogMaximum $LogMaximum -ShowTime:$LogShowTime -TimeFormat $LogTimeFormat -ScriptPath $MyInvocation.ScriptName -ParameterPSDefaultParameterValues $Script:PSDefaultParameterValues Write-Color '[i]', "[TheDashboard] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta if ($TestMode) { Write-Color -Text '[i]', "[TheDashboard] ", 'TestMode enabled', ' [Informative] ' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta $WhatIfPreference = $true } if (-not $Folders) { $Folders = [ordered] @{} } $GageConfiguration = [System.Collections.Generic.List[System.Collections.IDictionary]]::new() $FoldersConfiguration = [System.Collections.Generic.List[System.Collections.IDictionary]]::new() $ReplacementConfiguration = [System.Collections.Generic.List[System.Collections.IDictionary]]::new() $FolderLimit = [ordered] @{} $convertNestedElementsSplat = @{ Elements = $Elements GageConfiguration = $GageConfiguration FoldersConfiguration = $FoldersConfiguration ReplacementConfiguration = $ReplacementConfiguration FolderLimit = $FolderLimit } Convert-NestedElements @convertNestedElementsSplat $Extension = [io.path]::GetExtension($HTMLPath) $FolderPath = [io.path]::GetDirectoryName($HTMLPath) $ExportData = Import-DashboardStatistics -StatisticsPath $StatisticsPath $Replacements = Convert-MultipleReplacements -Replacements $Replacements -ReplacementConfiguration $ReplacementConfiguration Set-FolderConfiguration -Folders $Folders -FoldersConfiguration $FoldersConfiguration -FolderLimit $FolderLimit Copy-HTMLFiles -Folders $Folders -Extension $Extension $Files = Convert-FilesToMenuData -Folders $Folders -Replacements $Replacements -Extension $Extension $MenuBuilder = Convert-FilesToMenu -Files $Files -Folders $Folders -ExportData $ExportData -Force:$Force.IsPresent $FilePathsGenerated = New-HTMLReport -OutputElements $GageConfiguration -Logo $Logo -MenuBuilder $MenuBuilder -Configuration $Configuration -ExportData $ExportData -Files $Files -ShowHTML:$ShowHTML.IsPresent -HTMLPath $HTMLPath -Online:$Online.IsPresent -Force:$Force.IsPresent -Extension $Extension -UrlPath $UrlPath -Pretend:$TestMode.IsPresent Remove-DiscardedReports -FilePathsGenerated $FilePathsGenerated -FolderPath $FolderPath -Extension $Extension $FilesToKeepOrRemove = Convert-FilesToKeepOrRemove -FilePathsGenerated $FilePathsGenerated -Files $Files -DeleteMethod $DeleteMethod -RemoveNotIncluded:$RemoveNotIncluded.IsPresent -WhatIf:$WhatIfPreference if ($PassThru) { [ordered] @{ FilesToKeepOrRemove = $FilesToKeepOrRemove Folders = $Folders Extension = $Extension Files = $Files MenuBuilder = $MenuBuilder Statistics = $ExportData.Statistics StatisticsPath = $StatisticsPath Replacements = $Replacements FilePathsGenerated = $FilePathsGenerated } } $ExportData['MenuBuilder'] = $MenuBuilder if ($StatisticsPath) { try { $ExportData | Export-Clixml -Depth 5 -LiteralPath $StatisticsPath -ErrorAction Stop -WhatIf:$false } catch { Write-Color -Text '[e]', "[TheDashboard] ", 'Failed to export statistics', ' [Error] ', $_.Exception.Message -Color Yellow, DarkGray, Yellow, DarkGray, Red } } Write-Color -Text '[i]', "[TheDashboard] ", 'Done', ' [Informative] ' -Color Yellow, DarkGray, Yellow, DarkGray, Magenta } Export-ModuleMember -Function @('New-DashboardEmailParameter', 'New-DashboardFolder', 'New-DashboardGage', 'New-DashboardLimit', 'New-DashboardNotification', 'New-DashboardReplacement', 'Repair-DashboardContent', 'Repair-DashboardExtension', 'Start-TheDashboard') -Alias @('New-TheDashboardFolder', 'New-TheDashboardGage', 'New-TheDashboardReplacement') # SIG # Begin signature block # MIItqwYJKoZIhvcNAQcCoIItnDCCLZgCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCwyeM/1gpphIBM # tlb/PXrp9iJftzbsyV3Vj1QwBVyhLaCCJq4wggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG # SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw # aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK # EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm # dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu # d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD # eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1 # XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld # QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS # YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm # M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT # QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx # fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD # VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq # hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4 # XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ # aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg # X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk # apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL # FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy # 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u # KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54 # zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8 # 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8 # aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w # ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT # MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1 # c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG # SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS # g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9 # /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn # HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0 # VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f # sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj # gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0 # QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv # mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T # /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk # 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r # mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E # FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n # P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG # CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu # Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln # aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v # Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV # HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB # AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp # wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl # zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ # cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe # Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j # Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh # IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6 # OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw # N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR # 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2 # VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ # CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV # MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t # MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw # MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT # aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF # AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k # jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9 # NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9 # URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY # E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS # 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa # wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w # c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR # Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2 # 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK # ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC # AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2 # O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P # MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB # AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr # BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1 # c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln # aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH # BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6 # mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/ # SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY # gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9 # kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ # 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew # Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm # Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA # SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr # y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR # ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu # v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGvDCCBKSgAwIBAgIQC65mvFq6f5WHxvnp # BOMzBDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5 # NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTI0MDkyNjAwMDAwMFoXDTM1MTEy # NTIzNTk1OVowQjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSAwHgYD # VQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQAD # ggIPADCCAgoCggIBAL5qc5/2lSGrljC6W23mWaO16P2RHxjEiDtqmeOlwf0KMCBD # Er4IxHRGd7+L660x5XltSVhhK64zi9CeC9B6lUdXM0s71EOcRe8+CEJp+3R2O8oo # 76EO7o5tLuslxdr9Qq82aKcpA9O//X6QE+AcaU/byaCagLD/GLoUb35SfWHh43rO # H3bpLEx7pZ7avVnpUVmPvkxT8c2a2yC0WMp8hMu60tZR0ChaV76Nhnj37DEYTX9R # eNZ8hIOYe4jl7/r419CvEYVIrH6sN00yx49boUuumF9i2T8UuKGn9966fR5X6kgX # j3o5WHhHVO+NBikDO0mlUh902wS/Eeh8F/UFaRp1z5SnROHwSJ+QQRZ1fisD8UTV # DSupWJNstVkiqLq+ISTdEjJKGjVfIcsgA4l9cbk8Smlzddh4EfvFrpVNnes4c16J # idj5XiPVdsn5n10jxmGpxoMc6iPkoaDhi6JjHd5ibfdp5uzIXp4P0wXkgNs+CO/C # acBqU0R4k+8h6gYldp4FCMgrXdKWfM4N0u25OEAuEa3JyidxW48jwBqIJqImd93N # Rxvd1aepSeNeREXAu2xUDEW8aqzFQDYmr9ZONuc2MhTMizchNULpUEoA6Vva7b1X # CB+1rxvbKmLqfY/M/SdV6mwWTyeVy5Z/JkvMFpnQy5wR14GJcv6dQ4aEKOX5AgMB # AAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUB # Af8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s # BwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYEFJ9X # LAN3DigVkGalY17uT5IfdqBbMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwz # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1l # U3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzABhhho # dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9jYWNl # cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZU # aW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAD2tHh92mVvjOIQS # R9lDkfYR25tOCB3RKE/P09x7gUsmXqt40ouRl3lj+8QioVYq3igpwrPvBmZdrlWB # b0HvqT00nFSXgmUrDKNSQqGTdpjHsPy+LaalTW0qVjvUBhcHzBMutB6HzeledbDC # zFzUy34VarPnvIWrqVogK0qM8gJhh/+qDEAIdO/KkYesLyTVOoJ4eTq7gj9UFAL1 # UruJKlTnCVaM2UeUUW/8z3fvjxhN6hdT98Vr2FYlCS7Mbb4Hv5swO+aAXxWUm3Wp # ByXtgVQxiBlTVYzqfLDbe9PpBKDBfk+rabTFDZXoUke7zPgtd7/fvWTlCs30VAGE # sshJmLbJ6ZbQ/xll/HjO9JbNVekBv2Tgem+mLptR7yIrpaidRJXrI+UzB6vAlk/8 # a1u7cIqV0yef4uaZFORNekUgQHTqddmsPCEIYQP7xGxZBIhdmm4bhYsVA6G2WgNF # YagLDBzpmk9104WQzYuVNsxyoVLObhx3RugaEGru+SojW4dHPoWrUhftNpFC5H7Q # EY7MhKRyrBe7ucykW7eaCuWBsBb4HOKRFVDcrZgdwaSIqMDiCLg4D+TPVgKx2EgE # deoHNHT9l3ZDBD+XgbF+23/zBjeCtxz+dL/9NWR6P2eZRi7zcEO1xwcdcqJsyz/J # ceENc2Sg8h3KeFUCS7tpFk7CrDqkMIIHXzCCBUegAwIBAgIQB8JSdCgUotar/iTq # F+XdLjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT # aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTIzMDQxNjAwMDAwMFoX # DTI2MDcwNjIzNTk1OVowZzELMAkGA1UEBhMCUEwxEjAQBgNVBAcMCU1pa2/FgsOz # dzEhMB8GA1UECgwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMSEwHwYDVQQDDBhQ # cnplbXlzxYJhdyBLxYJ5cyBFVk9URUMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw # ggIKAoICAQCUmgeXMQtIaKaSkKvbAt8GFZJ1ywOH8SwxlTus4McyrWmVOrRBVRQA # 8ApF9FaeobwmkZxvkxQTFLHKm+8knwomEUslca8CqSOI0YwELv5EwTVEh0C/Daeh # vxo6tkmNPF9/SP1KC3c0l1vO+M7vdNVGKQIQrhxq7EG0iezBZOAiukNdGVXRYOLn # 47V3qL5PwG/ou2alJ/vifIDad81qFb+QkUh02Jo24SMjWdKDytdrMXi0235CN4Rr # W+8gjfRJ+fKKjgMImbuceCsi9Iv1a66bUc9anAemObT4mF5U/yQBgAuAo3+jVB8w # iUd87kUQO0zJCF8vq2YrVOz8OJmMX8ggIsEEUZ3CZKD0hVc3dm7cWSAw8/FNzGNP # lAaIxzXX9qeD0EgaCLRkItA3t3eQW+IAXyS/9ZnnpFUoDvQGbK+Q4/bP0ib98XLf # QpxVGRu0cCV0Ng77DIkRF+IyR1PcwVAq+OzVU3vKeo25v/rntiXCmCxiW4oHYO28 # eSQ/eIAcnii+3uKDNZrI15P7VxDrkUIc6FtiSvOhwc3AzY+vEfivUkFKRqwvSSr4 # fCrrkk7z2Qe72Zwlw2EDRVHyy0fUVGO9QMuh6E3RwnJL96ip0alcmhKABGoIqSW0 # 5nXdCUbkXmhPCTT5naQDuZ1UkAXbZPShKjbPwzdXP2b8I9nQ89VSgQIDAQABo4IC # AzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYE # FHrxaiVZuDJxxEk15bLoMuFI5233MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK # BggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEz # ODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0Rp # Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j # cmwwPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3 # dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcw # AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8v # Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmlu # Z1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEB # CwUAA4ICAQC3EeHXUPhpe31K2DL43Hfh6qkvBHyR1RlD9lVIklcRCR50ZHzoWs6E # BlTFyohvkpclVCuRdQW33tS6vtKPOucpDDv4wsA+6zkJYI8fHouW6Tqa1W47YSrc # 5AOShIcJ9+NpNbKNGih3doSlcio2mUKCX5I/ZrzJBkQpJ0kYha/pUST2CbE3JroJ # f2vQWGUiI+J3LdiPNHmhO1l+zaQkSxv0cVDETMfQGZKKRVESZ6Fg61b0djvQSx51 # 0MdbxtKMjvS3ZtAytqnQHk1ipP+Rg+M5lFHrSkUlnpGa+f3nuQhxDb7N9E8hUVev # xALTrFifg8zhslVRH5/Df/CxlMKXC7op30/AyQsOQxHW1uNx3tG1DMgizpwBasrx # h6wa7iaA+Lp07q1I92eLhrYbtw3xC2vNIGdMdN7nd76yMIjdYnAn7r38wwtaJ3KY # D0QTl77EB8u/5cCs3ShZdDdyg4K7NoJl8iEHrbqtooAHOMLiJpiL2i9Yn8kQMB6/ # Q6RMO3IUPLuycB9o6DNiwQHf6Jt5oW7P09k5NxxBEmksxwNbmZvNQ65Zn3exUAKq # G+x31Egz5IZ4U/jPzRalElEIpS0rgrVg8R8pEOhd95mEzp5WERKFyXhe6nB6bSYH # v8clLAV0iMku308rpfjMiQkqS3LLzfUJ5OHqtKKQNMLxz9z185UCszGCBlMwggZP # AgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEw # PwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2 # IFNIQTM4NCAyMDIxIENBMQIQB8JSdCgUotar/iTqF+XdLjANBglghkgBZQMEAgEF # AKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgor # BgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3 # DQEJBDEiBCAqSBIzJlo55+qsO5oHOvsaCJGAAwqjW2JtLkvxOszvJzANBgkqhkiG # 9w0BAQEFAASCAgA76s96LwKGmkC31aCf3IzJ7pW4ZCzxxNugNlWZWG3L23u0OOe4 # uWnRE3xTi69yQclcOIobBgLxSeA7fwBanIc+p5vWl+CJSA4pvk4oEyWFv1enWDNy # /nkV65TFTLR+LFWgnrT49fgiGlw3OQlKDNgj5klykH4npyZy7/yZgDOPGa4PhtCI # y0b/8noAiJn821IMX26yq+dfxmqbukVX2lQ3irFiUBrGM6jqSPIl0M3B5kr46JvL # uQgKJw3rH5Spedvq9RoQeUb0EWFMWA3D8ApM0ZuzRbAfspSoln2i9EvNVgMyGuW3 # 7d8VDUGDDDckf3VslmD4kkIZACw3MneX+m/pOut1f1dJ/j0EI94jZcpYisOOPTIR # JtAHKqSOltF7RXfo2pSZCd+EqaonkTpTIP7Fn8L4Ia1uy5+ZE7skaFiX7ohJ3Xdm # 4VIFKYo86GaFDgmMHIe75WdJ2/cUunNWhJNezStLEq+c0+Nu+sw9KY9QiwXnnDuz # DnjXzV3htgzE+mEJPQ3vLelkzl8BMSuL9OwQLW8l3CYQqKN9IMvOk7SWcnoxCpib # TDbRmEMWNjDAdkx050QodGO7YwUc65fmD448umVXU8jGbzzWtir4/2T3FuTLPk2c # zdGcz1zhoQ9HTQLft9XlAXytzEySLMOSoru/R4gJtpGrJo8n8ipkwMjWXqGCAyAw # ggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVTMRcwFQYD # VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH # NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAuuZrxaun+Vh8b56QTj # MwQwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG # CSqGSIb3DQEJBTEPFw0yNTAzMTcwODI5MjhaMC8GCSqGSIb3DQEJBDEiBCCoc2Uq # xIkcZiNu2ldiziKzypOHJptjD9kirGnpJxbxxzANBgkqhkiG9w0BAQEFAASCAgC2 # vfQRfx0E1qcHrhkH3GCKeold1M+INhrDirJgbloMpehFvdY8wKC5Gk0YWom7USTV # vH9uDGg92THKE05vounVpBjX1oNXmlYxxhnK3+qRkToE2lAKuyXj38nkYPbi3ZOv # Xw6nPuQANL5UkvoS1VkUwLJ8XAP53vwK49O9Ts9vNSyfl5mpb1KexU/+g4ic0Ibu # wkKD1l8gdpAITdEPmP4Jvn7tNGpHBbHUSVgewW6CMZ13FLLWfc0CBRJID33dw4tN # bimz/pqRAqGzvG94dVwaofyq4G7giRaI0G+DvRgB2nrHr+s7T3jRNMePe4dvrU4N # FPoCQu/N1kv08j36dTITsNT39YHAqU1lrpKp80OtcUwyZTTOKY8n7mOQoa2A7ZQe # zqvp8I/dnLj6Kyw/x9IxKfVroMzoha4POsRuxzpJF9K+bDCP7E2QJcLDo6pJaBv1 # RBOrvngUmdq04D93KRmF9S2dM0ucHStZ5XbR0t9CmkFzFwAAd3AKSJZEZW5CUT8l # vVac7FzAl9e6lr5jrmKbswKYYPdguIyaX2cDr8tfHzAvEtWUnia8tZ480Gr0YoEi # daiGDnoHfNhvobLhtWe5LtVHv/AA0GJWod8ImsJo+2doQvZNOfsxnFwgNW45Eywh # lVJ0acWxpaKBoA8dX+nkpOjZODz3huwlA6JrtT/Rkg== # SIG # End signature block |