SharePointEssentials.psm1

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.
 
    .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
    )

    $Script:PSDefaultParameterValues = @{
        "Write-Color:LogFile"    = $LogPath
        "Write-Color:ShowTime"   = if ($PSBoundParameters.ContainsKey('ShowTime')) {
            $ShowTime.IsPresent 
        }
        else {
            $null 
        }
        "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 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 
            } 
        } # Add empty line before
        if ($StartTab -ne 0) {
            for ($i = 0; $i -lt $StartTab; $i++) {
                Write-Host -Object "`t" -NoNewline 
            } 
        }  # Add TABS before text
        if ($StartSpaces -ne 0) {
            for ($i = 0; $i -lt $StartSpaces; $i++) {
                Write-Host -Object ' ' -NoNewline 
            } 
        }  # Add SPACES before text
        if ($ShowTime) {
            Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline 
        } # Add Time before output
        if ($Text.Count -ne 0) {
            if ($Color.Count -ge $Text.Count) {
                # the real deal coloring
                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 
        } # Support for no new line
        if ($LinesAfter -ne 0) {
            for ($i = 0; $i -lt $LinesAfter; $i++) {
                Write-Host -Object "`n" -NoNewline 
            } 
        }  # Add empty line after
    }
    if ($Text.Count -and $LogFile) {
        # Save to file
        $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-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
                    }
                }
                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
        }
    }
}
Function Export-FilesToSharePoint {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory)][Array] $Source,
        [Parameter(Mandatory)][string] $SourceFolderPath,
        [Parameter(Mandatory)][string] $TargetLibraryName,
        [Parameter(Mandatory)][Microsoft.SharePoint.Client.ClientObject] $TargetFolder
    )
    # Get all files from SharePoint Online
    $TargetFilesCount = 0
    $TargetDirectoryCount = 0
    $TargetFiles = Get-PnPListItem -List $TargetLibraryName -PageSize 2000
    $Target = foreach ($File in $TargetFiles) {
        # Dates are not the same as in SharePoint, so we need to convert them to UTC
        # And make sure we don't add miliseconds
        $Date = $File.FieldValues.Modified.ToUniversalTime()
        if ($File.FieldValues.FileRef -like "$($TargetFolder.ServerRelativeUrl)/*") {
            [PSCustomObject] @{
                FullName      = $File.FieldValues.FileRef.Replace($TargetFolder.ServerRelativeURL, $SourceFolderPath).Replace("/", "\")
                PSIsContainer = $File.FileSystemObjectType -eq "Folder"
                TargetItemURL = $File.FieldValues.FileRef.Replace($Web.ServerRelativeUrl, [string]::Empty)
                LastUpdated   = [datetime]::new($Date.Year, $Date.Month, $Date.Day, $Date.Hour, $Date.Minute, $Date.Second)
            }
            if (-not $File.FileSystemObjectType -eq "Folder") {
                $TargetFilesCount++
            }
            else {
                $TargetDirectoryCount++
            }
            #Write-Color -Text "[i] ", "File ", "'$($File.FieldValues.FileRef)'", " is in the target folder." -Color Yellow, White, Yellow
        }
        else {
            #Write-Color -Text "[!] ", "File ", "'$($File.FieldValues.FileRef)'", " is not in the target folder. Skipping." -Color Yellow, White, Yellow, Red
        }
    }

    Write-Color -Text "[i] ", "Total items (files) in target: ", "$($TargetFilesCount)" -Color Yellow, White, Green
    Write-Color -Text "[i] ", "Total items (folders) in target: ", "$($TargetDirectoryCount)" -Color Yellow, White, Green

    # Compare source/target and add files that are not in the target
    $CacheFilesTarget = [ordered] @{}
    $ActionsToDo = [ordered] @{
        "Add"     = [System.Collections.Generic.List[Object]]::new()
        "Nothing" = [System.Collections.Generic.List[Object]]::new()
        "Update"  = [System.Collections.Generic.List[Object]]::new()
        "Remove"  = [System.Collections.Generic.List[Object]]::new()
    }
    foreach ($File in $Target) {
        $CacheFilesTarget[$File.FullName] = $File
    }

    foreach ($File in $Source) {
        if ($CacheFilesTarget[$File.FullName]) {
            if (-not $File.PSIsContainer) {
                $TargetFile = $CacheFilesTarget[$File.FullName]
                if ($File.PSIsContainer -eq $TargetFile.PSiSContainer -and $File.TargetItemURL -eq $TargetFile.TargetItemURL -and $File.LastUpdated -eq $TargetFile.LastUpdated) {
                    $ActionsToDo["Nothing"].Add($File)
                }
                elseif ($File.PSIsContainer -eq $TargetFile.PSiSContainer -and $File.TargetItemURL -eq $TargetFile.TargetItemURL -and $File.LastUpdated -ne $TargetFile.LastUpdated) {
                    #Write-Color -Text "[>] Update ", $($File.FullName), " is required. Dates are different: ", "$($File.LastUpdated)", " vs ", "$($TargetFile.LastUpdated)" -Color Yellow, White, Yellow, White, Yellow, Red
                    $ActionsToDo["Update"].Add($File)
                }
                elseif ($File.PSIsContainer -ne $TargetFile.PSiSContainer -or $File.TargetItemURL -ne $TargetFile.TargetItemURL) {
                    # not really needed here
                    Write-Color -Text "This should never happen 1" -Color Red
                }
                else {
                    # this should never happen right?
                    Write-Color -Text "This should never happen 2" -Color Red
                }
            }
        }
        else {
            $ActionsToDo["Add"].Add($File)
        }
    }

    Write-Color -Text "[i] ", "Total items to update: ", "$($ActionsToDo['Update'].Count)" -Color Yellow, White, Green
    Write-Color -Text "[i] ", "Total items to add: ", "$($ActionsToDo['Add'].Count)" -Color Yellow, White, Green
    Write-Color -Text "[i] ", "Total items matching: ", "$($ActionsToDo['Nothing'].Count)" -Color Yellow, White, Green

    $Counter = 1
    foreach ($SourceFile in $ActionsToDo["Add"] | Sort-Object TargetItemURL) {
        # Calculate Target Folder URL for the file
        $TargetFolderURL = (Split-Path $SourceFile.TargetItemURL -Parent).Replace("\", "/")
        If ($TargetFolderURL.StartsWith("/")) {
            $TargetFolderURL = $TargetFolderURL.Remove(0, 1) 
        }
        $ItemName = Split-Path $SourceFile.FullName -Leaf
        # Replace Invalid Characters
        $ItemName = [RegEx]::Replace($ItemName, "[{0}]" -f ([RegEx]::Escape([String]'\*:<>?/\|')), '_')

        If ($SourceFile.PSIsContainer) {
        }
        else {
            If ($PSCmdlet.ShouldProcess($TargetFolderURL, "Adding new file '$($SourceFile.FullName)' to SharePoint folder")) {
                Write-Color -Text "[+] ", "Adding new file ", "($($Counter) of $($ActionsToDo["Add"].Count)) ", "'$($SourceFile.FullName)'", " to Folder ", "'$TargetFolderURL'" -Color Yellow, White, Yellow, White, Yellow, Cyan
                try {
                    $null = Add-PnPFile -Path $SourceFile.FullName -Folder $TargetFolderURL -Values @{"Modified" = $SourceFile.LastUpdated.ToLocalTime() } -ErrorAction Stop
                }
                catch {
                    Write-Color -Text "[!] ", "Error adding file ", "'$($SourceFile.FullName)'", " to Folder ", "'$TargetFolderURL'", ". Error: ", $_.Exception.Message -Color Yellow, White, Yellow, White, Yellow, Red
                }
            }
        }
        $Counter++
    }

    $Counter = 1
    foreach ($SourceFile in $ActionsToDo["Update"] | Sort-Object TargetItemURL) {
        # Calculate Target Folder URL for the file
        $TargetFolderURL = (Split-Path $SourceFile.TargetItemURL -Parent).Replace("\", "/")
        If ($TargetFolderURL.StartsWith("/")) {
            $TargetFolderURL = $TargetFolderURL.Remove(0, 1) 
        }
        $ItemName = Split-Path $SourceFile.FullName -Leaf
        # Replace Invalid Characters
        $ItemName = [RegEx]::Replace($ItemName, "[{0}]" -f ([RegEx]::Escape([String]'\*:<>?/\|')), '_')

        If ($SourceFile.PSIsContainer) {
        }
        else {
            If ($PSCmdlet.ShouldProcess($TargetFolderURL, "Updating file '$($SourceFile.FullName)' to SharePoint folder")) {
                Write-Color -Text "[+] ", "Updating file ", "($($Counter) of $($ActionsToDo["Update"].Count)) ", "'$($SourceFile.FullName)'", " to Folder ", "'$TargetFolderURL'" -Color Yellow, White, Yellow, White, Yellow, Cyan
                try {
                    $null = Add-PnPFile -Path $SourceFile.FullName -Folder $TargetFolderURL -Values @{"Modified" = $SourceFile.LastUpdated.ToLocalTime() } -ErrorAction Stop
                }
                catch {
                    Write-Color -Text "[!] ", "Error updating file ", "'$($SourceFile.FullName)'", " to Folder ", "'$TargetFolderURL'", ". Error: ", $_.Exception.Message -Color Yellow, White, Yellow, White, Yellow, Red
                }
            }
        }
        $Counter++
    }
}
function Find-TargetFolder {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $TargetLibraryName,
        [Microsoft.SharePoint.Client.List] $Library,
        [Microsoft.SharePoint.Client.Web] $Web
    )
    $TargetFolder = $null
    if ($TargetLibraryName -like "*/*") {
        $Path = $TargetLibraryName -split "/"
        $TargetLibraryPath = $Path[0]
        $ListOfSharePointFolders = Get-PnPFolderItem -ItemType Folder -Recursive -FolderSiteRelativeUrl $TargetLibraryPath #| Select-Object Name, ServerRelativeUrl, ItemCount
        foreach ($Folder in $ListOfSharePointFolders) {
            $FolderFound = $Folder.ServerRelativeUrl.Replace($Web.ServerRelativeURL, [string]::Empty)
            if ($FolderFound.TrimStart("/") -eq $TargetLibraryName.TrimStart("/")) {
                $TargetFolder = $Folder
                break
            }
        }
    }
    else {
        $TargetFolder = $Library.RootFolder
    }

    if (-not $TargetFolder) {
        Write-Color -Text "[e] ", "Unable to find folder ", $TargetLibraryName, " in the library. Please make sure the folder exists." -Color Yellow, Red
        return
    }
    $TargetFolder
}
function Get-FilesLocal {
    [CmdletBinding()]
    param(
        [Array] $SourceFileList,
        [string] $SourceFolderPath,
        [string] $Include,
        [string] $TargetFolderSiteRelativeURL
    )
    if ($SourceFileList) {
        $SourceDirectoryPath = $null
        # Lets get all files from the source folder
        [Array] $SourceItems = foreach ($Item in $SourceFileList) {
            # We need to find the shortest path to the files
            $TempSourceDirectoryPath = [io.path]::GetDirectoryName($Item)
            if ($null -eq $SourceDirectoryPath) {
                $SourceDirectoryPath = $TempSourceDirectoryPath
            }
            elseif ($SourceDirectoryPath -ne $TempSourceDirectoryPath) {
                if ($TempSourceDirectoryPath.Length -lt $SourceDirectoryPath.Length) {
                    $SourceDirectoryPath = $TempSourceDirectoryPath
                }
            }
            try {
                Get-Item -Path $Item -ErrorAction Stop
                Get-Item -Path $TempSourceDirectoryPath -ErrorAction Stop
            }
            catch {
                Write-Color -Text "[e] ", "Unable to get file '$Item' from the source file list. Make sure the path is correct and you have permissions to access it." -Color Yellow, Red
                Write-Color -Text "[e] ", "Error: ", $_.Exception.Message -Color Yellow, Red
                return
            }
        }
        if ($SourceItems.Count -eq 0) {
            Write-Color -Text "[e] ", "No files found in the source file list. Please make sure the list is not empty." -Color Yellow, Red
            return
        }
    }
    else {
        $SourceDirectoryPath = $SourceFolderPath
        # Lets get all files from the source folder
        $getChildItemSplat = @{
            Path    = $SourceFolderPath
            Recurse = $true
        }
        if ($Include) {
            $getChildItemSplat["Include"] = $Include
        }
        try {
            $SourceItems = @(
                Get-ChildItem -Directory -Path $SourceFolderPath -Recurse -ErrorAction Stop
                Get-ChildItem @getChildItemSplat -ErrorAction Stop
            )
        }
        catch {
            Write-Color -Text "[e] ", "Unable to get files from the source folder. Make sure the path is correct and you have permissions to access it." -Color Yellow, Red
            Write-Color -Text "[e] ", "Error: ", $_.Exception.Message -Color Yellow, Red
            return
        }
    }
    $SourceFilesCount = 0
    $SourceDirectoryCount = 0
    [Array] $Source = foreach ($File in $SourceItems | Sort-Object -Unique -Property FullName) {
        # Dates are not the same as in SharePoint, so we need to convert them to UTC
        # And make sure we don't add miliseconds, as it will cause issues with comparison
        $Date = $File.LastWriteTimeUtc
        [PSCustomObject] @{
            FullName      = $File.FullName
            PSIsContainer = $File.PSIsContainer
            TargetItemURL = $File.FullName.Replace($SourceDirectoryPath, $TargetFolderSiteRelativeURL).Replace("\", "/")
            LastUpdated   = [datetime]::new($Date.Year, $Date.Month, $Date.Day, $Date.Hour, $Date.Minute, $Date.Second)
        }
        if (-not $File.PSIsContainer) {
            $SourceFilesCount++
        }
        else {
            $SourceDirectoryCount++
        }
    }
    [ordered] @{
        Source               = $Source
        SourceFilesCount     = $SourceFilesCount
        SourceDirectoryCount = $SourceDirectoryCount
        SourceDirectoryPath  = $SourceDirectoryPath
    }
}
Function Remove-FilesFromSharePoint {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Array] $Source,
        [Parameter(Mandatory)] [string] $SiteURL,
        [Parameter(Mandatory)] [string] $SourceFolderPath,
        [Parameter(Mandatory)] [string] $TargetLibraryName,
        $TargetFolder,
        [string[]] $ExcludeFromRemoval
    )
    # Get all files on SharePoint Online
    $TargetFiles = Get-PnPListItem -List $TargetLibraryName -PageSize 2000

    [Array] $Target = foreach ($File in $TargetFiles) {
        $Date = $File.FieldValues.Modified.ToUniversalTime()
        if ($File.FieldValues.FileRef -like "$($TargetFolder.ServerRelativeUrl)/*") {
            [PSCustomObject] @{
                FullName      = $File.FieldValues.FileRef.Replace($TargetFolder.ServerRelativeURL, $SourceFolderPath).Replace("/", "\")
                PSIsContainer = $File.FileSystemObjectType -eq "Folder"
                TargetItemURL = $File.FieldValues.FileRef.Replace($Web.ServerRelativeUrl, [string]::Empty)
                LastUpdated   = [datetime]::new($Date.Year, $Date.Month, $Date.Day, $Date.Hour, $Date.Minute, $Date.Second)
            }
            #Write-Color -Text "[i] ", "File ", "'$($File.FieldValues.FileRef)'", " is in the target folder." -Color Yellow, White, Yellow
        }
        else {
            #Write-Color -Text "[!] ", "File ", "'$($File.FieldValues.FileRef)'", " is not in the target folder. Skipping." -Color Yellow, White, Yellow, Red
        }
    }

    # Compare source/target and remove files that are not in the source
    # Ignore LastUpdated as it doesn't matter - the file either exists or it doesn't
    if ($Target.Count -eq 0) {
        Write-Color -Text "[i] ", "No items found in the Target. Skipping removal." -Color Yellow, White, Green
        return
    }

    if ($Source.Count -gt 0) {
        $FilesDiff = Compare-Object -ReferenceObject $Source -DifferenceObject $Target -Property FullName, PSIsContainer, TargetItemURL #, LastUpdated
        [Array] $TargetDelta = foreach ($File in $FilesDiff) {
            If ($File.SideIndicator -eq "=>") {
                $File
            }
        }
    }
    else {
        $TargetDelta = $Target
    }

    If ($TargetDelta.Count -gt 0) {
        Write-Color -Text "[information] ", "Found ", "$($TargetDelta.Count)", " differences in the Target. Removal is required." -Color Yellow, White, Yellow, White, Yellow, Red

        $Counter = 1
        :topLoop foreach ($TargetFile in $TargetDelta | Sort-Object TargetItemURL -Descending) {
            If ($TargetFile.PSIsContainer) {
                $Folder = Get-PnPFolder -Url $TargetFile.TargetItemURL -ErrorAction SilentlyContinue
                If ($Null -ne $Folder -and $Folder.Items.Count -eq 0) {
                    if ($ExcludeFromRemoval) {
                        foreach ($Exclude in $ExcludeFromRemoval) {
                            If ($TargetFile.TargetItemURL -like $Exclude) {
                                Write-Color -Text "[!] ", "Folder ", "'$($TargetFile.TargetItemURL)'", " is excluded from removal." -Color Yellow, White, Yellow, Red
                                Continue topLoop
                            }
                        }
                    }
                    If ($PSCmdlet.ShouldProcess($TargetFile.TargetItemURL, "Removing folder from SharePoint")) {
                        Write-Color -Text "[-] ", "Removing Item ", "($($Counter) of $($TargetDelta.Count)) ", "'$($TargetFile.TargetItemURL)'" -Color Red, White, Yellow, Red
                        try {
                            $null = $Folder.Recycle()
                            Invoke-PnPQuery
                        }
                        catch {
                            Write-Color -Text "[!] ", "Failed to remove folder ", "'$($TargetFile.TargetItemURL)'", ". Error: ", $_.Exception.Message -Color Yellow, White, Yellow, Red
                        }
                    }
                }
                else {
                    Write-Color -Text "[!] ", "Folder ", "'$($TargetFile.TargetItemURL)'", " is not empty. Skipping." -Color Yellow, White, Yellow, Red
                }
            }
            else {
                $File = Get-PnPFile -Url $TargetFile.TargetItemURL -ErrorAction SilentlyContinue
                If ($Null -ne $File) {
                    if ($ExcludeFromRemoval) {
                        foreach ($Exclude in $ExcludeFromRemoval) {
                            If ($TargetFile.TargetItemURL -like $Exclude) {
                                Write-Color -Text "[!] ", "File ", "'$($TargetFile.TargetItemURL)'", " is excluded from removal." -Color Yellow, White, Yellow, Red
                                Continue topLoop
                            }
                        }
                    }
                    If ($PSCmdlet.ShouldProcess($TargetFile.TargetItemURL, "Removing file from SharePoint")) {
                        Write-Color -Text "[-] ", "Removing Item ", "($($Counter) of $($TargetDelta.Count)) ", "'$($TargetFile.TargetItemURL)'" -Color Red, White, Yellow, Red
                        try {
                            Remove-PnPFile -SiteRelativeUrl $TargetFile.TargetItemURL -Force -ErrorAction Stop
                        }
                        catch {
                            Write-Color -Text "[!] ", "Failed to remove file ", "'$($TargetFile.TargetItemURL)'", ". Error: ", $_.Exception.Message -Color Yellow, White, Yellow, Red
                        }
                    }
                }
            }
            $Counter++
        }
    }
}
Function Sync-FilesToSharePoint {
    <#
    .SYNOPSIS
    Synchronizes files from local folder to SharePoint Online library
 
    .DESCRIPTION
    Synchronizes files from local folder to SharePoint Online library
    Provides an easy way to keep local folder in sync with SharePoint Online library
    - Deleting content on local folder will delete it on SharePoint Online
    - Adding content to local folder will add it to SharePoint Online
    - Updating content on local folder will update it on SharePoint Online
    - Deleting content on SharePoint Online will trigger reupload from local folder
 
    .PARAMETER SiteURL
    Site URL where the library is located
 
    .PARAMETER SourceFolderPath
    Local folder path to synchronize
 
    .PARAMETER SourceFileList
    List of files to synchronize. If this is used, then SourceFolderPath is ignored
    This is useful when you want to synchronize specific files only
 
    .PARAMETER TargetLibraryName
    Name of the library to synchronize to without site url
 
    .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 Include
    Include filter for files. Default "*.*"
 
    .PARAMETER ExcludeFromRemoval
    List of files/folders to exclude from removal. Default $null
 
    .PARAMETER SkipRemoval
    Skip removal of files/folders from SharePoint Online. Default $false
 
    .EXAMPLE
    $Url = 'https://yoursharepoint.sharepoint.com/sites/TheDashboard'
    $ClientID = '438511c4' # Temp SharePoint App
    $TenantID = 'ceb371f6'
 
    Connect-PnPOnline -Url $Url -ClientId $ClientID -Thumbprint '2EC7C86E1AF0E434E93DE3EAC' -Tenant $TenantID
 
    $syncFiles = @{
        SiteURL = 'https://yoursharepoint.sharepoint.com/sites/TheDashboard'
        SourceFolderPath = "C:\Support\GitHub\TheDashboard\Examples\Reports"
        TargetLibraryName = "Shared Documents"
        LogPath = "$PSScriptRoot\Logs\Sync-FilesToSharePoint-$($(Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log"
        LogMaximum = 5
        Include = "*.aspx"
    }
 
    Sync-FilesToSharePoint @syncFiles -WhatIf
 
    .NOTES
    General notes
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'SourceFolder')]
    param (
        [Parameter(Mandatory)][string] $SiteURL,
        [Parameter(Mandatory, ParameterSetName = 'SourceFolder')][string] $SourceFolderPath,
        [Parameter(Mandatory, ParameterSetName = 'SourceFileList')][Array] $SourceFileList,
        [Parameter(Mandatory)][string] $TargetLibraryName,
        [string] $LogPath,
        [int] $LogMaximum,
        [switch] $LogShowTime,
        [string] $LogTimeFormat,
        [string] $Include,
        [string[]] $ExcludeFromRemoval,
        [switch] $SkipRemoval
    )

    Set-LoggingCapabilities -LogPath $LogPath -LogMaximum $LogMaximum -ShowTime:$LogShowTime -TimeFormat $LogTimeFormat -ScriptPath $MyInvocation.ScriptName

    Write-Color -Text "[i] ", "Starting synchronization of files from ", $SourceFolderPath, " to ", $SiteUrl -Color Yellow, White, Yellow, White, Green
    Write-Color -Text "[i] ", "Target library: ", $TargetLibraryName -Color Yellow, White, Green

    # Connect to SharePoint Online
    try {
        $Web = Get-PnPWeb -ErrorAction Stop
    }
    catch {
        Write-Color -Text "[e] ", "Unable to connect to SharePoint Online. Please make sure you are connected to the Internet and that you have permissions to the site." -Color Yellow, Red
        Write-Color -Text "[e] ", "Error: ", $_.Exception.Message -Color Yellow, Red
        return
    }
    try {
        $Library = Get-PnPList -Identity $TargetLibraryName -Includes RootFolder -ErrorAction Stop
    }
    catch {
        Write-Color -Text "[e] ", "Unable to get list of libraries on SharePoint Online. Make sure that you have permissions to the site." -Color Yellow, Red
        Write-Color -Text "[e] ", "Error: ", $_.Exception.Message -Color Yellow, Red
        return
    }

    $TargetFolder = Find-TargetFolder -TargetLibraryName $TargetLibraryName -Library $Library -Web $Web
    if (-not $TargetFolder) {
        return
    }

    # Get the site relative path of the target folder
    If ($web.ServerRelativeURL -eq "/") {
        $TargetFolderSiteRelativeURL = $TargetFolder.ServerRelativeUrl
    }
    Else {
        $TargetFolderSiteRelativeURL = $TargetFolder.ServerRelativeURL.Replace($Web.ServerRelativeUrl, [string]::Empty)
    }

    $FilesLocalOutput = Get-FilesLocal -SourceFileList $SourceFileList -SourceFolderPath $SourceFolderPath -Include $Include -TargetFolderSiteRelativeURL $TargetFolderSiteRelativeURL
    [Array] $Source = $FilesLocalOutput.Source
    [string] $SourceDirectoryPath = $FilesLocalOutput.SourceDirectoryPath

    Write-Color -Text "[i] ", "Total items (files) in source: ", "$($FilesLocalOutput.SourceFilesCount)" -Color Yellow, White, Green
    Write-Color -Text "[i] ", "Total items (folders) in source: ", "$($FilesLocalOutput.SourceDirectoryCount)" -Color Yellow, White, Green
    #Write-Color -Text "[i] ", "Total items in target: ", "$($TargetFolder.Itemcount)" -Color Yellow, White, Green

    Write-Color -Text "[i] ", "Starting processing files/folders to SharePoint ", $SiteUrl -Color Yellow, White, Green

    # Upload files to SharePoint
    $exportFilesToSharePointSplat = @{
        Source            = $Source
        SourceFolderPath  = $SourceDirectoryPath
        TargetLibraryName = $TargetLibraryName
        TargetFolder      = $TargetFolder
        WhatIf            = $WhatIfPreference
    }
    Export-FilesToSharePoint @exportFilesToSharePointSplat

    if (-not $SkipRemoval) {
        Write-Color -Text "[i] ", "Starting removal of files/folders from SharePoint ", $SiteUrl -Color Yellow, White, Green

        # Remove files from SharePoint that are no longer in the source folder
        $removeFileShareDeltaInSPOSplat = @{
            Source             = $Source
            SiteURL            = $SiteURL
            SourceFolderPath   = $SourceDirectoryPath
            TargetLibraryName  = $TargetLibraryName
            TargetFolder       = $TargetFolder
            WhatIf             = $WhatIfPreference
            ExcludeFromRemoval = $ExcludeFromRemoval
        }

        Remove-FilesFromSharePoint @removeFileShareDeltaInSPOSplat
    }
    else {
        Write-Color -Text "[i] ", "Skipping removal of files/folders from SharePoint as requested", $SiteUrl -Color Yellow, White, Green
    }
    Write-Color -Text "[i] ", "Finished synchronization of files from ", $SourceFolderPath, " to ", $SiteUrl -Color Yellow, White, Yellow, White, Green
}


# Export functions and aliases as required
Export-ModuleMember -Function @('Sync-FilesToSharePoint') -Alias @()
# SIG # Begin signature block
# MIItqwYJKoZIhvcNAQcCoIItnDCCLZgCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAZNUHIWNqeumFM
# YPyomvqAkM+gtXOKKwbAFkR4XGb5OKCCJq4wggWNMIIEdaADAgECAhAOmxiO+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
# DQEJBDEiBCA/3sy6v6W7Ap1M/ZWxy5W+Dv8SYJMQnmWsFVjgBkDjIDANBgkqhkiG
# 9w0BAQEFAASCAgAzEuUrUa+Lz7HwzvGD2b7s4JMiXa99DRsran+PTDtXV0SrafFj
# erLD8XXe3Df316StRwjJQooErizgVTgTEtYIslIIsdYijW9ZPzX3AqgBqQ4oiPVy
# IcGOvIZ45+aFetgn6F8+ifmx4zBGpxTrbr6L+dneWZpjaj6kKoX0cefzQyVqnLLz
# s+sU3ZKHtaKvbnOeQQbs/iXsK10Z2crBbpkDGtuIpcpI4MFjOclOui4h+PCdO/WA
# zNMpZaCdb+O8bXwQPvjHqQEzaH4NARpi8+L5T/zgiUeBdv7oC2tUMYnkoNLvoIxG
# cgPCOiBi8cs+ZePMTTBau08ydlHL43fC/WMEAgaT+Mi7kYBBT0p3LIVdUxzXYcs9
# rsGAbGZvdxnHTmt+iyWVq8n/Rdsggv5D/dEktNjIa9CByYXGrZZc534Z6nUKlsbD
# T9L90Acll01S4q6LqTmo7hWI3wmXta0KxBhbHJf7c8rElUDiOz/jPwY3OvNZHPgI
# i80dUaIsT0tfxZFtDfURjD3j85qU00WnjHnRNcUWgzdAPzgmvmX8WOkuBrbpomFL
# SpG6TVcSveXpTaoTqKbl6gX2EYZXmC2Kjp7eW3DCdV+9vnzYLsGYw2ZWa+6IcqVe
# mTV2yjNUV2S+WvlxeJnekrn867f5K0jL5saHRjk0ub6QgCDwhTM8TS8vdKGCAyAw
# ggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVTMRcwFQYD
# VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH
# NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAuuZrxaun+Vh8b56QTj
# MwQwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG
# CSqGSIb3DQEJBTEPFw0yNTAxMjIxNjA3MzVaMC8GCSqGSIb3DQEJBDEiBCDvFMoR
# dT6JwEptkRNtCfMe/dQFTNjwEfiwzjJJytWrgzANBgkqhkiG9w0BAQEFAASCAgC5
# RFh562GePnc238DultY4nEPH3Lxni46UJoReCnQcRAOxSC5wpUtzhxWciqvV8vWy
# j/+Ar7Wy5w41dJ7rk7pP75KsvneNAI7rlwFXEzNVH0SD/4aoMfhjCisi50KAtKVy
# WpyvoJzZoVBPpil9tE7VB31LpI2CNAIk0MgwRbQxr6RvV1SrgyNgXCQqZyIzcbeU
# Byp2wB/7dC5Cse7lbhOvRyq5NOYsG+BG68BMcQW3f4t+s7NSJP4vTbX4VFECIOZI
# rhbBnAtyuJsPLY2o2UsVz2b7Zpg9DVECFumtT0ll+AwoyU9eVHyA2CyXxvyIOKaT
# 8dlaqCTZegOkWJoZyMOlUJ+2EznU1ppys+ULMM88sQMx/xc7AfY1o4gj03yt7CB4
# AOntQmeVSuMmzXbYLiBdHByzkdyxzBb1sD4HOFyWB8GsnnUTkOsg71Ge4DNrDkoM
# H59m4uN+fnfwabBrdctFYaZKdD+l3eiW9xbOutAasZn+iBqHjU1kcA+B4suC/pMQ
# NBZYv2qFSDu6wC2sw8PX89m85zE8PBaBKow56KONwWWRjqyMwnWtGzWe9SEMyw/R
# U65KWQY/C7ykgPg+5Ne+MKht0Ts7eQjWOi1DDR29hlfTvt5LOnYqSAfcY5YriAOA
# 7fw/qxwgHJvMHauGJkabdtE4gxK/msv1pSQVlRn7Sw==
# SIG # End signature block