allcommands.ps1

<#
    .SYNOPSIS
 
    Add's ACLs to a folder.
 
    .DESCRIPTION
 
    Adds the acl to the specified folder - will also attempt to repair cannonical ordering issue
 
    .PARAMETER FolderPath
 
    Folder to set ACLs on.
 
    .PARAMETER User
 
    User to grant permission to.
 
    .PARAMETER Permission
 
    Permission to add
 
    .EXAMPLE
 
    Add-AclFolder -Folder ".\test" -User "hqcatalyst\dev.test" -Permission "Read"
#>

function Add-AclFolder {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateScript({
            if (!(Test-Path $_ -PathType Container)) {
                Write-DosMessage -Level "Fatal" -Message "FolderPath either $_ does not exist or is not a folder. Please enter valid folder path."
            }
            else {
                $true
            }
        })]
        [string] $FolderPath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $User,
        [Parameter(Mandatory=$true)]
        [ValidateSet("Read", "Write")]
        [string] $Permission
    )

    Write-DosMessage -Level "Verbose" -Message "Adding ACL to give $Permission to $User on folder $FolderPath"
    $directoryAcl = (Get-Item $FolderPath).GetAccessControl("Access")
    $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($User, $Permission, "ContainerInherit,ObjectInherit", "None", "Allow")

    try{
        $directoryAcl.AddAccessRule($accessRule)
    }
    catch [System.InvalidOperationException]{
        Write-DosMessage -Level "Warning" -Message "Error attempting to add ACL to list on $FolderPath, attempting to repair"
        Repair-AclCanonicalOrder -Acl $directoryAcl -Path $FolderPath
        $directoryAcl.AddAccessRule($accessRule)
    }

    try{
        Write-DosMessage -Level "Verbose" -Message "Adding ACL to give $Permission to $User on folder $FolderPath"
        Set-Acl -Path $FolderPath $directoryAcl
    }
    catch [System.InvalidOperationException]{
        Write-DosMessage -Level "Warning" -Message "Error attempting to add ACL to $FolderPath, attempting to repair"
        Repair-AclCanonicalOrder -Acl $directoryAcl -Path $FolderPath
        Set-Acl -Path $FolderPath $directoryAcl
    }

}


function Assert-DependencySemVerRequirementsMet {
    [CmdletBinding()]
    param (
        [string]$ServiceName,
        [array]$DiscoveryServiceEntries,
        [array]$CurrentDependencies
    )

    $allDependenciesMet = $true

    foreach ($dependency in $CurrentDependencies ) {
        $installedDependencyBuildNumber = ($DiscoveryServiceEntries | Where-Object { $_.ServiceName -eq $dependency.serviceName }).BuildNumber

        $s1 = [SemVer]::New($installedDependencyBuildNumber)
        $s2 = [SemVer]::New($dependency.serviceBuildVersion)

        $dependencyMet = Assert-SatisfiesSemVer -SemVer1 $s1 -SemVer2 $s2
        if (!$dependencyMet) {
            Write-DosMessage -Level "Warning" -Message "$($dependency.serviceName) dependency not met. Expected $($dependency.serviceBuildVersion), existing $($installedDependencyBuildNumber)"
            $allDependenciesMet = $false
        }
        else {
            Write-DosMessage -Level "Information" -Message "$($dependency.serviceName) dependency met. Expected $($dependency.serviceBuildVersion), existing $($installedDependencyBuildNumber)"
        }
    }

    return $allDependenciesMet
}



function Get-PossibleConstraints {
    return @('~', '^', '=', 'v', '>', '<', '>=', '<=')
}

function Compare-Numbers {
    [CmdletBinding()]
    param (
        $A,
        $B
    )

    if ([int]$A -eq [int]$B) { return 0 }
    elseif ([int]$A -lt [int]$B) { return -1 }
    else { return 1 }
}

enum VersionIdentifier {
    Major
    Minor
    Patch
}

class SemVer {
    [string]$Constraint
    [string]$Major
    [string]$Minor
    [string]$Patch

    SemVer(
        [string]$Version
    ) {
        $this.Parse($Version)
    }

    [void]Parse([string]$Version) {
        $possibleOperators = Get-PossibleConstraints
        $splitVersion = @()

        if ($Version[0] -in $possibleOperators) {
            $versionSubstring = ""
            if (($Version[0] -eq '<') -or ($Version[0] -eq '>') -and ($Version[1] -eq '=')) {
                $this.Constraint = "$($Version[0])$($Version[1])"
                $versionSubstring = $Version.Substring(2, $Version.length - 2)
            }
            else {
                $this.Constraint = $Version[0]
                $versionSubstring = $Version.Substring(1, $Version.length - 1)
            }

            $splitVersion = $versionSubstring.split('.')
        }
        else {
            $splitVersion = $Version.split('.')
        }
        
        if ($null -ne $splitVersion[0]) {
            $this.Major = "$($splitVersion[0])"
        }
        if ($null -ne $splitVersion[1]) {
            $this.Minor = "$($splitVersion[1])"
        }
        if ($null -ne $splitVersion[2]) {
            $this.Patch = "$($splitVersion[2])"
        }
    }

    [int]Compare([SemVer] $other) {
        if (Compare-Numbers -A $this.Major -B $other.Major) {
            return Compare-Numbers -A $this.Major -B $other.Major
        }
        elseif (Compare-Numbers -A $this.Minor -B $other.Minor) {
            return Compare-Numbers -A $this.Minor -B $other.Minor
        }
        else {
            return Compare-Numbers -A $this.Patch -B $other.Patch
        }
    }

    [string]ToString() {
        return "$($this.Major).$($this.Minor).$($this.Patch)"
    }

    [void]Increment([VersionIdentifier] $Identifier) {
        switch ($Identifier) {
            Major {
                # 1.2.3 -> 2.0.0
                $this.Major = (+$this.Major + 1).ToString()
                $this.Minor = "0"
                $this.Patch = "0"
            }
            Minor {
                # 1.2.3 -> 1.3.0
                $this.Minor = (+$this.Minor + 1).ToString()
                $this.Patch = "0"
            }
            Patch {
                # 1.2.3 -> 1.2.4
                $this.Patch = (+$this.Patch + 1).ToString()
            }
        }
    }
}

function Assert-Equal {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    # $SemVer2
    # 1 >=1.0.0 <2.0.0
    # 1.0 >=1.0.0 <1.1.0.
    # 1.0.0 1.0.0 exact

    $lessThanSemVer = [SemVer]::new($SemVer2.ToString())

    if ($SemVer2.Patch) {
        return $SemVer1.Compare($SemVer2) -eq 0 
    }
    elseif ($SemVer2.Minor) {
        $lessThanSemVer.Increment([VersionIdentifier]::Minor)
    }
    else {
        $lessThanSemVer.Increment([VersionIdentifier]::Major)
    }

    return (Assert-GreaterThanEqual $SemVer1 $SemVer2) -and (Assert-LessThan $SemVer1 $lessThanSemVer)
}

function Assert-LessThan {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    return $SemVer1.Compare($SemVer2) -lt 0
}

function Assert-LessThanEqual {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    $lteSemVer = [SemVer]::new($SemVer2.ToString())

    if ($SemVer2.Patch) {
        return $SemVer1.Compare($SemVer2) -le 0
    }
    elseif ($SemVer2.Minor) {
        $lteSemVer.Increment([VersionIdentifier]::Minor)
    }
    else {
        $lteSemVer.Increment([VersionIdentifier]::Major)
    }

    return $SemVer1.Compare($lteSemVer) -lt 0
}

function Assert-GreaterThan {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    $gtSemVer = [SemVer]::new($SemVer2.ToString())

    if ($SemVer2.Patch) {
        return $SemVer1.Compare($SemVer2) -gt 0
    }
    elseif ($SemVer2.Minor) {
        $gtSemVer.Increment([VersionIdentifier]::Minor)
    }
    else {
        $gtSemVer.Increment([VersionIdentifier]::Major)
    }

    return $SemVer1.Compare($gtSemVer) -ge 0
}

function Assert-GreaterThanEqual {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    return $SemVer1.Compare($SemVer2) -ge 0
}

function Assert-TildeConstraint {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    # $SemVer2
    # ~1: >=1.0.0 <2.0.0.
    # ~1.1: >=1.1.0 <1.2.0.
    # ~1.1.1: >=1.1.1 <1.2.0.

    $lessThanSemVer = [SemVer]::new($SemVer2.ToString())

    if ($SemVer2.Patch -or $SemVer2.Minor) {
        # ~1.1.1: >=1.1.1 <1.2.0.
        # ~1.1: >=1.1.0 <1.2.0.
        $lessThanSemVer.Increment([VersionIdentifier]::Minor)
    }
    else {
        # ~1: >=1.0.0 <2.0.0.
        $lessThanSemVer.Increment([VersionIdentifier]::Major)
    }

    return (Assert-GreaterThanEqual $SemVer1 $SemVer2) -and (Assert-LessThan $SemVer1 $lessThanSemVer)
}

function Assert-CaretConstraint {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    # $SemVer2
    # ^1 >=1.0.0 <2.0.0
    # ^1.1 >=1.1.0 <2.0.0
    # ^1.1.1 >=1.1.1 <2.0.0

    # ^0 >=0.0.0 <1.0.0
    # ^0.0 >=0.0.0 <0.1.0
    # ^0.0.0 >=0.0.0 <0.0.1

    $lessThanSemVer = [SemVer]::new($SemVer2.ToString())

    if (($SemVer2.Patch -eq 0) -and ($SemVer2.Minor -eq 0) -and ($SemVer2.Major -eq 0)) {
        # ^0.0.0 >=0.0.0 <0.0.1
        $lessThanSemVer.Increment([VersionIdentifier]::Patch)
    }
    elseif (($SemVer2.Minor -eq 0) -and ($SemVer2.Major -eq 0)) {
        # ^0.0 >=0.0.0 <0.1.0
        $lessThanSemVer.Increment([VersionIdentifier]::Minor)
    }
    else {
        # ^1 >=1.0.0 <2.0.0
        # ^0 >=0.0.0 <1.0.0
        $lessThanSemVer.Increment([VersionIdentifier]::Major)
    }

    return (Assert-GreaterThanEqual $SemVer1 $SemVer2) -and (Assert-LessThan $SemVer1 $lessThanSemVer)
}

function Assert-XRangeConstraint {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    # $SemVer2
    # * any
    # 1.* >=1.0.0 <2.0.0
    # 1.1.* >=1.1.0 <1.2.0

    $lessThanSemVer = [SemVer]::new($SemVer2.ToString())
    $semVer2ReplaceStar = [SemVer]::new($SemVer2.ToString())

    if ($SemVer2.Patch -eq '*') {
        # 1.1.* >=1.1.0 <1.2.0
        $lessThanSemVer.Increment([VersionIdentifier]::Minor)
        $semVer2ReplaceStar.Patch = "0"
    }
    elseif ($SemVer2.Minor -eq '*') {
        # 1.* >=1.0.0 <2.0.0
        $lessThanSemVer.Increment([VersionIdentifier]::Major)
        $semVer2ReplaceStar.Patch = "0"
        $semVer2ReplaceStar.Minor = "0"
    }
    else {
        # *
        return $true
    }

    return (Assert-GreaterThanEqual $SemVer1 $semVer2ReplaceStar) -and (Assert-LessThan $SemVer1 $lessThanSemVer)
}

function Assert-SatisfiesSemVer {
    [CmdletBinding()]
    param (
        [SemVer]$SemVer1,
        [SemVer]$SemVer2
    )

    if ($SemVer2.Constraint) {
        Switch ($SemVer2.Constraint) {
            '=' {
                return Assert-Equal -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
            'v' {
                return Assert-Equal -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
            '<' {
                return Assert-LessThan -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
            '<=' {
                return Assert-LessThanEqual -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
            '>' {
                return Assert-GreaterThan -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
            '>=' {
                return Assert-GreaterThanEqual -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
            '~' {
                return Assert-TildeConstraint -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
            '^' {
                return Assert-CaretConstraint -SemVer1 $SemVer1 -SemVer2 $SemVer2
            }
        }
    }

    if (($SemVer2.Major -eq '*') -or ($SemVer2.Minor -eq '*') -or ($SemVer2.Patch -eq '*')) {
        return Assert-XRangeConstraint -SemVer1 $SemVer1 -SemVer2 $SemVer2
    }

    return Assert-Equal -SemVer1 $SemVer1 -SemVer2 $SemVer2
}


<#
    .SYNOPSIS
 
    Validates ConfigStore object to be sure configuration values will be returned accuratelly and appropriately.
 
    .DESCRIPTION
 
    Checks if config store object properties are provided with valid values.
 
    .PARAMETER ConfigStore
 
    Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}.
    Current iteration only supports XML Files.
 
    .EXAMPLE
 
    Confirm-ConfigStore -ConfigStore $configHashtable
#>


function Confirm-ConfigStore {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $ConfigStore
    )

    Write-DosMessage -Level "Debug" -Message "Validating Config Store object."

    if ([string]::IsNullOrEmpty($ConfigStore.Type)){
        Write-DosMessage -Level "Warning" -Message "ConfigStore 'Type' attribute is missing. Please provide a valid value."
        return $false
    }

    if ($ConfigStore.Type -eq "File" -and [string]::IsNullOrEmpty($ConfigStore.Path)){
        Write-DosMessage -Level "Warning" -Message "ConfigStore 'Path' attribute cannot be empty when ConfigStore type is 'File'. Please provide an appropriate path."
        return $false
    }

    if ([string]::IsNullOrEmpty($ConfigStore.Format)){
        Write-DosMessage -Level "Warning" -Message "ConfigStore 'Format' attribute is missing. Please provide a valid value."
        return $false
    }

    $validConfigFormats = @("XML")

    if (!($validConfigFormats -contains $ConfigStore.Format)){
        Write-DosMessage -Level "Warning" -Message "$($ConfigStore.Format) is not a supported configuration type. Supported types include, $validConfigFormats"
        return $false
    }

    if (!(Test-Path $ConfigStore.Path)){
        Write-DosMessage -Level "Warning" -Message "Path $($ConfigStore.Path) does not exist or user does not have access. Please enter a valid path in the ConfigStore object."
        return $false
    }

    if (!(Test-Path $ConfigStore.Path -PathType Leaf)){
        Write-DosMessage -Level "Warning" -Message "Path $($ConfigStore.Path) is not a file. Please enter a valid path in the ConfigStore object."
        return $false
    }

    Write-DosMessage -Level "Debug" -Message "Config Store is valid."
    return $true
}


function Confirm-IsBoolean {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $name,
        [Parameter(Mandatory = $true)]
        [string] $type,
        $value
    )
    $result = @{check = "IsBoolean"; name = $name; type = $type; value = $value }

    if ($value -isnot [boolean]) {
        $result.Add("errorFlag", 1)
        $result.Add("level", "Fatal")
        $result.Add("message", "You must specify a valid boolean value (ex: `$true or `$false) for the ""$name"" configuration.")
    }
    else {
        $result.Add("errorFlag", 0)
    }
    return (New-CheckResult @result)
}



function Confirm-IsNotNull {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $name,
        [Parameter(Mandatory = $true)]
        [string] $type,
        $value
    )
    $result = @{check = "IsNotNull"; name = $name; type = $type; value = $value}

    if ([string]::IsNullOrEmpty($value)) {
        $result.Add("errorFlag", 1)
        $result.Add("level", "Fatal")
        $result.Add("message", "A null or empty ""$name"" value was found as a configuration.")
    }
    else {
        $result.Add("errorFlag", 0)
    }
    return (New-CheckResult @result)
}



function Confirm-IsValidConnection {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $name,
        [Parameter(Mandatory = $true)]
        [string] $type,
        $value
    )
    $result = @{check = "IsValidConnection"; name = $name; type = $type; value = $value}

    if ($value -is [Hashtable] -and $value.ContainsKey("sqlConnection") -and $value.ContainsKey("sqlTestCommand")) {
        # confirm string is a valid
        try {
            $connection = New-Object System.Data.SqlClient.SQLConnection($value.sqlConnection)
            # confirm access to connection
            try {
                $connection.Open()
                try {
                    $command = New-Object System.Data.SqlClient.SqlCommand($value.sqlTestCommand, $connection)
                    $out = $command.ExecuteReader()
                    if (($out | Measure-Object).Count -eq 0) {
                        throw
                    }
                    $result.Add("errorFlag", 0)
                }
                catch {
                    $result.Add("errorFlag", 1)
                    $result.Add("level", "Fatal")
                    $result.Add("message", "Test sql command failed '$($value.sqlTestCommand)' please check database connection settings.")
                }
            }
            catch {
                $result.Add("errorFlag", 1)
                $result.Add("level", "Fatal")
                $result.Add("message", "Could not connect to '$($value.sqlConnection)' please check database connection settings.")
            }
            finally {
                $connection.Close();
            }
        }
        catch {
            $result.Add("errorFlag", 1)
            $result.Add("level", "Fatal")
            $result.Add("message", "Invalid connection string '$($value.sqlConnection)'.")
        }
    }
    else {
        $result.Add("errorFlag", 1)
        $result.Add("level", "Fatal")
        $result.Add("message", "Valid connection string requires a hashtable with both sqlConnection and sqlTestCommand keys (example: connection = @{sqlConnection=""Data Source=<server>;Initial Catalog=<database>;Integrated Security=True;"";sqlTestCommand=""SELECT <test> FROM <schema><table>""}")
    }
    return (New-CheckResult @result)
}



function Confirm-IsValidDir {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $name,
        [Parameter(Mandatory = $true)]
        [string] $type,
        [string] $value
    )
    $result = @{check = "IsValidDir"; name = $name; type = $type; value = $value }

    if (!(Test-Path (Split-Path $value -Parent))) {
        $result.Add("errorFlag", 1)
        $result.Add("level", "Fatal")
        $result.Add("message", """$value"" does not have a valid directory. Please specify a valid directory for the ""$name"" configuration")
    }
    else {
        $result.Add("errorFlag", 0)
    }
    return (New-CheckResult @result)
}



function Confirm-IsValidEndpoint {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $name,
        [Parameter(Mandatory = $true)]
        [string] $type,
        $value
    )
    $result = @{check = "IsValidEndpoint"; name = $name; type = $type; value = $value }

    try {
        Invoke-WebRequest -Uri $value -Method GET -UseDefaultCredentials -UseBasicParsing
        $result.Add("errorFlag", 0)
    }
    catch [System.Net.WebException] {
        $result.Add("errorFlag", 1)
        $result.Add("level", "Fatal")
        $result.Add("message", "There was an error communicating with the configured $name endpoint. Request: $value. Status Code: $($_.Exception.Response.StatusCode.value__). Message: $($_.Exception.Response.StatusDescription)")
    }
    return (New-CheckResult @result)
}



function Confirm-IsValidPath {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $name,
        [Parameter(Mandatory = $true)]
        [string] $type,
        [string] $value
    )
    $result = @{check = "IsValidPath"; name = $name; type = $type; value = $value }

    if (!(Test-Path $value)) {
        $result.Add("errorFlag", 1)
        $result.Add("level", "Fatal")
        $result.Add("message", """$value"" is not a valid path. Please specify a valid path for the ""$name"" configuration")
    }
    else {
        $result.Add("errorFlag", 0)
    }
    return (New-CheckResult @result)
}



function Confirm-IsValidValue {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string[]] $validateSet,
        [Parameter(Mandatory = $true)]
        [string] $name,
        [Parameter(Mandatory = $true)]
        [string] $type,
        [string] $value
    )
    $result = @{check = "IsValidValue"; name = $name; type = $type; value = $value }

    if ($validateSet -notcontains $value) {
        $result.Add("errorFlag", 1)
        $result.Add("level", "Fatal")
        $result.Add("message", """$value"" is not a valid value. Please specify one of these valid configuration values ""$($validateSet -join ", ")""")
    }
    else {
        $result.Add("errorFlag", 0)
    }
    return (New-CheckResult @result)
}


<#
    .SYNOPSIS
 
    Checks Registry for .net core
 
    .DESCRIPTION
 
    Checks the array of .net core versions
 
#>

function Get-DotNetCoreVersion {
    param (
        [PSCustomObject] $Value,
        [PSCustomObject] $Registry
    )
    if($Registry) {
        $item = $null
        ForEach ($item in $Registry) {
            if($item.DisplayVersion -like "$($Value.softwareVersion)*") {
                Write-Host "Registry:"$item.DisplayVersion, " Manifest:"$Value.softwareVersion
                break
            } 
        }
        It "dependent software $($Value.softwareName) $($Value.softwareVersion) version check" {
            $item.DisplayVersion | Should -BeLike "$($Value.softwareVersion)*"
        }
    }
}


function Get-DotNetVersion {
    param (
        [PSCustomObject] $Value
    )
    $netVersion = @{
        378389 = [version]'4.5'
        378675 = [version]'4.5.1'
        378758 = [version]'4.5.1'
        379893 = [version]'4.5.2'
        393295 = [version]'4.6'
        393297 = [version]'4.6'
        394254 = [version]'4.6.1'
        394271 = [version]'4.6.1'
        394802 = [version]'4.6.2'
        394806 = [version]'4.6.2'
        460798 = [version]'4.7'
        460805 = [version]'4.7'
        461308 = [version]'4.7.1'
        461310 = [version]'4.7.1'
        461808 = [version]'4.7.2'
        461814 = [version]'4.7.2'
        528040 = [version]'4.8'
        528049 = [version]'4.8'
    }
    $netVersionArray = @()
    foreach ($item in $netVersion.GetEnumerator()) {
        #If the variable software i.e '4.6.2' is equal to the Value of '4.6.2' in the Hash table then set the
        #variable softwareConverted to the value of the Key or i.e '394806''
        if($Value.softwareVersion -eq $item.Value) {
            #Append to array as there are multiple DWORDs for each Value of our Table
            $netVersionArray += $item.Key
        } else {
            #Do Nothing (currently looping through entire Hash Table to make sure we dont miss anything)
            continue
        }
    }
    #This looks directly into the registry and compares against our netVersionArray to make sure the requested .NET version matches the DWORD inside our Windows registry
    $itemProperty = Get-ItemProperty -Path "HKLM:\Software\Microsoft\NET Framework Setup\NDP\v4\full\*" | 
        Where-Object { $_.Release -ige $netVersionArray[0] -Or $_.Release -ige $netVersionArray[1] } | 
        Select-Object Release
    $releaseProperty = [string]$itemProperty
    $dword = $releaseProperty.Replace("@{Release=", "").Replace("}", "")
    Write-Host "Registry:"$dword, " Manifest:"$Value.softwareVersion
    $versionNumber = $null
    foreach ($item in $netVersion.GetEnumerator()) {
            if($dword -eq $item.Key) {
                    $versionNumber = $item.Value
                    break
            }
        }
    It ".net dependent software version check" {
        $versionNumber| Should -Not -BeNullOrEmpty
        $versionNumber | Should -BeGreaterOrEqual $Value.softwareVersion
    }
}


<#
    .SYNOPSIS
 
    Returns scoped configuration values in a new object of key value pairs.
 
    .DESCRIPTION
 
    Retrieves content from an XML path. Searches XML content for values nested within the scoped provided. If nodes within the configuration contain values, it will return those values in a new object.
 
    .PARAMETER ConfigSection
 
    The configuration values you wish to return. Passing in an application specific scope will return values within the configuration that are nested in that application scope.
    Additionally, users can pass in a "common" scope to return values nested in the common scope.
 
    .PARAMETER InstallConfigPath
 
    The path to the desired configuration file. Will test if file exists. InstallConfigPath must be passed in as a string.
 
    .EXAMPLE
 
    Set-DosTelemetry -TelemetryKey "testkey" -TelemetryOptOut
#>


function Get-InstallationSettings
{
    param(
        [Parameter(Mandatory=$true)]
        [string] $ConfigSection,
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Fatal" -Message "Path $_ does not exist. Please enter valid path to the install.config."
            }
            if (!(Test-Path $_ -PathType Leaf)) {
                Write-DosMessage -Level "Fatal" -Message "Path $_ is not a file. Please enter a valid path to the install.config."
            }
            return $true
        })]  
        [string] $InstallConfigPath = "install.config"
    )

    Write-DosMessage -Level "Verbose" -Message "Attempting to parse XML content from $installConfigPath."
    try {
        $installConfigXml = [xml](Get-Content $installConfigPath)
    }
    catch {
        Write-DosMessage -Level "Warning" -Message "Error parsing XML content from $installConfigPath. Exception: $($_.Exception)"
        return $null
    }
    
    Write-DosMessage -Level "Verbose" -Message "Searching XML content for $configSection scoped values."
    $sectionSettings = $installConfigXml.installation.settings.scope | Where-Object {$_.name -eq $configSection}

    if($null -eq $sectionSettings){
        Write-DosMessage -Level "Warning" -Message "The '$ConfigSection' scope doesn't exist in '$installConfigPath'."
        return $null
    }

    $installationSettings = @{}

    Write-DosMessage -Level "Verbose" -Message "Reading scoped values if they are not null or empty."
    foreach($variable in $sectionSettings.variable){
        if($variable.name){
            $installationSettings.Add($variable.name, $variable.value)
        }
    }

    if ($installationSettings.Count -eq 0){
        Write-DosMessage -Level "Warning" -Message "There were no configuration values provided in '$ConfigSection' scope."
    }

    return $installationSettings
}


<#
    .SYNOPSIS
 
    Retrieves a list of applications from the windows registry.
 
    .DESCRIPTION
 
    Scans the windows registry for installed applications and returns a list of summary objects.
 
    .EXAMPLE
 
    $x = Get-InstalledApps ()
 
    .OUTPUTS
 
    Returns an array of registered applications. Each item in the array contains the application's DisplayName, Publisher, InstallDate, DisplayVersion and UninstallString
    32 Bit NOTE: IF this function is called from a 32 bit process, the apps returned may differ from the list when called fomr a 64 bit process!
        This is becuase microsfot redirects 32 bit apps in the registry.
        See: https://docs.microsoft.com/en-us/windows/desktop/winprog64/registry-reflection
         
#>


function Get-InstalledApps
{
    if (![Environment]::Is64BitProcess) {
        $regpath = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
    }
    else {
        $regpath = @(
            'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
            'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
        )
    }
    Get-ItemProperty $regpath | .{process{if($_.DisplayName -and $_.UninstallString) { $_ } }} | Select DisplayName, Publisher, InstallDate, DisplayVersion, UninstallString |Sort DisplayName
}



<#
.SYNOPSIS
Creates a new IntegrationServices object
 
.DESCRIPTION
Instantiates an IntegrationServices object using a specified connection string
 
.PARAMETER ConnectionString
Connection string to target Integration Services
 
.INPUTS
None. You cannot pipe objects to Get-IntegrationServices.
 
.OUTPUTS
Integration Services object
 
.EXAMPLE
PS> Get-IntegrationServices
#>

function Get-IntegrationServices {
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType()]
    param([parameter(Mandatory=$false)][string]$ConnectionString = 'Data Source=localhost;Initial Catalog=EDWAdmin;Integrated Security=True')

    $connection = New-Object Data.SqlClient.SqlConnection $ConnectionString

    if ($pscmdlet.ShouldProcess($CatalogName, "Provisioning SSIS catalog")) {
        $connection.Open()
        $integrationServices = New-Object "Microsoft.SqlServer.Management.IntegrationServices.IntegrationServices" $connection  
    }

    return $integrationServices
}


function Get-SqlServerVersion {
    $sqlVersionDict = @{
        13 = 2016
        11 = 2012
    }
    
    $sqlVersion = Get-ItemProperty HKLM:\SOFTWARE\Microsoft\MSSQLServer\MSSQLServer\CurrentVersion | Select-Object -ExpandProperty "CurrentVersion"
    $sqlVersion = [int]$sqlVersion.split('.')[0]

    $currentSqlVersion = $sqlVersionDict[$sqlVersion]

    if ($null -eq $currentSqlVersion) {
        Write-DosMessage -Level 'Error' -Message 'SQL Server Version not in Dictionary'
    }

    return $currentSqlVersion
}


function Invoke-DependentSoftwareCheck {
    param (
        [array] $Data
    )
    Describe "DependentSoftwareCheck" {
        ForEach ($value in $Data) {
            # Continue if sqlVersion is not specified or is different than sqlVersion of machine or if the $value is $null
            if ($null -eq $value -or ($value.PSObject.Properties['sqlVersion'] -and $value.sqlVersion -ne $(Get-SqlServerVersion))) {
                continue
            }
            $w64 = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | where-Object DisplayName -like "*$($value.softwareName)*"
            $w32 = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*  | where-Object DisplayName -like "*$($value.softwareName)*"
            if ($value.softwareName -like "*.net framework*") {
                Get-DotNetVersion $value
            }  elseif ($value.softwareName -like "*.net core*" -and $w64){
                Get-DotNetCoreVersion $value $w64
            } elseif ($value.softwareName -like "*.net core*" -and $w32) {
                Get-DotNetCoreVersion $value $w32
            } else {  
                if($w64) {
                    Get-RegistryAndLocationCheck $w64 $value
                } elseif ($w32) {
                    Get-RegistryAndLocationCheck $w32 $value
                } else {
                    Write-Host "Software not found in Registry - Manifest Path: $($value.softwareLocation)"
                    It "Dependent software $($value.softwareName) $($value.softwareVersion) location exists" {
                        $value.softwareLocation | Should exist 
                    }
                }
            }
        }
    }
}

function Get-RegistryAndLocationCheck {
    param (
        [PSCustomObject] $Registry,
        [PSCustomObject] $Value
    ) 
    if ($Registry) {
        Write-Host "Registry:"$Registry.DisplayName," Manifest:"$Value.softwareName
        It "Dependent software $($Value.softwareName) $($Value.softwareVersion) exists in registry" {
            $Registry.DisplayName | Should -Match "$($Value.softwareName)*"
        }   
        if ($Value.versionCheckType -eq 'exact') {
            Write-Host "Registry:"$Registry.DisplayVersion, " Manifest:"$Value.softwareVersion
            It "Dependent software $($Value.softwareName) $($Value.softwareVersion) version check" {
                $Registry.DisplayVersion  | Should -BeExactly $Value.softwareVersion
            }
        } elseif ($Value.versionCheckType -eq 'min') {
            Write-Host "Registry:"$Registry.DisplayVersion, " Manifest:"$Value.softwareVersion
            It "Dependent software $($Value.softwareName) $($Value.softwareVersion) version check" {
                $Registry.DisplayVersion  | Should -BeGreaterOrEqual $Value.softwareVersion
            }
        }
    } 
} 


<#
    .SYNOPSIS
    Checks whether the OS Version from a manifest json file is equal to that on the machine this is being ran on
 
    .DESCRIPTION
 
    .PARAMETER Data
    Accepts an array of OS Version checktype objects that have been converted from JSON
#>


function Invoke-OsVersionCheck {
    param (
        [array] $AllowedVersions
    )

    $currentOsVersion = (Get-ComputerInfo -Property osname).osname
    Describe "Os Version Check" {
        $matchedOSVersion = $null

        ForEach ($value in $AllowedVersions) {
            if ($currentOsVersion -like "*$value*") {
                $matchedOSVersion = $value
                break
            }
        }

        It "'$currentOsVersion' is an allowed OS" {
            $matchedOSVersion | Should -Not -BeNullOrEmpty
            $currentOsVersion | Should -BeLike "*$matchedOSVersion*"
        }
    }
}


<#
    .SYNOPSIS
 
    Checks whether the Powershell Version from a manifest json file is equal to that on the machine this is being ran on
 
    .DESCRIPTION
 
 
    .PARAMETER Data
 
    Accepts an array of powershell objects that have been converted from JSON
 
 
#>


function Invoke-PowershellVersionCheck {
    param (
        [array] $Data
    )
    $currentVersion = "$($psversiontable.psversion.major).$($psversiontable.psversion.minor)"
    Write-Host "Powershell version is $currentVersion)"
    Describe "PowershellVersionCheck" {
        ForEach ($value in $Data) {
            if ($null -eq $value) {
                continue
            }
             
            #process check
            if ($value.versionCheckType -eq 'min'){
                It "Version of powershell minimum version $($value.powershellVersion)" {
                    $currentVersion | Should -BeGreaterOrEqual $value.powershellVersion
                }
            } elseif ($value.versionCheckType -eq 'exact') {
                It "Version of powershell exactly $($value.powershellVersion)" {
                    $currentVersion | Should -BeExactly $value.powershellVersion
                }
            }
        }
    }
}


<#
    .SYNOPSIS
 
    Checks whether the Windows Features from a manifest json file are installed and enabled on the executing environmnet
 
    .DESCRIPTION
 
 
    .PARAMETER Data
 
    Accepts an array of powershell objects that have been converted from JSON
#>


function Invoke-WindowsFeatureCheck {
    param (
        [array] $Data
    )

    $currentOsVersion = (Get-ComputerInfo -Property osname).osname
    $amIAServer = $currentOsVersion.StartsWith("Microsoft Windows Server")
    $amIWindowsTen = $currentOsVersion.StartsWith("Microsoft Windows 10")

    Describe "Windows Feature Check" {
        ForEach ($value in $Data) {
            if ($amIAServer) {
                if ($value.os -like "*Server*") {
                    ForEach ($feature in $value.featureList) {
                        It "'$feature' windows feature is installed & enabled" {
                            Get-WindowsFeature -Name $feature | Should -Not -BeNullOrEmpty
                            (Get-WindowsFeature -Name $feature)."InstallState" | Should -Be "Installed"
                        }
                    }
                }
                else {
                    Write-Warning "Check designed for $($value.os) does not match the Current OS: $currentOsVersion. Skipping Test"
                }
            } elseif ($amIWindowsTen) {
                if ($value.os -like "*10*") {
                    ForEach ($feature in $value.featureList) {
                        It "'$feature' windows feature is installed & enabled" {
                            Get-WindowsOptionalFeature -Online -FeatureName $feature | Should -Not -BeNullOrEmpty
                            (Get-WindowsOptionalFeature -Online -FeatureName $feature).State | Should -Be "Enabled"
                        }
                    }
                }
                else {
                    Write-Warning "Check designed for $($value.os) does not match the Current OS: $currentOsVersion. Skipping Test"
                }
            }
            else {
                Write-Warning "Unrecognized OS: $currentOsVersion"
            }
        }
    }
}


function Merge-DosHashtable {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        $hash
    )
    $output = @{}
    $subHash = @{}
    foreach ($hash in $input) {
        if ($hash -is [Hashtable]) {
            foreach ($key in $hash.Keys) {
                if ($key -ne "_type_") {
                    $output.$key = $hash.$key
                }
                else {
                    foreach ($subKey in $hash[$key].Keys) {
                        $subHash.$subKey = $hash[$key].$subKey
                    }
                }
            }
        }
    }
    $output.Add("_type_", $subHash)
    return $output
}



<#
    .SYNOPSIS
 
    Creates an IIS Application Pool with the specified options.
 
    .DESCRIPTION
 
    Uses the WebAdministration powershell module to create IIS app pools.
 
    .PARAMETER IISAppPoolName
 
    Name of App Pool to create.
 
    .PARAMETER IdentityCredential
 
    PSCredential with a username and password.
 
    .PARAMETER RuntimeDotNetVersion
 
    Runtime version to be used in app pool creation. Defaults to 'v4.0'
 
    .EXAMPLE
 
    New-AppPool -IISAppPoolName "CatalystAppPool" -IdentityCredential $psCredential
#>


function New-AppPool {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateLength(1,64)]
        [ValidateScript({
            if ($_ -match '[^a-zA-Z0-9]') {
                Write-DosMessage -Level "Error" -Message "$_ must only contain alphanumeric values. Please remove special characters."
            }
            else {
                $true
            }
        })]
        [string] $IISAppPoolName,
        [PSCredential] $IdentityCredential,
        [string] $RuntimeDotNetVersion = "v4.0"
    )

    # Tests if current session has elevated permissions required to create IIS App Pool
    Test-ElevatedPermission

    # TODO: Validate bitness for powershell session. Import-Module WebAdministration most likely requires 64 bit

    Import-Module WebAdministration

    if ($IdentityCredential) {
        Write-DosMessage -Level "Information" -Message "Attempting to validate credential"
        if (!((Confirm-DosCredential -Credential $IdentityCredential).isValid)) {
            Write-DosMessage -Level "Fatal" -Message "Username or password is not valid"
        }
    }

    if(!(Test-Path "IIS:\AppPools\$IISAppPoolName" -PathType Container))
    {
        Write-DosMessage -Level "Information" -Message "Creating AppPool $IISAppPoolName."
        $appPool = New-WebAppPool $IISAppPoolName
        $appPool | Set-ItemProperty -Name "managedRuntimeVersion" -Value "$RuntimeDotNetVersion"
        Set-AppPoolSettings -IISAppPoolName $IISAppPoolName -IdentityCredential $IdentityCredential
        $appPool.Stop()
    }else{
        Write-DosMessage -Level "Error" -Message "AppPool: $IISAppPoolName already exists."
        return
    }         
    
}


function New-CheckResult {
    [CmdletBinding()]
    param (
        $name,
        $type,
        $value,
        $check,
        $errorFlag,
        $level,
        $message
    )
    $result = New-Object PSObject
    $result | Add-Member -Type NoteProperty -Name name -Value $name
    $result | Add-Member -Type NoteProperty -Name type -Value $type
    $result | Add-Member -Type NoteProperty -Name value -Value $value
    $result | Add-Member -Type NoteProperty -Name check -Value $check
    $result | Add-Member -Type NoteProperty -Name errorFlag -Value $errorFlag
    $result | Add-Member -Type NoteProperty -Name level -Value $level
    $result | Add-Member -Type NoteProperty -Name message -Value $message    
    return $result
}



<#
.SYNOPSIS
Creates a new SSIS catalog
 
.DESCRIPTION
Adds a new SSIS catalog with specified name and encryption key
 
.PARAMETER IntegrationServices
IntegrationServices object where catalog should be created
 
.PARAMETER CatalogEncryptionKey
Key to use for encrypting catalog, if it must be created
 
.PARAMETER CatalogName
Name of SSIS catalog to contain project
 
.INPUTS
None. You cannot pipe objects to New-SsisCatalog
 
.OUTPUTS
Created SSIS catalog
 
.EXAMPLE
PS> New-SsisCatalog -IntegrationServices $integrationServices -CatalogEncryptionKey 'password'
#>

function New-SsisCatalog {
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType()]
    param(
        [parameter(Mandatory=$true)]$IntegrationServices,
        [parameter(Mandatory=$true)][string]$CatalogEncryptionKey,
        [parameter(Mandatory=$false)][string]$CatalogName = 'SSISDB')

    if ($pscmdlet.ShouldProcess($CatalogName, "Provisioning SSIS catalog")) {
        $catalog = New-Object "Microsoft.SqlServer.Management.IntegrationServices.Catalog" ($IntegrationServices, $CatalogName, $CatalogEncryptionKey)  
        $catalog.Create()  
    }

    return $catalog
}


<#
.SYNOPSIS
Creates a new SSIS folder
 
.DESCRIPTION
Adds a new SSIS folder with specified name
 
.PARAMETER SsisCatalog
IntegrationServices Catalog object where folder should be created
 
.PARAMETER FolderName
Name of folder to create
 
.INPUTS
None. You cannot pipe objects to New-SsisFolder
 
.OUTPUTS
Created SSIS folder
 
.EXAMPLE
PS> New-SsisFolder -SsisCatalog $catalog -FolderName 'Catalyst'
#>

function New-SsisFolder {
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType()]
    param(
        [parameter(Mandatory=$true)]$SsisCatalog,
        [parameter(Mandatory=$true)][string]$FolderName)

    if ($pscmdlet.ShouldProcess($FolderName, "Creating SSIS folder")) {
        $folder = New-Object "Microsoft.SqlServer.Management.IntegrationServices.CatalogFolder" ($SsisCatalog, $FolderName, "Folder to contain SSIS projects")
        $folder.Create()
    }

    return $folder
}


<#
.SYNOPSIS
Creates a new SSIS project
 
.DESCRIPTION
Deploys an ISPAC into an existing SSIS catalog and folder
 
.PARAMETER SsisFolder
IntegrationServices Folder object where project should be created
 
.PARAMETER ProjectName
Name of project to create
 
.PARAMETER IspacPath
Path to ISPAC to deploy to initialize project
 
.INPUTS
None. You cannot pipe objects to New-SsisProject
 
.OUTPUTS
None.
 
.EXAMPLE
PS> New-SsisProject -SsisFolder $folder -ProjectName 'CatalystLoader' -IspacPath "C:\a\place"
#>

function New-SsisProject {
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType()]
    param(
        [parameter(Mandatory=$true)]$SsisFolder,
        [parameter(Mandatory=$true)][string]$ProjectName,
        [parameter(Mandatory=$true)][string]$IspacPath)

    if ($pscmdlet.ShouldProcess($ProjectName, "Deploying SSIS project")) {
        [byte[]] $projectFile = [System.IO.File]::ReadAllBytes($IspacPath)
        $SsisFolder.DeployProject($ProjectName, $projectFile)
    }
}


<#
    .SYNOPSIS
 
    Publishes a .net core web applications.
 
    .DESCRIPTION
 
    Creates the necessary folder and expands the archive containing the .net core web app to the folder. Also creates the appropriate IIS Site and associates the specified App Pool with the site.
 
    .PARAMETER WebApplicationPackagePath
 
    Path to the zip file containing the .net core web applications
 
    .PARAMETER AppPoolName
 
    Application pool to associate with the web application. This must already exist
 
    .PARAMETER IISWebSite
 
    IIS Site to install the application to. Defaults to "Default Web Site" if not specified
 
    .PARAMETER AppName
 
    Application name - used for both the site AND the folder created underneath the IISWebSite root
 
    .PARAMETER PathsToPreserve
 
    Array of paths to preserve during a deployment, such as logs, relative to the install directory in IIS, so they are not removed during the upgrade of an application. Ignored for new installs.
 
 
    .EXAMPLE
 
    Publish-DotNetCoreWebApp -WebApplicationPackagePath $WebAppPackagePath -AppPoolCredentials $AppPoolCredential -AppName $AppName -IISWebSite $IISWebSite -AppPoolName $AppPoolName -PathsToPreserve @("logs")
#>

function Publish-DotNetCoreWebApp{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Fatal" -Message "WebApplicationPackagePath $_ does not exist. Please enter valid path."
            }
            else {
                $true
            }
        })]
        [string] $WebApplicationPackagePath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateLength(1,64)]
        [ValidateScript({
            if ($_ -match '[^a-zA-Z0-9]') {
                Write-DosMessage -Level "Fatal" -Message "$_ must only contain alphanumeric values. Please remove special characters."
            }
            else {
                $true
            }
        })]
        [string] $AppPoolName,
        [string] $IISWebSite = "Default Web Site",
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $AppName,
        [string[]] $PathsToPreserve
    )

    # Tests if current session has elevated permissions required to create IIS App Pool
    Test-ElevatedPermission

    Import-Module WebAdministration

    #Stop app pool if it's running (upgrade scenario)
    [string] $appPoolUser = $null
    if(Test-Path "IIS:\AppPools\$AppPoolName" -PathType Container){
    
        $currentState = Get-WebAppPoolState -Name $AppPoolName
        if($currentState.Value -ne "Stopped"){
            Write-DosMessage -Level "Information" -Message "App pool $AppPoolName not stopped, current state $currentState"
            Stop-WebAppPool -Name $AppPoolName
            Wait-AppPoolState -AppPoolName $AppPoolName -AppPoolState "Stopped"
        }

        $appPool = Get-Item "IIS:\AppPools\$AppPoolName"

        $appPoolUser = $appPool.processModel.userName
        
    }
    else{
        Write-DosMessage -Level "Error" -Message "No app pool found named $AppPoolName"
        return
    }

    #Get/Create folder

    $physicalWebPath = Join-Path (Get-IISWebSitePath -WebSiteName $IISWebSite) $AppName

    if(!(Test-Path $physicalWebPath)){
        Write-DosMessage -Level "Information" -Message "Creating directory $physicalWebPath"
        New-Item -Path $physicalWebPath -ItemType Directory | Out-Null
    }
    else {
        Write-DosMessage -Level "Information" -Message "Directory $physicalWebPath exists, removing previous installation files"

        if ($PathsToPreserve) {
            Get-ChildItem -Path "$physicalWebPath\*" -Exclude $PathsToPreserve | Remove-Item -Recurse -Force
        }
        else {
            Remove-Item -Path "$physicalWebPath\*" -Recurse -Force
        }
    }

    #Folder ACL ops
    if (!([string]::IsNullOrEmpty($appPoolUser))) {
        Write-DosMessage -Level "Information" -Message "Adding read and write to $physicalWebPath for $appPoolUser"
        Add-AclFolder -FolderPath $physicalWebPath -User $appPoolUser -Permission "Read"

        Add-AclFolder -FolderPath $physicalWebPath -User $appPoolUser -Permission "Write"
    }

    #Extract zip file

    Write-DosMessage -Level "Information" -Message "Extracting $WebApplicationPackagePath to $physicalWebPath"
    Expand-DosArchive -ArchiveFile $WebApplicationPackagePath -DestinationPath $physicalWebPath -Overwrite

    Write-DosMessage -Level "Information" -Message "Creating web site $AppName on site $IISWebSite using application pool $AppPoolName"
    New-WebApplication -Name $AppName -Site $IISWebSite -PhysicalPath $physicalWebPath -ApplicationPool $AppPoolName -Force | Out-Null

    Start-WebAppPool -Name $AppPoolName

}


[string] $script:WebDeployExecutableName = "msdeploy.exe"

# alternatively the directory is located in registry: HKLM\SOFTWARE\Microsoft\IIS Extensions\MSDeploy
[Array] $script:MsdeployLocations = @([System.IO.Path]::Combine(([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::ProgramFilesX86)), "IIS", "Microsoft Web Deploy V3"))

<#
    .SYNOPSIS
 
    Publishes the target Web Deploy Web application.
 
    .DESCRIPTION
 
    Uses WebDeploy to deploy specified web application with specified parameters
 
    .PARAMETER WebDeployPackageFilePath
 
    File path to web deploy zip file to publish to server. Mandatory
 
    .PARAMETER WebDeployParameterFilePath
 
    File path to the set parameter XML file that contains transforms for the web.config file (connection strings, etc)
 
    .PARAMETER WebParameters
 
    Arraylist object to the set parameters for the web.config file (connection strings, etc)
 
    .EXAMPLE
 
    Publish-DosWebApp -WebDeployPackageFilePath ".\test.zip" -WebDeployParameterFilePath ".\test.params.xml"
#>

function Publish-WebDeployWebApp {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string] $WebDeployPackageFilePath,
        [string] $WebDeployParameterFilePath,
        [System.Collections.ArrayList] $WebParameters,
        [string] $AppName,
        [string] $IISWebSite = "Default Web Site",
        [string] $AppPoolName,
        [string[]] $PathsToPreserve
    )

    Write-DosMessage -Level "Verbose" -Message "Confirming process is running with elevated permissions."
    Test-ElevatedPermission

    if(!(Test-Path $WebDeployPackageFilePath)){
        Write-DosMessage -Level "Error" -Message "Unable to locate $WebDeployPackageFilePath"
        return
    }
    
    Write-DosMessage -Level "Verbose" -Message "Generating WebDeploy arguments with '$WebDeployPackageFilePath' file path."
    # --% escapes out the rest of the line. otherwise we would need to add a tick(`) for semi-colons, apostrophes, and quotes.
    [string] $webDeployArguments = "--%"`
                               +" -source:package='$WebDeployPackageFilePath'"`
                               +" -dest:auto,includeAcls=""False"""`
                               +" -verb:sync"`
                               +" -disableLink:AppPoolExtension"`
                               +" -disableLink:ContentExtension"`
                               +" -disableLink:CertificateExtension"

    Write-DosMessage -Level "Verbose" -Message "Ensuring that WebDeploy parameter file path exists."
    if(!([string]::IsNullOrEmpty($WebDeployParameterFilePath))){
        if ((Test-Path $WebDeployParameterFilePath) -and $WebParameters){
            Write-DosMessage -Level "Error" -Message "It is not supported to provide both an XML settings file and deploy arg objects. Provide only one."
            Return
        }

        if(!(Test-Path $WebDeployParameterFilePath)){
            Write-DosMessage -Level "Error" -Message "Unable to locate $WebDeployParameterFilePath"
            Return
        }
        else{
            $webDeployArguments = $webDeployArguments + " -setParamFile:""$WebDeployParameterFilePath"""
        }
        
    }
    else{
        foreach ($param in $WebParameters)
        {
            $webDeployArguments = $webDeployArguments + $(" -setParam:name='{0}',value='{1}'" -f $param.Name, $param.Value)
        }
    }

    Write-DosMessage -Level "Information" -Message "Attempting to retrieve WebDeploy executable path."
    [string] $webDeployExecutablePath = Resolve-FilePath -PathsToSearch $script:MsdeployLocations -FilePattern $script:WebDeployExecutableName

    if([String]::IsNullOrEmpty($webDeployExecutablePath)){
        Write-DosMessage -Level "Error" -Message "Unable to locate web deploy, unable to publish DOS application"
        return
    }

    if ($PathsToPreserve) {
        foreach ($path in $PathsToPreserve) {
            $webDeployArguments += " -skip:skipaction='Delete',objectName='dirPath',absolutepath='$path$'"
            $webDeployArguments += " -skip:skipaction='Delete',objectName='filePath',absolutepath='$path\\.*$'"
        }
    }

    Write-DosMessage -Level "Verbose" -Message "Running: $webDeployExecutablePath $webDeployArguments"
    $output = Start-CommandAndReturnOutput -Command  "& ""$webDeployExecutablePath"" $webDeployArguments | Out-String"

    if ([string]::IsNullOrEmpty($output)) {
        Write-DosMessage -Level "Fatal" -Message "Web Deploy output returned empty or null. Please validate Web Deploy 3.5 is correctly installed."
        return
    }

    $physicalWebPath = Join-Path (Get-IISWebSitePath -WebSiteName $IISWebSite) $AppName
    $appPool = Get-Item "IIS:\AppPools\$AppPoolName"
    $appPoolUser = $appPool.processModel.userName

    Write-DosMessage -Level "Information" -Message "Adding read and write to $physicalWebPath for $appPoolUser"
    if (!([string]::IsNullOrEmpty($appPoolUser))) {
        Add-AclFolder -FolderPath $physicalWebPath -User $appPoolUser -Permission "Read"
        Add-AclFolder -FolderPath $physicalWebPath -User $appPoolUser -Permission "Write"
    }

    Write-DosMessage -Level "Verbose" -Message $output
}


function Push-DosConfigType {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [Hashtable] $hash,
        [Parameter(Position = 0, Mandatory = $true)]
        [String] $type
    )
    $newHash = @{};
    foreach ($key in $hash.keys) {
        $newHash.Add($key, $type)
    }
    $hash._type_ = $newHash
    return $hash
}



<#
    .SYNOPSIS
 
    Looks for the v1/v2/v3/vN in the URL and removes it.
 
    .DESCRIPTION
 
    There are various times when we want the version number removed from the URL, thus we have this helper function to make it
    easy to remove the version number.
 
    .PARAMETER Url
 
    Name the URL
 
    .EXAMPLE
 
    Remove-VersionFromLocalPath -Url 'https://www.example.com/DiscoveryService/v1/Services
#>

function Remove-VersionFromLocalPath {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $Url
    )

    $uri = [System.Uri]$Url

    $uriLocalPath = $($uri.LocalPath) -replace "/v(\d+)([/]?)", "/"
    $uriLocalPath = $uriLocalPath -replace "//","/"

    if($($uri.LocalPath).EndsWith("/") -and -not($uriLocalPath.endswith("/"))){
        #the original ended with a slash, and the new one doesn't, add a slash
        $uriLocalPath = "$uriLocalPath/"
    }

    if(-not($($uri.LocalPath).EndsWith("/")) -and $uriLocalPath.endswith("/")){
        #the original didn't end with a slash, but the new one does.
        #removing trailing slashes, remove it.
        $uriLocalPath = $uriLocalPath.substring(0,$uriLocalPath.Length-1)
    }

    $newUrl = "$($uri.Scheme)://$($uri.Host)$uriLocalPath$($uri.Query)"

    #This was the original code, but it didn't handle all the cases that I wanted it to handle.
    # So the following line was replaced by all the code above.
    #$NewUrl = $Url -replace "/v(\d+)([/]?)", "/"
        
    if ($Url -ne $NewUrl) {
        Write-DosMessage -Level "Debug" -Message "Removed version from url. From: $Url To: $NewUrl"
    }
    return $newUrl
}


<#
    .SYNOPSIS
 
    Repairs non-cannonical ACLs
 
    .DESCRIPTION
 
    Attempts to reorder ACEs in the specified ACL.
 
    .PARAMETER Acl
 
    ACL needing repair
 
    .PARAMETER Path
 
    Path to the item needing ACL repairs - needed because sometimes the ACL passed in doesn't have an associated path.
 
    .EXAMPLE
 
    Repair-AclCanonicalOrder -Acl $x
#>

function Repair-AclCanonicalOrder {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Security.AccessControl.DirectorySecurity] $Acl,
        [string] $Path
    )
    
    if ($Acl.AreAccessRulesCanonical) {
        Write-DosMessage -Level "Debug" -Message "Acls are canonical"
        return
    }

    Write-DosMessage -Level "Debug" -Message "Acls are not canonnical, attempting to fix Acls"

    # Convert ACL to a raw security descriptor:
    $RawSD = New-Object System.Security.AccessControl.RawSecurityDescriptor($Acl.Sddl)

    # Create a new, empty DACL
    $NewDacl = New-Object System.Security.AccessControl.RawAcl(
        [System.Security.AccessControl.RawAcl]::AclRevision,
        $RawSD.DiscretionaryAcl.Count  # Capacity of ACL
    )

    # Put in reverse canonical order and insert each ACE (I originally had a different method that
    # preserved the order as much as it could, but that order isn't preserved later when we put this
    # back into a DirectorySecurity object, so I went with this shorter command)
    $RawSD.DiscretionaryAcl | Sort-Object @{E={$_.IsInherited}; Descending=$true}, AceQualifier | ForEach-Object {
        $NewDacl.InsertAce(0, $_)
    }

    # Replace the DACL with the re-ordered one
    $RawSD.DiscretionaryAcl = $NewDacl

    # Commit those changes back to the original SD object (but not to disk yet):
    $Acl.SetSecurityDescriptorSddlForm($RawSD.GetSddlForm("Access"))

    # Commit changes
    $Acl | Set-Acl -Path $Path
}


#List of common locations for assemblies
[System.Collections.ArrayList] $script:commonFilePaths = @("$PSScriptRoot\..\assemblies","C:\Program Files (x86)\Microsoft SQL Server")

<#
    .SYNOPSIS
 
    Attempts to resolve the the assembly file name specified to a fully qualified path.
 
    .DESCRIPTION
 
    Looks through the commonAssemblyFilePaths for files matching the specified file name.
 
    .PARAMETER FileName
 
    Assembly file name. Ex. "Microsoft.Test.dll"
 
    .EXAMPLE
 
    Resolve-AssemblyFilePath -AssemblyFileName ".\test.dll"
 
    .OUTPUTS
 
    $null if no file found, otherwise the fully qualitifed path to the file.
 
#>

function Resolve-CommonFilePath{
    [CmdletBinding()]
    param(
        [string] $AssemblyFileName
    )

    return Resolve-FilePath -PathsToSearch $script:commonFilePaths -FilePattern $AssemblyFileName
}


<#
    .SYNOPSIS
 
    Attempts to resolve the the assembly file name specified to a fully qualified path.
 
    .DESCRIPTION
 
    Looks through the commonAssemblyFilePaths for files matching the specified file name.
 
    .PARAMETER AssemblyFileName
 
    Assembly file name. Ex. "Microsoft.Test.dll"
 
    .EXAMPLE
 
    Resolve-FilePath -PathsToSearch "C:\Path\To\Search" -FilePatter "Executable.exe"
 
    .OUTPUTS
 
    $null if no file found, otherwise the fully qualitifed path to the file.
 
#>

function Resolve-FilePath{
    [CmdletBinding()]
    param(
        [Array] $PathsToSearch,
        [string] $FilePattern
    )

    Write-DosMessage -Level "Verbose" -Message "Attempting to retrieve file path that matches '$FilePattern'."

    foreach($filePath in $PathsToSearch){
        try {
            Write-DosMessage -Level "Verbose" -Message "Searching in '$filePath'"
            $files = Get-ChildItem -Path $filePath -Filter $FilePattern -Recurse | Where-Object { $_.PSIsContainer -ne $true }
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Error retrieving path that matches pattern '$FilePattern'. Exception: $($_.Exception)."
        }
        if(($files -ne $null) -and ($files.Length -gt 0)){
            Write-DosMessage -Level "Verbose" -Message "Found $($files.Length) files under $assemblyFilePath for $AssemblyFileName"
            return $files[0].FullName
        }
    }

    Write-DosMessage -Level "Verbose" -Message "Unable to find path that matches pattern '$FilePattern'. Returning null."
    return $null

}

<#
    .SYNOPSIS
 
    Adds to the common search paths used when attempting to resolve an assembly file location.
 
    .DESCRIPTION
 
    Adds a path to the list of paths to search.
 
    .EXAMPLE
 
    Add-CommonPath -Path "C:\Windows"
 
#>

function Add-CommonPath{
    [CmdletBinding()]
    param(
        [string] $Path
    )

    $script:commonFilePaths.Add($Path)

}

<#
    .SYNOPSIS
 
    Clears the common search paths.
 
    .DESCRIPTION
 
    Clears the search path variable.
 
    .EXAMPLE
 
    Clear-CommonPath
 
#>

function Clear-CommonPath{
    
    $script:commonFilePaths = @()
}



<#
    .SYNOPSIS
 
    Configure an IIS Application Pool with the specified settings.
 
    .DESCRIPTION
 
    Uses the WebAdministration powershell module to create IIS app pools.
 
    .PARAMETER IISAppPoolName
 
    Name of App Pool to configure.
 
    .PARAMETER IdentityCredential
 
    PSCredential with a username and password.
 
    .EXAMPLE
 
    Set-AppPoolSettings -IISAppPoolName "CatalystAppPool" -IdentityCredential $psCredential
#>


function Set-AppPoolSettings {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateLength(1, 64)]
        [ValidateScript( {
                if ($_ -match '[^a-zA-Z0-9]') {
                    Write-DosMessage -Level "Error" -Message "$_ must only contain alphanumeric values. Please remove special characters."
                }
                else {
                    $true
                }
            })]
        [string] $IISAppPoolName,
        [PSCredential] $IdentityCredential,
        [switch] $NoCredential
    )  

    $poolpath = "IIS:\AppPools\$IISAppPoolName"
    try {
        $appPool = Get-Item -Path $poolpath -ErrorAction Stop
    } Catch {
        Write-DosMessage -Level "Fatal" -Message "Failed to get application pool. Exception: $($_.Exception)"
        Break
    }

    Write-DosMessage -Level "Information" -Message "Configuring AppPool $IISAppPoolName."

    if ($IdentityCredential) {
        if (!((Confirm-DosCredential -Credential $IdentityCredential).isValid)) {
            Write-DosMessage -Level "Fatal" -Message "Username or password is not valid"
        }

        if (![string]::IsNullOrEmpty($IdentityCredential.UserName) -and $IdentityCredential.GetNetworkCredential().Password -ne $null) {
            Write-DosMessage -Level "Information" -Message "Configuring '$IISAppPoolName' app pool's identity with the credentials provided."
            $appPool.processModel.userName = $IdentityCredential.UserName
            $appPool.processModel.password = $IdentityCredential.GetNetworkCredential().Password
        }

        # IdentityType 3 references 'SpecificUser' https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/applicationpools/add/processmodel
        if (!($appPool.processModel.identityType -eq "SpecificUser")) {
            Write-DosMessage -Level "Information" -Message "Identity Type was not set to SpecificUser, setting to 3"
            $appPool.processModel.identityType = 3
        }
    }
    else {
        Write-DosMessage -Level "Information" -Message "No identity credential was provided. '$IISAppPoolName' identity configuration will not be altered."
    }

    if ($NoCredential.IsPresent) {
        Write-DosMessage -Level "Information" -Message "NoCredential parameter was provided. Attempting to configure '$IISAppPoolName' identity type."
        if (!($appPool.processModel.identityType -eq "ApplicationPoolIdentity")) {
            Write-DosMessage -Level "Information" -Message "Identity Type was not set to ApplicationPoolIdentity, setting to 4"
            $appPool.processModel.identityType = 4
        }
        Write-DosMessage -Level "Information" -Message "'$IISAppPoolName' identity type successfully set to 'ApplicationPoolIdentity'"
    }
    
    
    if ($appPool.processModel.loaduserprofile -eq $false) {   
        Write-DosMessage -Level "Verbose" -Message "loaduserprofile was not set to true, setting to true" 
        $appPool.processModel.loaduserprofile = $true
    }
    if (!($appPool.startMode -eq "alwaysrunning")) {
        Write-DosMessage -Level "Verbose" -Message "startmode was not set to alwaysrunning, setting to alwaysrunning"
        $appPool.startMode = "alwaysrunning"
    }
    if (!($appPool.processmodel.idletimeout -eq [TimeSpan]::FromMinutes(0))) {
        $appPool.processmodel.idletimeout = [TimeSpan]::FromMinutes(0)
    }
    if (!($appPool.processmodel.idletimeoutaction -eq "suspend")) {
        Write-DosMessage -Level "Verbose" -Message "idletimeoutaction was not set to suspend, setting to suspend"
        $appPool.processmodel.idletimeoutaction = "suspend"
    }
    if (!($appPool.cpu.action -eq "ThrottleUnderLoad")) {
        Write-DosMessage -Level "Verbose" -Message "cpu limit action was not set, setting ThrottleUnderLoad to 10%"
        $appPool.cpu.action = "ThrottleUnderLoad"
        $appPool.cpu.limit = 10000
    }
    try {
        $appPool | Set-Item -Verbose -ErrorAction Stop
    } catch {
        Write-DosMessage -Level "Fatal" -Message "Failed to set application pool settings. Exception: $($_.Exception)"
        Break
    }
    
    
}


<#
    .SYNOPSIS
 
    Alters IIS authentication type based off parameter values provided by user.
 
    .DESCRIPTION
 
    Pull current web.config values, unlocking the configurations, and altering based of provided values.
 
    .PARAMETER AuthenticationType
 
    Array of strings allowing for either 'Windows', 'Anonymous' or both.
 
    .PARAMETER SiteName
 
    Name of IIS website being used.
 
    .PARAMETER ApplicationName
 
    Name of IIS application being altered.
 
    .EXAMPLE
 
    Set-IISAuthentication -AuthenticationType 'Windows' -SiteName 'Default Web Site' -ApplicationName 'HCApp'
#>


function Set-IISAuthentication {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateSet("Windows", "Anonymous")]
        [string[]] $AuthenticationType,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $SiteName,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $ApplicationName
    )

    Add-Assembly -Assemblies "$env:systemroot\system32\inetsrv\Microsoft.Web.Administration.dll"
    $manager = New-Object Microsoft.Web.Administration.ServerManager      

    if ($AuthenticationType.Contains("Anonymous")){
        Edit-AuthenticationType -AuthenticationType "Anonymous" -SiteName $SiteName -AppName $ApplicationName -ApplicationHostManager $manager -Enable
    }
    else {
        Edit-AuthenticationType -AuthenticationType "Anonymous" -SiteName $SiteName -AppName $ApplicationName -ApplicationHostManager $manager
    }
    
    if ($AuthenticationType.Contains("Windows")){
        Edit-AuthenticationType -AuthenticationType "Windows" -SiteName $SiteName -AppName $ApplicationName -ApplicationHostManager $manager -Enable
    }
    else {
        Edit-AuthenticationType -AuthenticationType "Windows" -SiteName $SiteName -AppName $ApplicationName -ApplicationHostManager $manager
    }

    $manager.CommitChanges()
}

<#
    .SYNOPSIS
 
    Used to alter authentication type of an IIS web site/application
 
    .DESCRIPTION
 
 
 
    .PARAMETER AuthenticationType
 
    Authentication Mode that will be altered
 
    .PARAMETER SiteName
 
    Name of the IIS Website
 
    .PARAMETER AppName
 
    Name of the IIS application
 
    .PARAMETER Enable
 
    Switch toggling whether the specified Authentication will be enabled or disabled
 
    .EXAMPLE
 
    Alter-AuthenticationType -AuthenticationType "Windows" -SiteName "TestSite" -AppName "TestApp" -ApplicationConfiguration $config -Enable
#>


function Edit-AuthenticationType {
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateSet("Windows", "Anonymous")]
        [string] $AuthenticationType,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $SiteName,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $AppName,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [Microsoft.Web.Administration.ServerManager] $ApplicationHostManager,
        [switch] $Enable
    )

    if ($AuthenticationType -eq  "Windows") {
        $authenticationString = "windowsAuthentication"
    }
    else {
        $authenticationString = "anonymousAuthentication"
    }

    $config = $ApplicationHostManager.GetApplicationHostConfiguration()
    $section = $config.GetSection("system.webServer/security/authentication/$authenticationString")
    $section.OverrideMode = "Allow"  
    $ApplicationHostManager.CommitChanges()  
    # When Invoke-Pester is called on the Publish-DosWebApplication Integration tests there seems to be inconsistent behavior when ran in and out of a debug session.
    Start-Sleep -s 3
    Write-DosMessage -Level "Information" -Message "Unlocked system.webServer/security/authentication/$authenticationString for configuration"

    Set-WebConfigurationProperty -Filter "/system.webServer/security/authentication/$authenticationString" -Name Enabled -Value $Enable.IsPresent -PSPath "IIS:\Sites\$SiteName\$AppName"
    
    if ($Enable.IsPresent){
        Write-DosMessage -Level "Information" -Message "Enabled $AuthenticationType Authentication on $SiteName/$AppName"
    }
    else {
        Write-DosMessage -Level "Information" -Message "Disabled $AuthenticationType Authentication on $SiteName/$AppName"
    }
}


function Start-CommandAndReturnOutput {
    [CmdletBinding()]
    param(
        [ValidateNotNullOrEmpty()]
        [string] $Command
    )

    Write-DosMessage -Level "Verbose" -Message "Running: $Command"

    $output = Invoke-Expression "$Command"

    return $output
}


<#
    .SYNOPSIS
 
    NON-PUBLIC Validates if the assembly is loaded into the current PSSession
 
    .DESCRIPTION
 
    Checkes if the specified DLL is already loaded.
 
    .PARAMETER AssemblyFile
 
    DLL file to check if it is loaded
 
    .PARAMETER ExactVersion
 
    Specifies if we require an exact version match. If not, we will return true if a higher level version is loaded than the specified version
 
    .EXAMPLE
 
    Test-AssemblyLoaded -assemblyFile "C:\Sql\Microsoft.Sql.Smo.Dll"
 
    .OUTPUTS
 
    True if the assembly or higher version is loaded (if exactVersion = $false). Throws an exception if a down level version is found to be loaded (this can cause issues)
 
#>

function Test-AssemblyLoaded{
    [CmdletBinding()]
    param(
        [string] $AssemblyFile,
        [bool] $ExactVersion = $false

    )

    if(!(Test-Path $AssemblyFile)){
        Write-DosMessage -Level "Error" -Message "Can't find $AssemblyFile"
    }

    [System.Reflection.AssemblyName] $targetName = [System.Reflection.AssemblyName]::GetAssemblyName($AssemblyFile)

    Write-DosMessage -Level "Verbose" -Message "Assembly info $targetName for $AssemblyFile"

    [Array] $loadedAssemblies = [AppDomain]::CurrentDomain.GetAssemblies()

    foreach($loadedAssembly in $loadedAssemblies){
        [System.Reflection.AssemblyName] $loadedName = $loadedAssembly.GetName()
        #Have to do deep comparison - Equals and -eq just compare references
        if(($loadedName.Name -eq $targetName.Name) -and ($loadedName.Version -eq $targetName.Version)){
            Write-DosMessage -Level "Verbose" -Message "Exact assembly $($loadedName.Name) already loaded"
            return $true
        }

        if(!$ExactVersion){
            if($loadedName.Name -eq $targetName.Name){
                if($loadedName.Version -ge $targetName.Version){
                    Write-DosMessage -Level "Verbose" -Message "Found assembly $($loadedName.Name) with version $($loadedName.Version) which is greater or equal to targeted version $($targetName.Version)"
                    return $true
                }
                else{
                    Write-DosMessage -Level "Error" -Message "Older assembly version loaded than $($targetName.Name) with specified version $($targetName.Version)"
                }
            }
        }
    }

    return $false
}



<#
    .SYNOPSIS
 
    Tests if the powershell session is running with elevated permissions.
 
    .DESCRIPTION
 
    Tests if the powershell session is running with elevated permissions.
#>


function Test-ElevatedPermission {
    $elevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
    if (-not $elevated ) {
        Write-DosMessage -Level "Fatal" -Message "This procedure requires elevated permissions."
    }
}


<#
    .SYNOPSIS
 
    Checks for a specific version of an installed application.
 
    .DESCRIPTION
 
    Checks the windows registry for an installed application has the exact version specified.
 
    .PARAMETER appName
 
    Application name pattern. The function uses "like" to match against the DisplayName in the registry.
 
    .PARAMETER supportedVersion
 
    The specific version to check. This is a string paramreter and should match the registered version exactly.
 
    .EXAMPLE
 
    Test-PrerequisiteExact -appName "Dot Net Runtime" -supportedVersion "1.2.3.4"
 
    .OUTPUTS
 
    boolean. Ture if and only if the the application us installed at the exact version. A newer dersion will return false.
#>


function Test-PrerequisiteExact {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        $appName,         
        $supportedVersion
    )
    $installedAppResults = Get-InstalledApps | Where-Object {$_.DisplayName -like $appName}
    if($null -eq $installedAppResults){
        return $false;
    }

    if($null -eq $supportedVersion)
    {
        return $true;
    }

    $supportedVersionAsSystemVersion = [System.Version]$supportedVersion

    Foreach($version in $installedAppResults)
    {
        $installedVersion = [System.Version]$version.DisplayVersion
        if($installedVersion -eq $supportedVersionAsSystemVersion)
        {
            return $true;
        }
    }
    return $false;
}


<#
    .SYNOPSIS
 
    Waits for the application pool to enter the specified state
 
    .DESCRIPTION
 
    Waits for the application pool to enter the specified state. Needed to allow files to be overwritten during upgrade
 
    .PARAMETER AppPoolName
 
    App pool to wait for state change on
 
    .PARAMETER AppPoolState
 
    State to wait for the app pool to enter. Current allowed values of "Started" and "Stopped"
 
    .PARAMETER TimeOut
 
    Amount of time to wait before throwing an error. Default is 2 minutes (240 seconds)
 
    .EXAMPLE
 
    Wait-AppPoolStateChange -AppPoolName "Identity" -AppPoolState "Started"
#>


function Wait-AppPoolState{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateLength(1,64)]
        [ValidateScript({
            if ($_ -match '[^a-zA-Z0-9]') {
                Write-DosMessage -Level "Fatal" -Message "$_ must only contain alphanumeric values. Please remove special characters."
            }
            else {
                $true
            }
        })]
        [string] $AppPoolName,
        [Parameter(Mandatory=$true)]
        [ValidateSet("Started", "Stopped")]
        [string] $AppPoolState,
        [int] $TimeOut = 240
    )

    Test-ElevatedPermission

    Import-Module WebAdministration

    $currentState = Get-WebAppPoolState -Name $AppPoolName
    Write-DosMessage -Level "Information" -Message "Waiting for app pool '$AppPoolName' to enter the '$AppPoolState' state"
    $loopTimes = 0
    while($currentState.Value -ne $AppPoolState) {
        Write-DosMessage -Level "Debug" -Message "Waiting 1 second"
        Start-Sleep 1
        $currentState = Get-WebAppPoolState -Name $AppPoolName
        if($loopTimes -ge $TimeOut) {
            Write-DosMessage -Level "Error" -Message "Timed out waiting for $AppPoolName to enter state $AppPoolState - timeout $TimeOut seconds"
            return
        }
        $loopTimes++
    }

    Write-DosMessage -Level "Information" -Message "App Pool $AppPoolName successfully entered state $AppPoolState"
}


<#
    .SYNOPSIS
 
    Adds the target assembly - essentially loads the assembly
 
    .DESCRIPTION
 
    Attempts to load the specified DLL files (assemblies). Will attempt to resolve from a couple of common paths during an attempt to load
 
    .PARAMETER Assemblies
 
    DLL files to attempt to load
 
    .OUTPUTS
 
    $true if the specified assemblies are already loaded, $false if the assemblies aren't loaded due to not being found or other issues.
 
    .EXAMPLE
 
    Add-Assembly -Assemblies "Microsoft.Sql.Smo.Dll"
 
#>

function Add-Assembly {
    [CmdletBinding()]
    param(
        [Object[]] $Assemblies,
        [string] $AssemblyFilter = "*.dll"
    )

    if(($null -eq $Assemblies) -or ([String]::IsNullOrEmpty($Assemblies[0]))){
        Write-DosMessage -Level "Error" -Message "Must specify one or more assemblies to load"
        return $false
    }

    foreach($assemblyFilePath in $Assemblies){

        Write-DosMessage -Level "Verbose" -Message "Attempting to load assembly $assemblyFilePath"
    
        if([String]::IsNullOrEmpty($assemblyFilePath)){
            Write-DosMessage -Level "Error" -Message "Specified assembly file path is null or empty"
            return $false
        }

        [string] $targetAssemblyFilePath = $assemblyFilePath
        
        if(!(Test-Path -Path $targetAssemblyFilePath)){
            Write-DosMessage -Level "Verbose" -Message "Unable to resolve $assemblyFilePath, searching common folder"
            $targetAssemblyFilePath = Resolve-CommonFilePath -AssemblyFileName $targetAssemblyFilePath
        }

        if([String]::IsNullOrEmpty($targetAssemblyFilePath)){
            Write-DosMessage -Level "Error" -Message "Unable to find specified assembly $assemblyFilePath"
            return $false
        }

        Write-DosMessage -Level "Verbose" -Message "Found $targetAssemblyFilePath for specified file $assemblyFilePath"
        
        if(!(Test-AssemblyLoaded -assemblyFile $targetAssemblyFilePath)){

            try{
                Write-DosMessage -Level "Verbose" -Message "Attempting to load $targetAssemblyFilePath"
                Add-Type -Path $targetAssemblyFilePath
            }
            catch{
                Write-DosMessage -Level "Error" -Message "Unable to load. Exception: $($_.Exception)"
            }
        
        }
    }

    Write-DosTelemetry -Message "Add-Assembly called."
}


function Add-IISUrlRewriteRule {
    <#
        .SYNOPSIS
        Add URL rewrite rule
 
        .DESCRIPTION
        Adds a given URL rewrite rule to IIS with a Redirect action. The IIS URL Rewrite extension must be installed in
        order to use this function.
 
        .PARAMETER IISWebSite
        The IIS site from which to remove the application. Defaults to "Default Web Site"
 
        .PARAMETER RuleName
        A unique name for the rule.
 
        .PARAMETER MatchUrl
        A regular expression that specifies the URL match pattern.
 
        .PARAMETER RedirectUrl
        The URL in which to redirect on a match.
 
        .PARAMETER UseRewrite
        Use a Rewrite action instead of a Redirect. Defaults to $false.
 
        .PARAMETER StopProcessing
        Enable the StopProcessing flag. When the rule action is performed (i.e. the rule matched) and the StopProcessing flag is turned on,
        it means that no more subsequent rules will be processed and the request will be passed to the IIS request pipeline.
        Defaults to $true.
 
        .EXAMPLE
        Add-IISUrlRewriteRule -RuleName "Atlas4-Atlas-Redirect" -MatchUrl "^Atlas4(.*)" -RedirectUrl "/Atlas" -UseRewrite $false -StopProcessing $true -IISWebSite "Default Web Site"
 
        .NOTES
        Rules are always appended to the list of existing rules, which are evaluated in order by IIS.
        Rules may be viewed from within the "URL Rewrite" feature within IIS Manager.
 
        Refer to the "URL Rewrite Module Configuration Reference" at
        https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/url-rewrite-module-configuration-reference
        for further details.
    #>


    Param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $RuleName,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $MatchUrl,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $RedirectUrl,
        [bool] $UseRewrite = $false,
        [bool] $StopProcessing = $true,
        [string] $IISWebSite = "Default Web Site"
    )
    Import-Module WebAdministration -Force
    $iisPath = "IIS:\Sites\$($IISWebSite)"
    $ruleFilter = "/system.webserver/rewrite/rules/rule[@name='$RuleName']"
    $rulesXPath = "/system.webserver/rewrite/rules"

    $exists = Get-WebConfigurationProperty -PSPath $iisPath -Filter $ruleFilter -Name *
    if ($exists) {
        Write-DosMessage -Level "Information" -Message "Removing existing URL Rewrite rule '$ruleFilter'."
        Clear-WebConfiguration -PSPath $iisPath -Filter $ruleFilter
    }

    $actionType = "Redirect"
    if ($UseRewrite) {
        $actionType = "Rewrite"
    }

    Write-DosMessage -Level "Information" -Message "Adding URL Rewrite rule '$RuleName'."
    Add-WebConfigurationProperty -PSPath $iisPath -Filter $rulesXPath -Name "." -value @{ name = $RuleName; patternSyntax = "ECMAScript"; stopProcessing = $StopProcessing }
    Set-WebConfigurationProperty -PSPath $iisPath -Filter "$ruleFilter" -Name "match" -value @{ url = $MatchUrl }
    Set-WebConfigurationProperty -PSPath $iisPath -Filter "$ruleFilter" -Name "action" -value @{ type = $actionType; url = $RedirectUrl; appendQueryString = "false"; }
}



<#
.SYNOPSIS
    Asserts if an application install meets upgrade version and dependency version requirements.
.DESCRIPTION
    Returns true if application upgrade version is greater than the currently installed version and all dependencies meet SemVer requirements. Returns false otherwise.
.PARAMETER DiscoveryServiceUrl
    The parameter DiscoveryServiceUrl is used to define the Discovery Service URL.
.PARAMETER DependencyManifestPath
    The parameter DependencyManifestPath is used to define the location of the dependency manifest file for the service/app being installed.
 
    dependency.manifest example
    {
        "$schema": "./InstallReadinessTool.schema.json",
        "manifestName": "Analytics Manifest",
        "operationMode": "validate",
        "readinessChecks": [
            {
                "name": "Authorization Service Dependency",
                "checkType": "dependentService",
                "serviceName": "AuthorizationService",
                "serviceVersion": "1",
                "serviceBuildVersion": "^1.7"
            },
            {
                "name": "Identity Service Dependency",
                "checkType": "dependentService",
                "serviceName": "IdentityService",
                "serviceVersion": "1",
                "serviceBuildVersion": "^1.7"
            }
        ]
    }
.PARAMETER ServiceName
    The parameter ServiceName is used to define the service name to lookup in Discovery Service.
.PARAMETER ServiceVersion
    The parameter ServiceVersion is used to define the service version to lookup in Discovery Service.
.PARAMETER InstallBuildNumber
    The parameter InstallBuildNumber is used to define the installer build number.
.EXAMPLE
    $vars = @{
        DiscoveryServiceUrl = "https://test/DiscoveryService/v1"
        DependencyManifestPath = "."
        ServiceName = "TestService"
        ServiceVersion = "1"
        InstallBuildNumber = "1.2.3"
    }
 
    Assert-DosValidUpgrade @vars
 
.EXAMPLE
    Assert-DosValidUpgrade -DiscoveryServiceUrl "https://test/DiscoveryService/v1" -DependencyManifestPath "." -ServiceName "TestService" -ServiceVersion "1" -InstallBuildNumber "1.2.3"
#>


function Assert-DosValidUpgrade {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]$DiscoveryServiceUrl,
        [Parameter(Mandatory=$true)]
        [ValidateScript({Test-Path $_})]
        [string]$DependencyManifestPath,
        [Parameter(Mandatory=$true)]
        [string]$ServiceName,
        [Parameter(Mandatory=$true)]
        [string]$ServiceVersion,
        [Parameter(Mandatory=$true)]
        [string]$InstallBuildNumber
    )

    $currentDependencies = Get-CurrentDependencyList -Path $DependencyManifestPath

    $service = Get-DosService -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $ServiceName -ServiceVersion $ServiceVersion -Exists

    if ($service.Exists) {
        $currentBuildNumber = $service.BuildNumber
        if (!(Assert-BuildNumberGreater -CurrentBuildNumber $currentBuildNumber -InstallBuildNumber $InstallBuildNumber)) {
            Write-DosMessage -Level "Warning" -Message "Installer version of $($ServiceName) $($InstallBuildNumber) is not greater than current version $($currentBuildNumber)"
            return $false
        }
    }
    else {
        Write-DosMessage -Level "Warning" -Message "$($ServiceName) not found in Discovery Service. Assuming this is a new install and proceeding with dependency checks."
    }

    $discoveryServiceEntries = Get-ServiceList -CurrentDependencies $currentDependencies.readinessChecks -DiscoveryServiceUrl $DiscoveryServiceUrl

    if (!(Assert-DependencySemVerRequirementsMet -ServiceName $ServiceName -DiscoveryServiceEntries $discoveryServiceEntries -CurrentDependencies $currentDependencies.readinessChecks)) {
        return $false
    }

    return $true 
}

function Assert-BuildNumberGreater {
    [CmdletBinding()]
    param (
        [string]$CurrentBuildNumber,
        [string]$InstallBuildNumber
    )

    Write-DosMessage -Level "Information" -Message "Checking if upgrade build number is greater than currently installed package"

    return ($InstallBuildNumber.split('.') -join '') -gt ($CurrentBuildNumber.split('.') -join '')
}

function Get-CurrentDependencyList {
    [CmdletBinding()]
    param (
        [string]$Path
    )

    return Get-Content -Path $Path | ConvertFrom-Json
}

function Get-ServiceList {
    [CmdletBinding()]
    param (
        [array]$CurrentDependencies,
        [string]$DiscoveryServiceUrl
    )
    $discoveryResponses = @()
    foreach ($dependency in $CurrentDependencies) {
        $discoveryResponse = Get-DosService -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $dependency.serviceName -ServiceVersion $dependency.serviceVersion
        $discoveryResponses += $discoveryResponse
    }
    return $discoveryResponses
}



function Confirm-DosConfiguration {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [Hashtable] $config,
        [Parameter(Mandatory = $true)]
        [Hashtable] $checkList
    )
    begin {
        $results = @();
        function Invoke-Check {
            [CmdletBinding()]
            param (
                [Parameter(Position = 0, Mandatory = $true)]
                [string] $name,
                [Parameter(Position = 1)]
                [object[]] $checkList
            )
            $result = @();
            $splat = @{
                name  = $name
                type  = $config["_type_"][$name]
                value = $config[$name]
            }
            switch -Wildcard ($checkList) {
                "isNotNull" { $result += Confirm-IsNotNull @splat }
                "isValidPath" { $result += Confirm-IsValidPath @splat }
                "isValidDir" { $result += Confirm-IsValidDir @splat }
                "isBoolean" { $result += Confirm-IsBoolean @splat }
                "isValidConnection" { $result += Confirm-IsValidConnection @splat }                
                "isValidEndpoint" { $result += Confirm-IsValidEndpoint @splat }
                "isValidValue=(*)" {
                    $list = $checkList.Split("'*',", [System.StringSplitOptions]::RemoveEmptyEntries)
                    [string[]]$list = $list[(1..($list.Length - 2))]
                    $splat.Add('validateSet', $list)
                    $result += Confirm-IsValidValue @splat
                }
            }

            return $result
        }        
    }
    process {
        foreach ($item in $checkList.Keys) {
            if ($config.ContainsKey($item)) {
                $results += Invoke-Check $item $checkList[$item]
            }
            else {
                Write-DosMessage -Level "Warning" -Message "Unable to find $item as a configuration. Please remove from checklist."
            }
        }
    }
    end {
        $errors = $results | Where-Object errorFlag -eq 1
        $warnings = $results | Where-Object errorFlag -eq -1
        $success = $results | Where-Object errorFlag -eq 0

        $errorsCnt = ($errors | Measure-Object).Count
        $warningsCnt = ($warnings | Measure-Object).Count
        $successCnt = ($success | Measure-Object).Count

        if ($warnings) {
            $msg = ($warnings | Sort-Object type, name | Format-Table name, type, check, @{Label = "result"; Expression = {"warning"}}, message -Wrap)
            Write-DosMessage -Level "Warning" -Message "WARNINGS: $warningsCnt >>>`n$(($msg | Out-String).trim())"
        }
        if ($errors) {
            if ($errors | Where-Object type -eq "store") {
                $msg = ($errors | Where-Object type -eq "store" | Sort-Object type, name | Format-Table name, type, check, @{Label = "result"; Expression = {"failed"}}, message -Wrap)
                $errorsCnt = ($errors | Where-Object type -eq "store" | Measure-Object).Count
            }
            else {
                $msg = ($errors | Sort-Object type, name | Format-Table name, type, check, @{Label = "result"; Expression = {"failed"}}, message -Wrap)
            }
            Write-DosMessage -Level "Fatal" -Message "ERRORS: $errorsCnt >>>`n$(($msg | Out-String).trim())"
        }

        Write-DosMessage -Level "Information" -Message "Configuration checks summary - Success: $successCnt, Warnings: $warningsCnt, Errors: $errorsCnt"
    }
}



<#
    .SYNOPSIS
 
    Checks if the credential provided is valid
 
    .DESCRIPTION
 
    Checks that the provided credential (username/password pairing) is valid and returns you and object with the validity response ($returnObject.isValid) and the validated credential ($returnObject.credential).
    Note: If the isValid property of the return object is $false, the credential property will be invalid and should be handled accordingly.
 
    .PARAMETER Credential
 
    Credential object used to check if the username and password combination are correct
 
    .PARAMETER promptOnInvalid
 
    Switch - If provided and credential is invalid, will prompt for the correct password.
 
    .EXAMPLE
 
    $validationResponse = (Confirm-DosCredential -Credential $credential).isValid
    $returnedCredential = (Confirm-DosCredential -Credential $credential -PromptOnInvalid).credential
#>

function Confirm-DosCredential {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [PSCredential] $Credential,
        [switch] $PromptOnInvalid
    )
    $iisUser = $Credential.UserName
    $isValid = Assert-DosCredential($Credential)
    if(!$isValid){
        if ($PromptOnInvalid) {
            Write-DosMessage -Level "Warning" -Message "Incorrect credentials for $iisUser"
            Write-DosMessage -Level "Verbose" -Message "PromptOnInvalid parameter provided. Please input appropriate credential information."
            $count = 0
            do {
                $count++
                $Credential = Read-DosCredential -UserName $Credential.UserName
                $isValid = Assert-DosCredential($Credential)
                if(!$isValid){
                    Write-DosMessage -Level "Warning" -Message "Credential you provide is incorrect for user: $iisUser. Please try again."
                }
            }until ($isValid -or ($count -ge 3))
            if (!$isValid) {
                Write-DosMessage -Level "Error" -Message "Maximum number of credential validation attempts reached for user: $iisUser"
                Write-DosMessage -Level "Verbose" -Message "Credential is invalid for $iisUser. Returning false."
            }
        }else {
            Write-DosMessage -Level "Error" -Message "Incorrect credentials provided for user: $iisUser"
        }
    }
    
    $credentialValidationResult = @{ isValid = $isValid; credential = $Credential }

    return $credentialValidationResult
}


    
<#
    .SYNOPSIS
 
    Helper Function: Checks if the credential provided is valid
 
    .DESCRIPTION
 
    Uses System.DirectoryServices.AccountManagement.ContextType to check that the credential provided is valid
 
    .PARAMETER Credential
 
    Credential object used to check if the username and password combination are correct
 
    .EXAMPLE
 
    Assert-DosCredential -Credential $credential
#>


function Assert-DosCredential {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [PSCredential] $Credential
    )
    
    Write-DosMessage -Level "Information" -Message "Checking credential validity."

    $principalContext = "domain"
    $contextName = $Credential.GetNetworkCredential().Domain
    
    [System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.AccountManagement") | Out-Null
    
    $pc = New-Object System.DirectoryServices.AccountManagement.PrincipalContext($principalContext, $contextName)

    $isValid = $pc.ValidateCredentials($Credential.GetNetworkCredential().UserName, $Credential.GetNetworkCredential().Password, [System.DirectoryServices.AccountManagement.ContextOptions]::Negotiate)
    return $isValid
}


<#
    .SYNOPSIS
 
    Helper Function: Prompts for username and password for a credential object
 
    .DESCRIPTION
 
    Prompts for username and password and returns a credential object
 
    .PARAMETER Credential
 
    Credential object used to check if the username and password combination are correct
 
    .EXAMPLE
 
    Prompt-DosCredential -UserName 'testaccount' -Password 'password'
#>

function Read-DosCredential {
    param (
        [string] $UserName,
        [securestring] $UserPassword 
    )

    if (!$UserName){
        $UserName = Read-Host "Enter the domain\username to use for the credential"
    }
    if (!$UserPassword) {
        $UserPassword = Read-Host "Enter the password for $UserName" -AsSecureString
    }

    Write-DosMessage -Level "Information" -Message "Creating credential from inputs provided."

    $Credential = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $UserName, $UserPassword
    return $Credential
}


<#
    .SYNOPSIS
 
    Extracts the target zip file to the target directory.
 
    .DESCRIPTION
 
    Either uses Expand-Archive in PS 5.0+ or [System.IO.Compression.ZipFile] and associated calls in PS 4.0
 
    .PARAMETER ArchiveFile
 
    Target zip file to extract
 
    .PARAMETER DestinationPath
 
    Destination directory to extract the zip file into.
 
    .PARAMETER OverWrite
 
    Forces overwriting existing files. If not specified and similar files already exists in the target directory, errors will be displayed for each file NOT overwritten
 
    .EXAMPLE
 
    Expand-DosArchive -ArchiveFile "x.zip" -DestinationPath "c:\inetpub\wwwroot\x" -OverWrite
 
#>

function Expand-DosArchive{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Error" -Message "ArchiveFile $_ does not exist. Please enter valid path."
            }
            else {
                $true
            }
        })]
        [string] $ArchiveFile,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $DestinationPath,
        [switch] $OverWrite
    )

    Expand-Archive -Path $ArchiveFile -DestinationPath $DestinationPath -Force:$OverWrite.IsPresent
    Write-DosTelemetry -Message "Expand-DosArchive called."
}



<#
    .SYNOPSIS
 
    Obtains the path to the root of the install directory.
 
    .DESCRIPTION
 
    Traverses up the path until it finds the root of the install directory. E.g. C:\install\DosInstaller2016_20.2.2019.11\DosInstaller2016
 
    .EXAMPLE
 
    Get-DosBaseInstallerPath
 
#>

#replaces get-baseinstallerpath

function Get-DosBaseInstallerPath {
    $parentPath = Get-Location
    $installerPath = $parentPath
    $rootDrivePath = (Split-Path -Path $installerPath -Qualifier) + "\"

    do {
        if(Get-ChildItem $installerPath | Where-Object {$_.Name -eq "CatalystSetup.exe"})
        {
            break
        } else
        {
            $installerPath = Split-Path -Path $installerPath -Parent
        }
        if($installerPath -eq $rootDrivePath) {
            Write-DosMessage -Level "Error" -Message "Could not resolve the base path to the installer starting from $parentPath."
        }
    } 
    until ($installerPath -eq $rootDrivePath) 

    return $installerPath
}


<#
    .SYNOPSIS
        Fixes default PowerShell url dispatch decoding
     
    .DESCRIPTION
        PowerShell by default has a url parser setting which decodes a backslash before the request dispatch.
        This has caused problems when attempting to send web requests with urls that contain backslashes in them.
        This function fixes that issue and returns a url that can be passed to Invoke-WebRequest or Invoke-RestMethod.
     
    .PARAMETER url
        The URL string of a given web request.
     
    .EXAMPLE
        PS C:\> Get-DosCleanUri -url "http://localhost/DOMAIN\My User"
     
    .NOTES
        This function was created by following a helpful article found on Stack Overflow
        Topic: "Percent-encoded slash (“/”) is decoded before the request dispatch"
        Article: https://stackoverflow.com/a/30927141
#>

function Get-DosCleanUri {
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true,
            Position = 0)]
        [string]$url
    )
    
    try {
        $escapedUrl = Format-UriString -Url $url
        $escapedUrl.PathAndQuery | Out-Null
        $m_Flags = Get-MFlagsFieldDotNet
        [uint64]$flags = Get-MFlagsValue -Url $escapedUrl -mFlags $m_Flags
        Set-MFlagsValue -Url $escapedUrl -mFlags $m_Flags -Flags $flags
        Write-DosMessage -Level "Information" -Message "Url cleaned to prevent decoding on dispatch ($($escapedUrl.OriginalString))."
    }
    catch {
        Write-DosMessage -Level "Error" -Message "An error occurred while attempting to clean the url ($($escapedUrl.OriginalString)). Exception: $($_.Exception)."
    }

    return $escapedUrl
}

function Format-UriString {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string]$Url
    )

    [Uri]$escapedUrl = [System.Uri]::EscapeUriString($Url)

    return $escapedUrl
}

function Get-MFlagsFieldDotNet {
    [CmdletBinding()]
    $m_Flags = [Uri].GetField("m_Flags", $([Reflection.BindingFlags]::Instance -bor [Reflection.BindingFlags]::NonPublic))
    
    return $m_Flags
}

function Get-MFlagsValue {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [Uri]$Url,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        $mFlags
    )
    [uint64]$flags = $mFlags.GetValue($Url)
    
    return $flags
}

function Set-MFlagsValue {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [Uri]$Url,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        $mFlags,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        $Flags
    )
    $mFlags.SetValue($Url, $($Flags -bxor 0x30))
}


<#
    .SYNOPSIS
 
    Gets configuration values from a configuration store of a certain type and format.
 
    .DESCRIPTION
 
    Given a user has a valid Configuration Store object, when the user asks for all values for a particular scope, values are returned in a hash table.
    If user provides an invalid Configuration Store object, warning messages will be displayed & logged, and Get-DosConfigValues will return $null.
 
    .PARAMETER ConfigStore
 
    Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}.
    Current iteration only supports XML Files.
 
    .PARAMETER Scope
 
    The configuration values you wish to return. Passing in an application specific scope will return values within the configuration that are contained in that application scope.
    Additionally, users can pass in a "common" scope to return values contained in the common scope.
 
    .EXAMPLE
 
    Get-DosConfigScopeValues -ConfigStore $configHashtable -Scope "common"
    Get-DosConfigScopeValues -ConfigStore $configHashtable -Scope "terminology"
#>


function Get-DosConfigValues {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Values should be plural.")]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $ConfigStore,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $Scope
    )

    $validConfigStore = Confirm-ConfigStore -ConfigStore $ConfigStore

    if (!$validConfigStore) {
        Write-DosMessage -Level "Warning" -Message "ConfigStore object is invalid. No configuration values will be returned."
        return $null
    }

    Write-DosMessage -Level "Verbose" -Message "ConfigStore object is valid."

    Write-DosMessage -Level "Information" -Message "Retrieving '$Scope' scoped configuration values from '$($ConfigStore.Path)'"
    $installConfig = Get-InstallationSettings -ConfigSection $Scope -InstallConfigPath $ConfigStore.Path

    Write-DosTelemetry -Message "Get-DosConfigValues called with the following parameters. Scope: $Scope. ConfigStore: $($ConfigStore | Out-String)"

    return $installConfig
}


<#
    .SYNOPSIS
 
    Returns a service url from Discovery Service.
 
    .DESCRIPTION
 
    Given a valid Discovery Service url and valid service name, Get-DosServiceUrl will return a service url string.
    If a valid service version is provided, Get-DosServiceUrl will reutrn the service url string for the specified version.
    If a valid credential is provided, Get-DosServiceUrl will query Discovery Service with the credential provided, else it will use the local powershell session user.
 
    .PARAMETER DiscoveryServiceUrl
 
    REQUIRED
    Accepts a valid (not null or empty) Discovery Service Url string.
 
    .PARAMETER ServiceName
 
    REQUIRED
    Accepts a valid (not null or empty) service name that will be used to query Discovery Service
 
    .PARAMETER ServiceVersion
 
    REQUIRED
    Accepts a valid (not null or empty) service name that will be used to query Discovery Service
 
    .PARAMETER Credential
 
    Accepts a valid (not null or empty) service name that will be used to query Discovery Service
 
    .PARAMETER Exists
 
    Switch - If provided, appends an 'Exists' property to the output object and does not write a fatal message if the ServiceName is not found.
 
    .EXAMPLE
 
    Get-DosService -DiscoveryServiceUrl "https://server/DiscoveryService/v1" -ServiceName "AnalyticsService" -ServiceVersion 1
 
    $credential = Get-Credential
    Get-DosService -DiscoveryServiceUrl "https://server/DiscoveryService/v1" -ServiceName "AnalyticsService" -ServiceVersion 1 -Credential $credential
 
    Get-DosService -DiscoveryServiceUrl "https://server/DiscoveryService/v1" -ServiceName "AnalyticsService" -ServiceVersion 1 -Exists
#>

function Get-DosService {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $DiscoveryServiceUrl,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $ServiceName,
        [int] $ServiceVersion,
        [pscredential] $Credential,
        [switch]$Exists
    )

    if ($ServiceVersion) {
        Write-DosMessage -Level "Verbose" -Message "Service Version provided. Request formed with specified version."
        $discoveryRequest = "$DiscoveryServiceUrl/Services(ServiceName='$ServiceName',Version=$ServiceVersion)"
    }
    else {
        Write-DosMessage -Level "Verbose" -Message "Service Version was not provided. Request formed without specified version."
        $discoveryRequest = "$DiscoveryServiceUrl/Services?`$filter=ServiceName eq $ServiceName"
    }
    
    $discoveryResponse = @{ }

    if ($null -ne $Credential) {
        try {
            Write-DosMessage -Level "Information" -Message "Attempting to retrieve Discovery Service object from Discovery Service at '$discoveryRequest' using '$($Credential.UserName)' credential."
            $discoveryResponse = Invoke-RestMethod -Method Get -Uri $discoveryRequest -Credential $Credential
            Write-DosMessage -Level "Information" -Message "Successfully retrieved Discovery Service object from Discovery Service."
        }
        catch {
            $statusCode = $_.Exception.Response.StatusCode.value__
            if ($statusCode -eq "400" -and $Exists.IsPresent) {
                $discoveryResponse.Exists = $false
                return $discoveryResponse
            }
            Write-DosMessage -Level "Fatal" -Message "Error retrieving service registration with '$($Credential.UserName)' for '$ServiceName'. Please confirm Discovery Service installation and/or credential permissions. Exception: $($_.Exception)."
        }
    }
    else {
        try {
            Write-DosMessage -Level "Information" -Message "Attempting to retrieve Discovery Service object from Discovery Service at '$discoveryRequest' using '$env:UserName' credential."
            $discoveryResponse = Invoke-RestMethod -Method Get -Uri $discoveryRequest -UseDefaultCredentials
            Write-DosMessage -Level "Information" -Message "Successfully retrieved Discovery Service object from Discovery Service."
        }
        catch {
            $statusCode = $_.Exception.Response.StatusCode.value__
            if ($statusCode -eq "400" -and $Exists.IsPresent) {
                $discoveryResponse.Exists = $false
                return $discoveryResponse
            }
            Write-DosMessage -Level "Fatal" -Message "Error retrieving service registration with '$($Credential.UserName)' for '$ServiceName'. Please confirm Discovery Service installation and/or credential permissions. Exception: $($_.Exception)."
        }
    }

    if ($Exists.IsPresent) {
        $discoveryResponse | Add-Member -NotePropertyName "Exists" -NotePropertyValue $true
    }
    
    return $discoveryResponse
}


function Get-DosServiceUrl {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $DiscoveryServiceUrl,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $ServiceName,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [int] $ServiceVersion,
        [pscredential] $Credential
    )

    if ($Credential) {
        $discoveryResponse = Get-DosService -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $ServiceName -ServiceVersion $ServiceVersion -Credential $Credential
    }
    else {
        $discoveryResponse = Get-DosService -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $ServiceName -ServiceVersion $ServiceVersion
    }

    Write-DosMessage -Level "Verbose" -Message "Pulling 'ServiceUrl' from the Discovery Service response."
    $serviceUrl = Get-ServiceUrlString -DiscoveryServiceResponse $discoveryResponse

    Write-DosMessage -Level "Information" -Message "Checking if a value was returned for '$ServiceName version $ServiceVersion'."
    if ([string]::IsNullOrWhiteSpace($serviceUrl)) {
        Write-DosMessage -Level "Fatal" -Message "The service $ServiceName is not registered with the Discovery service. Make sure that '$ServiceName $ServiceVersion' is registered with Discovery Service. Exception: $($_.Exception)"
    }

    Write-DosMessage -Level "Information" -Message "Returning url '$serviceUrl' for '$ServiceName' service."

    return $serviceUrl
}

function Get-ServiceUrlString {
    [CmdletBinding()]
    param (
        $DiscoveryServiceResponse
    )

    $serviceUrl = $DiscoveryServiceResponse.ServiceUrl

    return $serviceUrl
}


<#
    .SYNOPSIS
 
    Returns the physical root of the specified web site
 
    .DESCRIPTION
 
    Uses the WebAdministration powershell module to locate the web site's physical root
 
    .PARAMETER WebSiteName
 
    Name of the IIS site - defaults to "Defautl Web Site"
 
    .EXAMPLE
 
    Get-IISWebSitePath -WebSiteName $IISWebSite
#>


function Get-IISWebSitePath{
    [CmdletBinding()]
    param(
        [string] $WebSiteName = "Default Web Site"
    )

    Import-Module WebAdministration

    Test-ElevatedPermission

    try{
        [Microsoft.IIs.PowerShell.Framework.ConfigurationElement] $webSite = Get-Item "IIS:\Sites\$WebSiteName"
        return [System.Environment]::ExpandEnvironmentVariables($webSite.PhysicalPath)
    }
    catch{
        Write-DosMessage -Level Error -Message "Unable to get inforomation for $WebSiteName : error $($_.Exception)"
    }
}


<#
    .SYNOPSIS
 
    Downloads the specified Uri using a WebRequest to the specified OutFile
 
    .DESCRIPTION
 
    Wraps the PowerShell native Invoke-WebRequest and performs the request without the default progress bar behavior.
 
    .PARAMETER Uri
 
    Specifies the Uniform Resource Identifier (URI) of the Internet resource to which the web request is sent. Enter a URI.
    This parameter supports HTTP, HTTPS, FTP, and FILE values.
 
    This parameter is required.
 
    .PARAMETER OutFile
 
    Specifies the output file for which this cmdlet saves the response body. Enter a path and file name. If you omit the path,
    the default is the current location.
 
    .PARAMETER NoCache
 
    When supplied will provide the "Cache-Control" header set to "no-cache" to the underlying WebRequest.
 
    .EXAMPLE
 
    Download-WebRequest -Uri http://some.valid.uri/file -OutFile theFile.txt
#>

function Get-WebRequestDownload {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string] $Uri,

        [Parameter(Mandatory=$true)]
        [string] $OutFile,

        [switch] $NoCache = $false
    )

    $originalProgressPreference = $progressPreference
    try {
        $headers = @{}
        if ($NoCache) {
            $headers.Add("Cache-Control", "no-cache")
        }
        $progressPreference = 'silentlyContinue'
        Invoke-WebRequest -Uri $Uri -Headers $headers -OutFile $OutFile -UseBasicParsing
    } finally {
        $progressPreference = $originalProgressPreference
    }
}


<#
.SYNOPSIS
Installs SSIS project
 
.DESCRIPTION
Ensures SSIS catalog and folder exist, and installs SSIS project from specified ISPAC
 
.PARAMETER ProjectName
Name to deploy project under
 
.PARAMETER IspacPath
Path to ISPAC to deploy
 
.PARAMETER CatalogEncryptionKey
Key to use for encrypting catalog, if it must be created
 
.PARAMETER CatalogName
Name of SSIS catalog to contain project
 
.PARAMETER FolderName
Name of SSIS catalog folder to contain project
 
.PARAMETER ConnectionString
Connection string for connecting to SQL Server instance with Integration Services
 
.INPUTS
None. You cannot pipe objects to Install-DosIspac.
 
.OUTPUTS
None.
 
.EXAMPLE
PS> Install-DosIspac -ProjectName 'CatalystLoader' -IspacPath 'SetupContent/SSISLoader2016.ispac'
#>

function Install-DosIspac {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Passing through the -WhatIf and -Confirm to the private functions - this is a supported scenario")]
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType()]
    param(
        [parameter(Mandatory=$true)][string]$ProjectName,
        [parameter(Mandatory=$true)][string]$IspacPath,
        [parameter(Mandatory=$false)][string]$CatalogEncryptionKey,
        [parameter(Mandatory=$false)][string]$CatalogName = 'SSISDB',
        [parameter(Mandatory=$false)][string]$FolderName = 'Catalyst',
        [parameter(Mandatory=$false)][string]$ConnectionString = 'Data Source=localhost;Initial Catalog=EDWAdmin;Integrated Security=True')

    [Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Management.IntegrationServices")
    $integrationServices = Get-IntegrationServices -ConnectionString $ConnectionString

    Write-DosMessage -Level Debug -Message "Testing whether catalog $CatalogName exists"
    if ($integrationServices.Catalogs.Contains($CatalogName)) {
        Write-DosMessage -Level Information -Message "SSIS catalog already exists, skipping"
        $catalog = $integrationServices.Catalogs[$CatalogName]
    }
    else {
        Write-DosMessage -Level Information -Message "SSIS catalog does not exist, creating"

        if ([string]::IsNullOrEmpty($CatalogEncryptionKey)) {
            Write-DosMessage -Level Error -Message "Please provide a catalog encryption key using the CatalogEncryptionKey parameter so that a new catalog can be created." -ErrorAction Stop
        }

        $catalog = New-SsisCatalog -IntegrationServices $integrationServices -CatalogEncryptionKey $CatalogEncryptionKey -CatalogName $CatalogName -WhatIf:$PSBoundParameters.ContainsKey('WhatIf') -Confirm:$PSBoundParameters.ContainsKey('Confirm') 
        Write-DosMessage -Level Debug -Message "SSIS catalog created successfully"
    }

    Write-DosMessage -Level Debug -Message "Testing whether folder $FolderName exists"
    if ($catalog.Folders.Contains($FolderName)) {
        Write-DosMessage -Level Information -Message "SSIS catalog folder already exists"
        $folder = $catalog.Folders[$FolderName]
    }
    else {
        Write-DosMessage -Level Information -Message "SSIS catalog folder does not exist, creating"
        $folder = New-SsisFolder -SsisCatalog $catalog -FolderName $FolderName -WhatIf:$PSBoundParameters.ContainsKey('WhatIf') -Confirm:$PSBoundParameters.ContainsKey('Confirm')
        Write-DosMessage -Level Debug -Message "SSIS catalog folder created successfully"
    }

    Write-DosMessage -Level Information -Message "Deploying SSIS project $ProjectName from $IspacPath"
    New-SsisProject -SsisFolder $folder -ProjectName $ProjectName -IspacPath $IspacPath -WhatIf:$PSBoundParameters.ContainsKey('WhatIf') -Confirm:$PSBoundParameters.ContainsKey('Confirm')
    Write-DosMessage -Level Debug -Message "SSIS project $ProjectName deployed successfully"
}


<#
    .SYNOPSIS
 
    If needed, Installs the correct version of Dot Net Core
 
    .DESCRIPTION
 
    Checks for a specific version of Dot Net Core. If the version is not installed,download and install it.
 
    .PARAMETER version
 
    The string representation of the version. This should match the DisplayVersion for the object as installed in the windows registry.
 
    .PARAMETER downloadUrl
 
    The complete download URL for the exe installer that corresponds to the given version.
 
    .EXAMPLE
 
    Install-DotNetCoreIfNeeded -version "1.2.3.4" -downloadUrl "https://some.web.site/folder/installFill.exe"
#>

function Install-DotNetCoreIfNeeded {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $version,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $downloadUrl,

        [string] $filePattern = "*.NET Core*Windows Server Hosting*" 
    )

    if (Test-PrerequisiteExact $filePattern $version) {
        Write-DosMessage -Level "Information" -Message  ".NET Core Windows Server Hosting Bundle (v$version) installed and meets expectations."
        return 
    }

    try {
        Write-DosMessage -Level "Information" -Message "Windows Server Hosting Bundle version $version not installed...installing version $version"        
        Get-WebRequestDownload -Uri $downloadUrl -OutFile $env:Temp\bundle.exe
        Start-Process $env:Temp\bundle.exe -Wait -ArgumentList '/quiet /install /norestart'
        net stop was /y
        net start w3svc
    }
    catch {
        Write-DosMessage -Level "Fatal" -Message "Could not install .NET Windows Server Hosting bundle. Please install the hosting bundle before proceeding. $downloadUrl"
    }
    
    if (Test-PrerequisiteExact $filePattern $version) {
        Write-DosMessage -Level "Information" -Message "Windows Server Hosting Bundle installed version $version"        
    }
    else {
        Write-DosMessage -Level "Fatal" -Message "Error executing .NET Windows Server Hosting bundle. Please install the hosting bundle before proceeding. $downloadUrl"
    }

    try {
        Remove-Item $env:Temp\bundle.exe
    }
    catch {
        $e = $_.Exception
        Write-DosMessage -Level "Warning" -Message "Unable to remove temporary download file for server hosting bundle exe" 
        Write-DosMessage -Level "Warning" -Message  $e.Message
    }

}


<#
    .SYNOPSIS
 
    Imports/Installs a module required to run this module/function
 
    .DESCRIPTION
 
    Attempts to load a local module first - if the module isn't available it will attempt to download/install from PSGallery.
 
    .PARAMETER ModuleName
 
    Name of the module to install
 
    .PARAMETER Scope
 
    Scope used to install the module - default is CurrentUser
 
    .EXAMPLE
 
    Install-RequiredModule -ModuleName dbatools
#>

function Install-RequiredModule{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $ModuleName,
        [string] $Scope = "CurrentUser",
        [version] $RequiredVersion
    )

    Write-DosTelemetry -Message "Install-RequiredModule start"
    # Adding PSModule path becuse in some cases the path to user directory was not in the module system path
    $currentUserPSModulePath = "$home\Documents\WindowsPowerShell\Modules"
    $replaceCurrentUserModulePath = Add-ToPSModulePath -Path $currentUserPSModulePath

    #First scenario - required version of the module is already loaded
    $importedModule = Get-ModuleWorkAround -Name $ModuleName

    if($null -ne $importedModule){
        Write-DosMessage -Level "Information" -Message "Module '$ModuleName' already imported."
        
        if ($RequiredVersion) {
            Write-DosMessage -Level "Verbose" -Message "Checking if imported module '$ModuleName' matches required version '$RequiredVersion'."
            if ((Compare-ModuleVersion -ModuleToCompare $importedModule -RequiredVersion $RequiredVersion)) {
                Write-DosMessage -Level "Information" -Message "Confirming '$ModuleName' with version '$RequiredVersion' is loaded into session."
                
                if ($importedModule.Count -gt 1) {
                    Write-DosMessage -Level "Information" -Message "Multiple '$ModuleName' modules loaded in session. This should never happen?"
                    Remove-Module -Name $ModuleName
                    try {
                        Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Global -ErrorAction Stop
                    }
                    catch [System.Management.Automation.RuntimeException]{
                        Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." 
                    }
                    Write-DosMessage -Level "Information" -Message "Successfully imported '$ModuleName' with version '$RequiredVersion' into session."
                    Write-DosTelemetry -Message "Multiple modules found in session. Removed all and successfully imported '$ModuleName' with version '$RequiredVersion' into session."
                }
                else {
                    Write-DosMessage -Level "Information" -Message "Using '$ModuleName' that is currently loaded in the session."
                    Write-DosTelemetry -Message "Using '$ModuleName' that is currently loaded in the session."
                }

                # Removing user path from ps module to cover cases in it is not included in system path
                if ($replaceCurrentUserModulePath) {
                    Remove-FromPSModulePath -Path $currentUserPSModulePath
                }
                return
            }
            else {
                Write-DosMessage -Level "Information" -Message "Removing '$ModuleName' that does not match required version '$RequiredVersion'."
                
                Remove-Module -Name $ModuleName
            }
        }
        else {
            # Removing user path from ps module to cover cases in it is not included in system path
            if ($replaceCurrentUserModulePath) {
                Remove-FromPSModulePath -Path $currentUserPSModulePath
            }
            return
        }
    }

    Write-DosMessage -Level "Information" -Message "Did not find module '$ModuleName' loaded in session."
    
    # Second scenario is required version of the module is installed on the system
    Write-DosMessage -Level "Information" -Message "Checking if module '$ModuleName' is already installed."
    $installedModule = Get-ModuleWorkAround -Name $ModuleName -ListAvailable
        
    if($null -ne $installedModule){
        Write-DosMessage -Level "Information" -Message "Module '$ModuleName' already installed."
        if ($RequiredVersion) {
            Write-DosMessage -Level "Verbose" -Message "Checking if installed module '$ModuleName' matches required version '$RequiredVersion'."
            if ((Compare-ModuleVersion -ModuleToCompare $installedModule -RequiredVersion $RequiredVersion)) {
                Write-DosMessage -Level "Information" -Message "Importing '$ModuleName' with version '$RequiredVersion'."
                try {
                    Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Global -ErrorAction Stop
                }
                catch [System.Management.Automation.RuntimeException]{
                    Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." 
                }
                Write-DosTelemetry -Message "'$ModuleName' found on the system and successfully imported with version '$RequiredVersion'"

                if ($replaceCurrentUserModulePath) {
                    Remove-FromPSModulePath -Path $currentUserPSModulePath
                }
                return
            }                        
        }
        else {
            Write-DosMessage -Level "Information" -Message "Module '$ModuleName' is installed on system, attempting to import."
            try {
                Import-Module -Name $ModuleName -Global -ErrorAction Stop
            }
            catch [System.Management.Automation.RuntimeException]{
                Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." 
            }
            Write-DosTelemetry -Message "'$ModuleName' found on the system and successfully imported with version"

            if ($replaceCurrentUserModulePath) {
                Remove-FromPSModulePath -Path $currentUserPSModulePath
            }
            return
        }
    }

    Write-DosMessage -Level "Information" -Message "Did not find module '$ModuleName' installed on system."
    #Third scenario is module is installed from PSGallery
    Write-DosMessage -Level "Information" -Message "Attempting to fetch '$ModuleName'."

    $desiredRepo = "PSGallery"
    $isTrusted = Get-RepositoryTrust -RepositoryName $desiredRepo

    if (!($isTrusted)) {
        Write-DosMessage -Level "Information" -Message "'$desiredRepo' is not trusted. Toggling trust to download '$ModuleName'"
        Set-RepositoryTrust -RepositoryName $desiredRepo -Trust
    }

    #Error check here - also assume that PowerShellGet is loaded/available.
    try {
        if ($RequiredVersion) {
            Write-DosMessage -Level "Information" -Message "Module '$ModuleName' version '$RequiredVersion' being downloaded from PSGallery."
            Install-Module $ModuleName -RequiredVersion $RequiredVersion -Scope $scope
            Write-DosMessage -Level "Information" -Message "Successfully downloaded module '$ModuleName' with version '$RequiredVersion' from PSGallery."
            try {
                    Import-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Global -ErrorAction Stop
                }
                catch [System.Management.Automation.RuntimeException]{
                    Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." 
                }
            Write-DosMessage -Level "Information" -Message "Successfully imported module '$ModuleName' with version '$RequiredVersion' from PSGallery."
            Write-DosTelemetry -Message "Successfully imported module '$ModuleName' with version '$RequiredVersion' from PSGallery."
        }
        else {
            Write-DosMessage -Level "Information" -Message "Module '$ModuleName' being downloaded from PSGallery."
            Install-Module $ModuleName -Scope $scope
            Write-DosMessage -Level "Information" -Message "Successfully downloaded module '$ModuleName' from PSGallery."
            try {
                Import-Module -Name $ModuleName -Global -ErrorAction Stop
            }
            catch [System.Management.Automation.RuntimeException]{
                Write-DosMessage -Level "Warning" -Message "'$ModuleName' was previously loaded during this session. If you encounter weird issues with DosInstallUtilities, please restart PowerShell and resolve conflicts with '$ModuleName' imports." 
            }
            Write-DosMessage -Level "Information" -Message "Successfully imported module '$ModuleName' from PSGallery."
            Write-DosTelemetry -Message "Successfully imported module '$ModuleName' from PSGallery."
        }
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error installing or importing '$ModuleName'. Exception: $($_.Exception)"
        if (!($isTrusted)) {
            Write-DosMessage -Level "Information" -Message "Returning '$desiredRepo' to an untrusted state."
            Set-RepositoryTrust -RepositoryName $desiredRepo
        }
    }

    if (!($isTrusted)) {
        Write-DosMessage -Level "Information" -Message "Returning '$desiredRepo' to an untrusted state."
        Set-RepositoryTrust -RepositoryName $desiredRepo
    }

    if ($replaceCurrentUserModulePath) {
        Remove-FromPSModulePath -Path $currentUserPSModulePath
    }

    Write-DosTelemetry -Message "Install-RequiredModule completed - successfully"
}

#Work around for pester issue: https://github.com/pester/Pester/issues/1007
function Get-ModuleWorkAround{
    param(
        [string] $Name,
        [switch] $ListAvailable
    )


    if($ListAvailable.IsPresent){
        return Get-Module -Name $Name -ListAvailable
    }
    else {
        return Get-Module -Name $Name
    }
}

function Get-RepositoryTrust {
    param (
        [string] $RepositoryName
    )

    $repo = Get-PSRepository -Name $RepositoryName
    return $repo.Trusted
}

function Set-RepositoryTrust {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    param (
        [string] $RepositoryName,
        [switch] $Trust
    )

    if ($Trust.IsPresent) {
        Set-PSRepository -Name $RepositoryName -InstallationPolicy Trusted
    }
    else {
        Set-PSRepository -Name $RepositoryName -InstallationPolicy Untrusted
    }
}

function Add-ToPSModulePath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string] $Path
    )

    if (!($env:PSModulePath.split(";") -contains $Path)){
        Write-DosMessage -Level "Information" -Message "Adding '$Path' to PSModulePath"
        $current = $env:PSModulePath
        [Environment]::SetEnvironmentVariable("PSModulePath",$current + ";" + $Path, "Machine")
        $env:PSModulePath = [System.Environment]::GetEnvironmentVariable("PSModulePath","Machine")
        return $true
    }else{
        Write-DosMessage -Level "Information" -Message "'$Path' is already present in PSModulePath"
        return $false
    }
}

function Remove-FromPSModulePath{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string] $Path
    )
    if ($env:PSModulePath.split(";") -contains $Path){
        $newValue = (($env:PSModulePath).Split(";") | Where-Object { $_ -ne $Path }) -join ";"
        [Environment]::SetEnvironmentVariable("PSModulePath", $newValue, "Machine")
        $env:PSModulePath = [System.Environment]::GetEnvironmentVariable("PSModulePath","Machine")
        Write-DosMessage -Level "Information" -Message "$Path removed from PSModulePath." 
    }else{
        Write-DosMessage -Level "Information" -Message "$Path is not present in $env:PSModulePath"
    }
}

function Compare-ModuleVersion {
    [CmdletBinding()]
    param(
        [object] $ModuleToCompare,
        [version] $RequiredVersion
    )

    $isMatch = $false

    foreach($module in $ModuleToCompare) {
        if ($module.Version.CompareTo($RequiredVersion) -eq 0) {
            Write-DosMessage -Level "Information" -Message "Found Module '$ModuleName' that meets the version requirements."
            $isMatch = $true
            break;
        }
    }

    if (!$isMatch) {
        Write-DosMessage -Level "Information" -Message "No module with version '$RequiredVersion' was found for module '$ModuleName'."
        
    }

    return $isMatch
}


<#
    .SYNOPSIS
    Makes a web request to the $ServiceUrl that is passed in and returns the odata results.
 
    .DESCRIPTION
    Makes a web request to the $ServiceUrl that is passed in and returns the odata results. If there are not results then it will return an empty object.
 
    .PARAMETER ServiceUrl
    [string] (Required) The URL to make a request to
 
    .PARAMETER AccessToken
    [string] (Required) The AccessToken from Identity needed for authentication
 
    .PARAMETER Headers
    [hashtable] (Optional) Headers to send with the request - by default the code will add: @{Accept = "application/json"; Authorization = "Bearer $AccessToken"} but those can be overwritten
    Note: It will only add the Authorization header if AccessToken is set
 
    .PARAMETER ContentType
    [string] (Optional) The ContentType of the Body being sent with the request (if a body is required) - by default the code will set it to "application/json" if it is not set
 
    .PARAMETER Body
    [string] (Optional) The JSON body to send with Patch, Put, and Post requests - will fail if included with Get or Delete requests
 
    .PARAMETER Get
    [switch] Sets GET as the method type
    One of the Get, Patch, Put, Post, or Delete switches MUST be present
 
    .PARAMETER Patch
    [switch] Sets PATCH as the method type
    One of the Get, Patch, Put, Post, or Delete switches MUST be present
 
    .PARAMETER Put
    [switch] Sets PUT as the method type
    One of the Get, Patch, Put, Post, or Delete switches MUST be present
 
    .PARAMETER Post
    [switch] Sets POST as the method type
    One of the Get, Patch, Put, Post, or Delete switches MUST be present
 
    .PARAMETER Delete
    [switch] Sets DELETE as the method type
    One of the Get, Patch, Put, Post, or Delete switches MUST be present
 
    .EXAMPLE
    Invoke-DosOdataRequest -ServiceUrl "https://mymachine.hqcatalyst.local/MetadataService2/v2/DataMarts" -AccessToken $token $Get
    or
    Invoke-DosOdataRequest -ServiceUrl "https://mymachine.hqcatalyst.local/MetadataService2/v2/DataMarts" -AccessToken $token $Post -Body $jsonPayload
#>

function Invoke-DosOdataRequest {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateScript({
            if ($null -eq ($_ -as [System.URI]).AbsoluteURI) {
                throw """$_"" is not a valid url"
            }
            
            return $true
        })]
        [string] $ServiceUrl,
        
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $AccessToken,

        [hashtable] $Headers = @{},

        [string] $ContentType = "application/json",
        
        # The parameter set will require this parameter only when that same parameter set is on another parameter (Put, Patch, Post)
        # It will fail if it is included when not required (Get, Delete)
        [Parameter(ParameterSetName="RequestBody")]
        [string] $Body,

        [Parameter(ParameterSetName="GetRequest")]
        [switch] $Get,

        # Body is a required parameter for Patch
        [Parameter(ParameterSetName="PatchRequest")]
        [Parameter(ParameterSetName="RequestBody")]
        [switch] $Patch,

        # Body is a required parameter for Put
        [Parameter(ParameterSetName="PutRequest")]
        [Parameter(ParameterSetName="RequestBody")]
        [switch] $Put,

        # Body is a required parameter for Post
        [Parameter(ParameterSetName="PostRequest")]
        [Parameter(ParameterSetName="RequestBody")]
        [switch] $Post,

        [Parameter(ParameterSetName="DeleteRequest")]
        [switch] $Delete

    )

    Write-DosMessage -Level "Debug" -Message "Checking for required headers"
    if (-Not $Headers.ContainsKey("Accept")) {
        Write-DosMessage -Level "Debug" -Message "Adding ""Accept"" header"
        $Headers.Add("Accept", "application/json")
    }
    
    # Only add the header if AccessToken is not null or empty and the Headers doesn't already contain Authorization
    if (-Not $Headers.ContainsKey("Authorization")) {
        Write-DosMessage -Level "Debug" -Message "Adding ""Authorization"" header"
        $Headers.Add("Authorization", "Bearer $AccessToken")
    }

    Write-DosMessage -Level "Debug" -Message "Checking that Content-Type is set"
    if ([String]::IsNullOrEmpty($ContentType)) {
        Write-DosMessage -Level "Debug" -Message "Setting Content-Type to ""application/json"""
        $ContentType = "application/json"
    }

    Write-DosMessage -Level "Debug" -Message "Determining which request method was selected"
    if ($Get.IsPresent) {
        $method = "GET"
    }
    elseif ($Post.IsPresent) {
        $method = "POST"
    }
    elseif ($Put.IsPresent) {
        $method = "PUT"
    }
    elseif ($Patch.IsPresent) {
        $method = "PATCH"
    }
    elseif ($Delete.IsPresent) {
        $method = "DELETE"
    }
    Write-DosMessage -Level "Information" -Message "Method selected is ""$method"""

    $output = @()
    $url = $ServiceUrl

    try {
        Write-DosMessage -Level "Information" -Message "Invoking ""$method"" request to ""$ServiceUrl"""
        
        $requestParameters = @{
            Method = $method
            URI = $url
            Headers = $Headers
            ContentType = $ContentType
            UseBasicParsing = $true #required until Powershell 6+
        }

        if ($Body) {
            Write-DosMessage -Level "Debug" -Message "Testing if the body is already Json"
            # When we go to Powershell 7 there is a Test-Json method that can take the place of the next several lines of code
            try {
                ConvertFrom-Json $Body -ErrorAction Stop
                $validJson = $true
            }
            catch {
                $validJson = $false
            }

            if (-Not $validJson) {
                Write-DosMessage -Level "Debug" -Message "Attempting to convert the body to Json"
                $Body = $Body | ConvertTo-Json
            } 

            Write-DosMessage -Level "Debug" -Message "Request body is ""$Body"""
            $requestParameters.Add("Body", $Body)
        }

        do {
            $response = Invoke-RestMethod @requestParameters

            if ($response.PSOjbect.Properties.Name -contains "value") { 
                $output += $response.value 
            }
            else {
                $output += $response
            }

            $url = $response.'@odata.nextLink';
        }
        while ($url);
    }
    catch [System.Net.WebException] {
        Write-DosMessage -Level "Warning" -Message "A non 200 response was returned from ""$url"". This may be expected.`nRequest: $url`nStatus Code: $($_.Exception.Response.StatusCode.value__)`nMessage: $($_.Exception.Response.StatusDescription)"
        throw $_ # Rethrow error for downstream catching if desired
    }
    catch {
        Write-DosMessage -Level "Fatal" -Message "An error was encountered while making the web request. Exception: $($_.Exception)"
    }

    return $output
}


function Invoke-DosPingServices {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Values should be plural.")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $DiscoveryServiceUrl,
        [string] $AccessToken,
        [Parameter(Mandatory = $true)]
        [hashtable[]] $Services
    )
    begin {
        Write-DosMessage -Level "Information" -Message "PING DISCOVERYSERVICE" -HeaderType H2
        Invoke-DosPingService -ServiceName "DiscoveryService" -Uri "$(Remove-VersionFromLocalPath $DiscoveryServiceUrl)/ping" -Headers @{"Accept" = "application/json" } -UseDefaultCredentials -ErrorLevel Fatal
    }
    process {
        foreach ($Service in $Services) {
            try {
                Write-DosMessage -Level "Information" -Message "PING $($Service.Name.ToUpper())" -HeaderType H2
                $Headers = @{"Accept" = "application/json" }
                if ($Service.RequireAuthToken) {
                    if ($AccessToken) {
                        $Headers.Add("Authorization", "Bearer $AccessToken")
                    }
                    else {
                        Write-DosMessage -Level "Fatal" -Message "$($Service.Name) ping header did not provide the required AccessToken"
                    }
                }
            
                if([string]::IsNullOrEmpty($Service.AbsoluteEndpoint)){
                    $UriRoot = Remove-VersionFromLocalPath (Get-DosServiceUrl -DiscoveryServiceUrl $DiscoveryServiceUrl -ServiceName $Service.Name -ServiceVersion $Service.Version)
                    #removing trailing slashes, if there are any
                    if ($UriRoot.EndsWith("/")){
                        $UriRoot = $UriRoot.substring(0,$UriRoot.Length-1)
                    }
        
                    $Uri = "$($UriRoot)/$($Service.PingEndpoint)"
                } else {
                    $Uri = $Service.AbsoluteEndpoint
                }
                
                Write-DosMessage -Level "Information" -Message "Appended ping endpoint to root url --> $Uri"
                $invokePingServiceParams = @{
                    ServiceName           = $Service.Name
                    Uri                   = $Uri
                    Headers               = $Headers
                    ErrorLevel            = $Service.ErrorLevel
                    UseDefaultCredentials = if ($Service.UseDefaultCredentials) { $true } else { $false }
                }
                Invoke-DosPingService @invokePingServiceParams
            }
            catch {
                Write-DosMessage -Level $Service.ErrorLevel -Message "There was an error pinging the service `"$($Service.Name)`". This installation is configured to return a `"$($Service.ErrorLevel.ToUpper())`" message if this occurs."
                if ($Service.ErrorLevel -eq "Fatal") {
                    Write-DosMessage -Level $Service.ErrorLevel -Message "Please check and fix the service `"$($Service.Name)`" before trying this installation again."
                }
            }
        }    
    }
}

function Invoke-DosPingService {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $ServiceName,
        [Parameter(Mandatory = $true)]
        [string] $Uri,
        [Parameter(Mandatory = $true)]
        [hashtable] $Headers,
        [string] $Method = "Get",
        [switch] $UseDefaultCredentials,
        [ValidateSet("Error", "Fatal", "Warning")]
        [string] $ErrorLevel = "Error",
        [int] $RetryAttempts = 5,
        [int] $RetrySleepSeconds = 3
    )
    $counter = 0
    $success = $false
    try {
        Write-DosMessage -Level "Information" -Message "Pinging $($ServiceName)...$($Uri)"
        $webRequestParams = @{
            Method  = $Method
            Uri     = $Uri
            Headers = $Headers
        }
        if ($UseDefaultCredentials) {
            $webRequestParams.Add("UseDefaultCredentials", $UseDefaultCredentials)
        }
        while (!$success -and $counter -lt $RetryAttempts) {
            try {
                $response = Invoke-WebRequest @webRequestParams -UseBasicParsing
                Write-DosMessage -Level "Information" -Message "$($ServiceName) ping successful! $($response.StatusCode) ($($response.StatusDescription))"
                $success = $true
            }
            catch {
                $counter++
                Write-DosMessage -Level "Warning" -Message "There was an error pinging the service `"$($ServiceName)`"."
                Write-DosMessage -Level "Information" -Message "Retrying ping - attempt $($counter) of $($RetryAttempts) ..."
                Start-Sleep -Seconds $RetrySleepSeconds
            }
        }
        if (!$success) { throw }
    }
    catch {
        Write-DosMessage -Level $ErrorLevel -Message "There was an error pinging the service `"$($ServiceName)`". This installation is configured to return a `"$($ErrorLevel.ToUpper())`" message if this occurs."
        if ($ErrorLevel -eq "Fatal") {
            Write-DosMessage -Level $ErrorLevel -Message "Please check and fix the service `"$($ServiceName)`" before trying this installation again."
        }
    }
}


<#
    .SYNOPSIS
 
    Reads in json manifest files and invokes the appropriate readiness checks based of the supported check types.
 
    .DESCRIPTION
 
     
 
    .PARAMETER ManifestPath
 
    Accepts an array of valid paths to json manifest files.
 
    .PARAMETER ResourceToCheck
 
    Accepts a string of either "dbServer", or "webServer". The environment that the readiness checks will run against.
 
    .EXAMPLE
 
    Invoke-DosPrerequisiteChecks -ManifestPath @("C:/Exampe/Path/Manifest.json", "C:/Another/Example/Manifest.json") -ResourceToCheck "dbServer" -PathToLogRoot "C:\install"
#>


function Invoke-DosPrerequisiteChecks {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Values should be plural.")]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateScript({
            foreach ($path in $_) {
                if (!(Test-Path $path)) {
                    throw "ManfiestPath $path does not exist. Please enter valid path."
                }
                else {
                    $true
                }
            }
        })]
        [string[]] $ManifestPath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateSet("dbServer", "webServer")]
        [string] $ResourceToCheck,
        [ValidateScript({
            foreach ($path in 0) {
                if (!(Test-Path $path)) {
                    throw "PathToLogRoot '$path' does not exist. Please enter valid path."
                }
                else {
                    $true
                }
            }
        })]
        [string] $PathToLogRoot = "C:\install\"
    )

    $fileDate = (Get-Date).tostring("dd-MM-yyyy-hh-mm-ss")
    $logFile =  "InstallReadinessTool_" + $fileDate + ".log"
    $logPath = $PathToLogRoot + $logFile
    try {
        New-Item -ItemType File -Path $PathToLogRoot -Name $logFile -Force -ErrorAction Stop | Out-Null
    }
    catch {
        throw "Error creating file, '$logPath'. Exception: $_"
    }
    Write-Output "Log file generated in '$PathToLogRoot'"

    # Potentially Loop for the manifests provided
    foreach ($manifest in $ManifestPath) {
        # Get-Content json manfiests
        $manifestData = Get-Content -Raw -Path "$manifest" | ConvertFrom-Json

        # Powershell Checktype
        $powershellData = $manifestData.readinessChecks | Where-Object { $_.checkType -eq "powershellVersion" -and $_.resourceToCheck -contains "$ResourceToCheck" }
        $psVersionCheckResult = Invoke-PowershellVersionCheck -Data $powershellData *>&1
        Write-VariableToConsoleAndFile -VariableToParse $psVersionCheckResult -Log $logPath

        # Os Version Checktype
        $allowedOSVersions = @("Server 2012 R2", "Server 2016", "Windows 10")
        $osVersionCheckResult = Invoke-OsVersionCheck -AllowedVersions $allowedOSVersions *>&1
        Write-VariableToConsoleAndFile -VariableToParse $osVersionCheckResult -Log $logPath

        # Dependent Software Checktype
        $dependentSoftwareData = $manifestData.readinessChecks | Where-Object { $_.checkType -eq "dependentSoftware" -and $_.resourceToCheck -contains "$ResourceToCheck" }
        $dependentSoftwareCheckResult = Invoke-DependentSoftwareCheck -Data $dependentSoftwareData *>&1
        Write-VariableToConsoleAndFile -VariableToParse $dependentSoftwareCheckResult -Log $logPath

        # Windows Feature Checktype
        $windowsFeatureData = $manifestData.readinessChecks | Where-Object { $_.checkType -eq "windowsFeature" -and $_.resourceToCheck -contains "$ResourceToCheck" }
        $windowsFeatureCheckResult = Invoke-WindowsFeatureCheck -Data $windowsFeatureData *>&1
        Write-VariableToConsoleAndFile -VariableToParse $windowsFeatureCheckResult -Log $logPath
    }
}

function Write-VariableToConsoleAndFile {
    param (
        [object[]] $VariableToParse,
        [string] $Log
    )
    ForEach ($line in $($VariableToParse -split "`r`n"))
    {
        if ($line -like '*`[+`]*') {
            Write-Host $line -Foregroundcolor Green
            $line | Out-File -FilePath $Log -Append
        } elseif ($line -like '*`[-`]*') {
            Write-Host $line -Foregroundcolor Red 
            $line | Out-File -FilePath $Log  -Append
        } else {
            Write-Host $line 
            $line | Out-File -FilePath $Log -Append
        }
    }
}


<#
.SYNOPSIS
Executes a query
 
.DESCRIPTION
Executes TSQL against a SQL Server
 
.PARAMETER SqlConnection
The sql connection to use to execute the query
 
.PARAMETER ConnectionString
The connection string used to create a new connection.
 
.PARAMETER InstanceName
Instance name for use in creating an AdHoc connection
 
.PARAMETER DatabaseName
Database name for use in creating an AdHoc connection
 
.PARAMETER Credentail
An optional PsCredential object if using sql auth
 
.PARAMETER Query
TSQL to execute
 
.PARAMETER Parameters
A hashtable of parameters to build a parameterized query
 
.PARAMETER AsResult
Return results as DataSet, DataTable, or array or DataRows
 
.PARAMETER NonQuery
Executes the query as ExecuteNonQuery()
This will allow you to run the query and then suppress the results output
 
.INPUTS
None.
 
.OUTPUTS
[System.Data.DataSet], [System.Data.DataTable], [System.Data.DataRow[]]
 
.EXAMPLE
PSn Invoke-DosSqlQuery ...
#>

function Invoke-DosSqlQuery {
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType([System.Data.DataSet],[System.Data.DataTable],[System.Data.DataRow[]])]
    param(
        [parameter(Mandatory=$true,ParameterSetName='Connection')][Data.SqlClient.SqlConnection]$SqlConnection,
        [parameter(Mandatory=$true,ParameterSetName='ConnectionString')][string]$ConnectionString,
        [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$InstanceName,
        [parameter(Mandatory=$false,ParameterSetName='AdHoc')][string]$DatabaseName,
        [parameter(Mandatory=$false,ParameterSetName='AdHoc')][PSCredential]$Credential,
        [parameter(Mandatory=$true)][string]$Query,
        [parameter(Mandatory=$false)][hashtable]$Parameters,
        [parameter(Mandatory=$false)][ValidateSet("DataSet", "DataTable", "Array")][string]$AsResult='Array',
        [parameter(Mandatory=$false)][switch]$NonQuery,
        [parameter(Mandatory=$false,ValueFromRemainingArguments=$true)]$Arguments
    )

    <# This function is going to be splatted on by other functions $PSBoundParametes as they share
    common parameter sets for connectivity. Non defined parameters lands in $Arguments which
    dont care about nor do we want to pass that only New-SqlConnection. Remove $Arguments from our
    $PSBoundParameters #>

    $PSBoundParameters.Remove('Arguments') | Out-Null
    $PSBoundParameters.Remove('ErrorAction') | Out-Null

    if(@('AdHoc','ConnectionString').Contains($PSCmdlet.ParameterSetName)){
        $SqlConnection = New-DosSqlConnection @PSBoundParameters -ErrorAction Stop
    }

    $Cmd = New-Object System.Data.SqlClient.SqlCommand($Query,$SqlConnection)

    if($null -ne $Parameters){
        foreach($key in $Parameters.Keys){
            $cmd.Parameters.AddWithValue($Key,$Parameters[$Key]) | Out-Null
        }
    }

    if($NonQuery){
        if ($pscmdlet.ShouldProcess($Query, "Executing non SQL query")){
            Invoke-ExecuteNonQuery -cmd $cmd
        }
    }
    else{
        if ($pscmdlet.ShouldProcess($Query, "Executing SQL Query")){
            $results = Invoke-Fill -cmd $cmd -AsResult $AsResult
        }
    }

    if(@('AdHoc','ConnectionString').Contains($PSCmdlet.ParameterSetName)){
        if ($pscmdlet.ShouldProcess($ConnectionString, "Closing SQL connection")){
            Invoke-CloseAndDispose -SqlConnection $SqlConnection
        }
    }

    if($NonQuery -eq $false){
        return $results
    }
}

function Invoke-CloseAndDispose{
    param(
        [Data.SqlClient.SqlConnection]$SqlConnection
    )
    $SqlConnection.Close() | Out-Null
    $SqlConnection.Dispose() | Out-Null
}

function Invoke-ExecuteNonQuery{
    param(
        [System.Data.SqlClient.SqlCommand]$cmd
    )
    $cmd.ExecuteNonQuery() | Out-Null
}

function Invoke-Fill{
    param(
        [System.Data.SqlClient.SqlCommand]$cmd
        ,[string]$AsResult
    )

    $ds=New-Object system.Data.DataSet
    $da=New-Object system.Data.SqlClient.SqlDataAdapter($cmd)
    $da.Fill($ds) | Out-Null
    switch($AsResult){
        'DataSet'{
            $results = $ds
        }
        'DataTable'{
            $results = $ds.Tables[0]
        }
        'Array'{
            $results = @()
            $results += $ds.Tables[0].Rows
        }
    }
    return $results

}


<#
    .SYNOPSIS
 
    Creates a new ConfigStore
 
    .DESCRIPTION
 
    Attempts to create a new config store
 
    .PARAMETER configStore
 
    The configstore hashtable that will be created
 
    .OUTPUTS
 
    $configStore - the $configStore hashtable is returned
 
    .EXAMPLE
 
    New-DosConfigStore -configStore @{Type = "File"; Format = "XML"; Path = "$PSScriptRoot\nonexistent\nonexistent.config"}
    New-DosConfigStore -configStore $configStore
 
#>

function New-DosConfigStore {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $configStore
    )
    Write-DosTelemetry -Message "$($MyInvocation.MyCommand.Name) called."

    if($configStore.Type -eq "File" -and $configStore.Format -eq "XML"){
        return New-DosConfigStoreXml -configStore $configStore
    } else {
        Write-DosMessage -Level 'Fatal' -Message "New-DosConfigStore not implemented yet for type: $($configStore.Type), format: $($configStore.Format)"
        return $null
    }

    
}

<#
    .SYNOPSIS
    NON PUBLIC - creates the xml file for the XML/File config store
     
    .DESCRIPTION
    creates the xml file for the XML/File config store
     
    .PARAMETER configStore
    The xml object that represents the install.config
     
    .EXAMPLE
    New-DosConfigStoreXml -configStore $configStore
     
    .NOTES
    General notes
#>

function New-DosConfigStoreXml {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory=$true)]
        [hashtable] $configStore
    )

    if(Test-Path -Path $configStore.Path){
        Write-DosMessage -Level Warning -Message "$($configStore.Path) already exists. No config store created"
    } else {
        $folder = split-path -path $configStore.Path
        if(-not(Test-Path -Path $folder)){
            Write-DosMessage -Level Information -Message "Creating $folder."
            if($PSCmdlet.ShouldProcess("Create $folder")){
                New-Item -ItemType directory -Path $folder | Out-Null
            }
            
        }
        if(-not(Test-Path -Path $configStore.Path)){
            Write-DosMessage -Level Information -Message "Creating $($configStore.Path) with template settings."
            $installationConfigTemplate = "<installation>`n`t<settings>`n`t</settings>`n</installation>"
            if($PSCmdlet.ShouldProcess("Create $($configStore.Path))")){
                New-Item -Path $configStore.Path -ItemType "file" -Value $installationConfigTemplate -Force | Out-Null
            }
        }
        
    }

    return $configStore
}


function New-DosDacPacPublishFile {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    param (
        [Parameter(Mandatory = $true)]
        [string] $publishProfilePath,
        [Parameter(Mandatory = $true)]
        [hashtable] $publishProfileValues
    )
    begin {
        Write-DosMessage -Level "Information" -Message "Creating Dacpac Publish Profile $publishProfilePath"
        [xml]$publishProfileXml = @'
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
</Project>
'@

    }
    process {
        try {
            Add-PublishDacPacFile -publishProfileXml $publishProfileXml -publishProfileValues $publishProfileValues -publishProfilePath $publishProfilePath
            Write-DosMessage -Level "Information" -Message "Saving changes to file $publishProfilePath"
        }
        catch {
            $ErrorMessage = $_.Exception.Message
            Write-DosMessage -Level "Fatal" -Message $ErrorMessage    
        }
    }
}

function Add-PublishDacPacFile {
    param (
        [xml] $publishProfileXml,
        [hashtable] $publishProfileValues,
        [string] $publishProfilePath
    )
    $project = $publishProfileXml.Project            
    $itemGroup = $publishProfileXml.CreateElement("ItemGroup", $project.xmlns)
    foreach ($publishProfileValue in $publishProfileValues.GetEnumerator()) {        
        $sqlCmdVariable = $publishProfileXml.CreateElement("SqlCmdVariable", $project.xmlns)
        $include = $publishProfileXml.CreateAttribute("Include")        
        $include.Value = $publishProfileValue.Name
        $value = $publishProfileXml.CreateElement("Value", $project.xmlns)
        $value.InnerText = $publishProfileValue.Value
        $sqlCmdVariable.AppendChild($value) | Out-Null
        $sqlCmdVariable.Attributes.Append($include) | Out-Null
        $itemGroup.AppendChild($sqlCmdVariable) | Out-Null
        Write-DosMessage -Level "Information" -Message "Added $($publishProfileValue.Name) -> $($publishProfileValue.Value)"
    }
    $project.AppendChild($itemGroup) | Out-Null
    $publishProfileXml.Save($publishProfilePath)
}


<#
.SYNOPSIS
Creates a SQL connection
 
.DESCRIPTION
Creates and returns an ***open*** SQL connection.
 
.PARAMETER InstanceName
The full <ComputerName>/<NamedInstance> instance name of SQL server
 
.PARAMETER DatabaseName
The database name to connect too
 
.PARAMETER Credential
A credential containing the SQL auth user and password
 
.PARAMETER ConnectionString
A SQL connection string to build the connection from
 
.INPUTS
None.
 
.OUTPUTS
Data.SqlClient.SqlConnection
 
.EXAMPLE
PS> New-DosSqlConnection ...
#>

function New-DosSqlConnection {
    [cmdletbinding(DefaultParameterSetName='Default',SupportsShouldProcess=$true)]
    [OutputType([Data.SqlClient.SqlConnection])]
    param(
        [parameter(Mandatory=$true,ParameterSetName='Default')]
        [parameter(Mandatory=$true,ParameterSetName='SqlAuth')]
        [string]$InstanceName,
        [parameter(Mandatory=$false,ParameterSetName='Default')]
        [parameter(Mandatory=$false,ParameterSetName='SqlAuth')]
        [string]$DatabaseName='master',
        [parameter(Mandatory=$true,ParameterSetName='SqlAuth')][PSCredential]$Credential,
        [parameter(Mandatory=$true,ParameterSetName='ConnectionString')][string]$ConnectionString,

        [parameter(Mandatory=$false,ParameterSetName='Default',ValueFromRemainingArguments=$true)]
        [parameter(Mandatory=$false,ParameterSetName='SqlAuth',ValueFromRemainingArguments=$true)]
        [parameter(Mandatory=$false,ParameterSetName='ConnectionString',ValueFromRemainingArguments=$true)]
        $Arguments
    )

    switch($PSCmdlet.ParameterSetName){
        'ConnectionString'{}
        'SqlAuth'{
            $ConnectionString = "Server={0};Database={1};User ID={2};Password={3};Pooling=false" -f $InstanceName, $DatabaseName, $Credential.UserName, $Credential.GetNetworkCredential().Password
        }
        'Default'{
            $ConnectionString = "Server={0};Database={1};Integrated Security=True" -f $InstanceName, $DatabaseName
        }
    }

    try{
        if ($pscmdlet.ShouldProcess($ConnectionString, "Creating SQL connection")){
            $connection = New-Object Data.SqlClient.SqlConnection $ConnectionString
            $connection.Open()
        }
    }
    catch{
        Write-DosMessage -Level 'Fatal' -Message "$_.Exception.Message"
    }

    return $connection
}


<#
.SYNOPSIS
Creates SQL login
 
.DESCRIPTION
Creates an instance level SQL login
 
.PARAMETER InstanceName
Full instance name of SQL server
 
.PARAMETER ConnectionString
Connection string to a SQL instnace
 
.PARAMETER SqlConnection
SQL connection to target SQL Server
 
.PARAMETER LoginName
The full name of the login to create
 
.PARAMETER AuthenticationType
Windows or SQL
 
.PARAMETER Credential
The cred (user/pass) to set for the new login
 
.PARAMETER IfNotExists
A switch to only create if login does not exist
 
.INPUTS
None
 
.OUTPUTS
None
 
.EXAMPLE
PS> New-DosSqlLogin -ConnectionString $connectionString -LoginName 'domain\username' -IfNotExists
 
Replaces New-SqlLogin
#>

function New-DosSqlLogin {
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType()]
    param(
        [parameter(Mandatory=$true,ParameterSetName='Connection')][Data.SqlClient.SqlConnection]$SqlConnection,
        [parameter(Mandatory=$true,ParameterSetName='ConnectionString')][string]$ConnectionString,
        [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$InstanceName,
        [parameter(Mandatory=$false,ParameterSetName='AdHoc')][PSCredential]$Credential,
        [parameter(Mandatory=$true)][string]$LoginName,
        [parameter(Mandatory=$false)][ValidateSet("Windows", "SQL")][string]$AuthType='Windows',
        [parameter(Mandatory=$false)][pscredential]$NewLoginCredential,
        [parameter(Mandatory=$false)][switch]$IfNotExists
    )

    if($AuthType -eq 'SQL' -and $null -eq $Credential){
        throw [System.Management.Automation.ParameterBindingException] "Credential must be provided when using an AuthType of SQL"
    }
    elseif($AuthType -eq 'Windows' -and $null -ne $Credential){
        throw [System.Management.Automation.ParameterBindingException] "Credential cannot be provided when using an AuthType of Windows"
    }
    $query = ""
    if($IfNotExists){
        $query += "IF NOT EXISTS (SELECT * FROM [sys].[server_principals] WHERE name = '$LoginName')`n"
    }

    switch($AuthType){
        'Windows'{
            $query += "CREATE LOGIN [$LoginName] FROM WINDOWS WITH DEFAULT_DATABASE=[master]"
        }
        'SQL'{
            $query += "CREATE LOGIN [$LoginName] WITH PASSWORD=N'$($Credential.GetNetworkCredential().Password.Replace("'","''"))', DEFAULT_DATABASE=[master], CHECK_EXPIRATION=ON, CHECK_POLICY=ON"
        }

    }

    $PSBoundParameters.Remove('ErrorAction') | Out-Null
    if($PSCmdlet.ShouldProcess($Query,"Creating login: $LoginName")){
        Invoke-DosSqlQuery @PSBoundParameters -Query $query -NonQuery | Out-Null
    }
}


<#
.SYNOPSIS
Creates SQL database user
 
.DESCRIPTION
Creates an SQL database user in a given database
 
.PARAMETER InstanceName
Full instance name of SQL server
 
.PARAMETER ConnectionString
Connection string to a SQL instnace
 
.PARAMETER SqlConnection
SQL connection to target SQL Server
 
.PARAMETER DatabaseName
Database name in which to create the user
 
.PARAMETER UserName
The full name of the user to create
 
.PARAMETER IfNotExists
A switch to only create if user does not exist
 
.INPUTS
None
 
.OUTPUTS
None
 
.EXAMPLE
PS> New-DosSqlUser -ConnectionString $connectionString -UserName 'domain\username' -IfNotExists
 
Replaces New-SqlUser
#>

function New-DosSqlUser {
    [cmdletbinding(SupportsShouldProcess=$true)]
    [OutputType()]
    param(
        [parameter(Mandatory=$true,ParameterSetName='Connection')][Data.SqlClient.SqlConnection]$SqlConnection,
        [parameter(Mandatory=$true,ParameterSetName='ConnectionString')][string]$ConnectionString,
        [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$InstanceName,
        [parameter(Mandatory=$true,ParameterSetName='AdHoc')][string]$DatabaseName,
        [parameter(Mandatory=$false,ParameterSetName='AdHoc')][PSCredential]$Credential,
        [parameter(Mandatory=$true)][string]$UserName,
        [parameter(Mandatory=$false)][switch]$IfNotExists
    )

    $query = ""
    if($IfNotExists){
        $query += "IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = N'$UserName')`n"
    }

    $query += "CREATE USER [$UserName] FOR LOGIN [$UserName] WITH DEFAULT_SCHEMA=[dbo]"

    $PSBoundParameters.Remove('ErrorAction') | Out-Null
    if($PSCmdlet.ShouldProcess($Query,"Creating user: $UserName")){
        Invoke-DosSqlQuery @PSBoundParameters -Query $query -NonQuery | Out-Null
    }
}


<#
    .SYNOPSIS
 
    Publishes the target DOS dac file to the specified database with the specified options
 
    .DESCRIPTION
 
    Uses Microsoft.SqlServer.Dac.DacServices to install DAC file to specified server.
 
    .PARAMETER DacPacFilePath
 
    File path to dacpac file to publish to server
 
    .PARAMETER TargetSqlInstance
 
    Sql server connection string supports specifiying non-default sql instance and port if needed
 
    .PARAMETER TargetDb
 
    Target database to publish dac pac to.
 
    .PARAMETER PublishOptionsFilePath
 
    Path to publish options file - Required - See tests/SampleFiles/DefaultDacDeployOptions.xml for an example
 
    .PARAMETER ForceMountPointCreation
 
    Will attempt to create the mount points specified in PublishOptionsFilePath if the folders don't exist. If the specified on an upgrade, a warning will be displayed stating that mount points will be whatever the current DB has set.
 
    .EXAMPLE
 
    Publish-DosDacPac -DacPacFilePath ".\test.dac" -TargetSqlInstance "localhost" -TargetDb "EDWAdmin" -PublishOptionsFilePath ".\test.publish.xml"
 
    Publish-DosDacPac -DacPacFilePath ".\test.dac" -TargetSqlInstance "localhost,1433\MSSQLServer" -TargetDb "EDWAdmin" -PublishOptionsFilePath ".\test.publish.xml"
#>

function Publish-DosDacPac {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Error" -Message "DacPacFilePath $_ does not exist. Please enter valid path." -ErrorAction Stop
            }
            else {
                $true
            }
        })]
        [string] $DacPacFilePath,
        [Parameter(Mandatory=$true)]
        [string] $TargetSqlInstance,
        [Parameter(Mandatory=$true)]
        [string] $TargetDb,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Error" -Message "PublishOptionsFilePath $_ does not exist. Please enter valid path." -ErrorAction Stop
            }
            else {
                $true
            }
        })]
        [string] $PublishOptionsFilePath,
        [switch] $ForceMountPointCreation,
        [string] $MountPointComputerName = $TargetSqlInstance
    )
    Write-DosMessage -Level "Information" -Message "Publish-DosDacPac start"
    Write-DosMessage -Level "Information" -Message "Publishing '$DacPacFilePath' to '$TargetDb'"
    $dbatoolsVersion = New-Object -TypeName System.Version -ArgumentList "1.0.115"

    Write-DosMessage -Level "Information" -Message "Attempting to install dbatools with a version of '$dbatoolsVersion'"
    Write-DosMessage -Level "Information" -Message "Checking for required modules: dbatools '$dbatoolsVersion'"
    Install-RequiredModule -ModuleName dbatools -RequiredVersion $dbatoolsVersion
    Write-DosMessage -Level "Information" -Message "Successfully installed dbatools."

    $previousDataBase = Get-DbaDatabase -SqlInstance $TargetSqlInstance -Database $TargetDb

    $previousRecoveryModel = $null
    [string] $previousDbOwner = $null
    if($null -ne $previousDataBase){
        Write-DosMessage -Level "Information" -Message "Found existing DB $TargetDb on $TargetSqlInstance, will ensure that previous owner and recovery model are preserved across update"
        $previousDbOwner = $previousDataBase.Owner
        $previousRecoveryModel = $previousDataBase.RecoveryModel

        if($ForceMountPointCreation){
            Write-DosMessage -Level "Warning" -Message "Previous database already installed, existing mount points will be used"
        }

    }
    else{
        [xml] $parsedPublishOptions = [xml] (Get-Content $PublishOptionsFilePath -ErrorAction Stop)

        $sqlCmdVariable = $parsedPublishOptions.Project.ItemGroup.SqlCmdVariable
        
        try {
            Write-DosMessage -Level "Information" -Message "Beginning attempt to create database mount points"

            $dataMountPoints = $sqlCmdVariable | Where-Object {$_.Include -like "*Data*MountPoint" -or $_.Include -like "PrimaryMountPoint"}

            if($null -eq $dataMountPoints) {
                Write-DosMessage -Level "Error" -Message "Missing data mount point in $PublishOptionsFilePath"
                return
            }

            foreach($dataMountPoint in $dataMountPoints) {
                Write-DosMessage -Level "Information" -Message "Beginning attempt to create mount point $dataMountPoint"
                Add-MountPoint -Path $dataMountPoint.Value -CreateIfForced $ForceMountPointCreation.IsPresent -MountPointComputerName $MountPointComputerName
            }

            foreach($logMountPoint in $sqlCmdVariable | Where-Object {$_.Include -like "*Log*MountPoint"}) {
                Write-DosMessage -Level "Information" -Message "Beginning attempt to create mount point $logMountPoint"
                Add-MountPoint -Path $logMountPoint.Value -CreateIfForced $ForceMountPointCreation.IsPresent -MountPointComputerName $MountPointComputerName
            }
            
            foreach($indexMountPoint in $sqlCmdVariable | Where-Object {$_.Include -like "*Index*MountPoint"}) {
                Write-DosMessage -Level "Information" -Message "Beginning attempt to create mount point $indexMountPoint"
                Add-MountPoint -Path $indexMountPoint.Value -CreateIfForced $ForceMountPointCreation.IsPresent -MountPointComputerName $MountPointComputerName
            }
            
            Write-DosMessage -Level "Information" -Message "Finished attempt to create database mount points - Success"
        }
        catch {
            Write-DosMessage -Level "Fatal" -Message "Error occured while attempting to confirm '$TargetDb' mount points. Validate the connection capabilites of '$MountPointComputerName'. Exception: $($_.Exception)"
            Write-DosMessage -Level "Information" -Message "Finished attempt to create database mount points - Failure"
        }
        
    }

    try {
        Write-DosMessage -Level Information -Message "Beginning Dacpac deployment"

        $dacpacReport = Publish-DbaDacPackage -SqlInstance $TargetSqlInstance -Database $TargetDb -Path $DacPacFilePath -PublishXml $PublishOptionsFilePath -EnableException
        Write-DosMessage -Level Information -Message "$dacpacReport"
        
        Write-DosMessage -Level Information -Message "Finished Dacpac deployment"
        $currentDatabase = Get-DbaDatabase -SqlInstance $TargetSqlInstance -Database $TargetDb

        if($null -ne $previousDataBase){
            Write-DosMessage -Level Information -Message "Checking that Recovery Model and DbOwner settings are preserved on the database"
            if($currentDatabase.RecoveryModel -ne $previousRecoveryModel){
                Write-DosMessage -Level "Information" -Message "New recovery model $($currentDatabase.RecoveryModel) doesn't match previous recovery model $previousRecoveryModel, reverting to previous recovery model"
                Set-DbaDbRecoveryModel -RecoveryModel $previousRecoveryModel.ToString() -SqlInstance $TargetSqlInstance -Database $TargetDb -Confirm:$false -EnableException
            }
            if($currentDatabase.Owner -ne $previousDbOwner){
                Write-DosMessage -Level "Information" -Message "New DB owner $($currentDatabase.Owner) doesn't match previous owner, reverting to old owner $previousDbOwner"
                Set-DbaDbOwner -SqlInstance $TargetSqlInstance -Database $TargetDb -TargetLogin $previousDbOwner -Confirm:$false -EnableException
            }
        }
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Unable to deploy $DacPacFilePath to $TargetDb on $TargetSqlInstance. Exception: $($_.Exception)"
        Write-DosMessage -Level "Information" "Finished Dacpac deployment - failure"
    }

    Write-DosMessage -Level "Information" -Message "Publish-DosDacPac complete"
    Write-DosMessage -Level "Information" -Message "Publishing '$DacPacFilePath' to '$TargetDb' completed"
}


function Add-MountPoint{
    [CmdletBinding()]
    param(
        [string] $Path,
        [bool] $CreateIfForced,
        [string] $MountPointComputerName
    )
    
    $HostcomputerName = hostname

    $scriptBlock = {
        $CreateIfForced = $args[1]
        $Path = $args[0]
        if($CreateIfForced){
            if(!(Test-Path $Path)){
                try {
                    New-Item -ItemType Directory $Path | Out-Null
                    return "Mount point $Path created"
                }
                catch {
                    return "Error creating mount point $Path. Exception: $($_.Exception)"
                }
                
            }
        }
        else{
            if(!(Test-Path $Path)){
                return "Mount point $Path not found use -ForceMountPointCreation to enable creation of necessary folders"
            }
        }
    }

    Test-ElevatedPermission
    Write-DosMessage -Level "Information" -Message "Validating $Path Mount Point on $MountPointComputerName"
    
    $params = @{
        ScriptBlock = $scriptBlock
        ArgumentList = @($Path, $CreateIfForced)
    }


    $MountPointHostname = $MountPointComputerName.split('.')[0]
    if($MountPointHostname -match "^\d+$"){
        #if the $MountPointHostname is a number (e.g. because it is actually an ip address), just use the full computer name
        $MountPointHostname = $MountPointComputerName
    }

    if($MountPointHostname -eq 'localhost' -or $MountPointHostname -eq '127.0.0.1'){
        #don't add computer name to the parameters
        Write-DosMessage -Level "Debug" -Message "It appears that '$MountPointComputerName' is localhost, so we run the Invoke-Command locally"
    } elseif($MountPointHostname -eq $HostcomputerName) {
        #don't add computer namme to the parameters
        Write-DosMessage -Level "Debug" -Message "It appears that '$HostcomputerName' is the same as '$MountPointComputerName', so we run the Invoke-Command locally"
    } else {
        try{
            $IpAddressMountPointHostname = $(Test-NetConnection -computername $MountPointComputerName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue).RemoteAddress.IPAddressToString
            $IpAddressLocalhost = $(Test-NetConnection -computername $HostcomputerName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue).RemoteAddress.IPAddressToString
            if($IpAddressMountPointHostname -eq $IpAddressLocalhost){
                #don't add computer name to the parameters
                Write-DosMessage -Level "Debug" -Message "It appears that '$IpAddressMountPointHostname' is the same as '$IpAddressLocalhost', so we run the Invoke-Command locally"
            } else {
                Write-DosMessage -Level "Debug" -Message "It appears that '$HostcomputerName' is a different computer than '$MountPointComputerName', so we'll use PS Remoting"
                $params.Add("ComputerName", $MountPointComputerName)
            }
        } catch {
            Write-DosMessage -Level "Debug" -Message "It appears that '$HostcomputerName' is a different computer than '$MountPointComputerName', so we'll use PS Remoting."
            $params.Add("ComputerName", $MountPointComputerName)
        }

    }
    
    Write-DosMessage -Level "Information" -Message "Running invoke-command"
    $addMountResult = Invoke-Command @params

    if ($addMountResult -like "*use -ForceMountPointCreation*" -or $addMountResult -like "Error creating mount point*") {
        Write-DosMessage -Level "Error" -Message $addMountResult
    }
    if ($addMountResult -eq "Mount point $Path created") {
        Write-DosMessage -Level "Information" -Message $addMountResult
    }
}


<#
    .SYNOPSIS
 
    Publishes the target DOS web application with the specified options
 
    .DESCRIPTION
 
    Publishes the target DOS web application with the specified options. Either uses WebDeploy (for older apps), or internal logic for new .Net Core applicaitons.
 
    .PARAMETER WebAppPackagePath
 
    File path to web application zip to publish
 
    .PARAMETER SettingsXmlPath
 
    ONLY USED in WebDeploy Applications - File path to xml settings
 
    .PARAMETER AppPoolName
 
    IIS Application Pool name
 
    .PARAMETER AppPoolCredential
 
    Credential object used to configure the built-in account the IIS Application Pool runs as
     
    .PARAMETER AuthenticationType
 
    ONLY USED in WebDeploy Applications - Windows or Anonymous authentication
 
    .PARAMETER WebDeploy
 
    A toggle for the web application to be deployed via WebDeploy or by other means
 
    .PARAMETER AppName
 
    ONLY USED in NON-Webdeploy applications. Specifies both the application's name AND the folder name where the application will be placed underneath the IIS site's root folder.
 
    .PARAMETER IISWebSite
 
    ONLY USED in NON-WebDeploy applications. Specifies the IIS site to publish the application to. Defaults to "Default Web Site"
     
    .PARAMETER WebDeployParameters
     
    ONLY USED in WebDeploy Applications - Arraylist object containing site settings
     
    .PARAMETER PathsToPreserve
     
    Array of paths to preserve during a deployment, such as logs, relative to the install directory in IIS, so they are not removed during the upgrade of an application. Ignored for new installs.
     
    .EXAMPLE
 
    Publish-DosWebApplication -WebAppPackagePath "testapp.zip" -SettingsXmlPath "testapp.settings.xml" -AppPoolName "x" -AppPoolCredential $creds -AuthenticationType "Windows" -WebDeploy -WebDeployParameters $webDeployParams -PathsToPreserve @("logs")
#>

function Publish-DosWebApplication {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Error" -Message "$_ does not exist. Please enter valid path." -ErrorAction Stop
            }
            else {
                $true
            }
        })]
        [string] $WebAppPackagePath,
        [string] $SettingsXmlPath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [ValidateLength(1,64)]
        [ValidateScript({
            if ($_ -match '[^a-zA-Z0-9]') {
                Write-DosMessage -Level "Error" -Message "$_ must only contain alphanumeric values. Please remove special characters." -ErrorAction Stop
            }
            else {
                $true
            }
        })]
        [string] $AppPoolName,
        [PSCredential] $AppPoolCredential,
        [ValidateSet("Windows", "Anonymous")]
        [string[]] $AuthenticationType,
        [switch] $WebDeploy,
        [string] $AppName,
        [string] $IISWebSite = "Default Web Site",
        [System.Collections.ArrayList] $WebDeployParameters,
        [switch] $NoCredential,
        [string[]] $PathsToPreserve = @()
    )

    Test-ElevatedPermission
    Import-Module WebAdministration -Force
    
    if ($PathsToPreserve.Length -gt 0) {
        for ($i = 0; $i -lt $PathsToPreserve.Length; $i++) {
            $PathsToPreserve[$i] = $PathsToPreserve[$i].Trim("\", "/", " ")
        }
    }

    ###Only allow one source of input for webdeploy args

    try {
        if ($NoCredential.IsPresent) {
            Write-DosMessage -Level "Information" -Message "The NoCredential parameter was provided. Proceeding to application pool validation."
    
            if(!(Test-Path "IIS:\AppPools\$AppPoolName" -PathType Container)){
                New-AppPool -IISAppPoolName $AppPoolName
            }
            else {
                Set-AppPoolSettings -IISAppPoolName $AppPoolName -NoCredential $NoCredential.IsPresent
            }
        }
        else {
            if(!(Test-Path "IIS:\AppPools\$AppPoolName" -PathType Container)){
                if($AppPoolCredential -eq $null){
                    Write-DosMessage -Level "Fatal" -Message "No app pool found named $AppPoolName and no credentials specified. Please specify credentials -AppPoolCredential if you want to create a new application pool"
                    return
                }
                
                New-AppPool -IISAppPoolName $AppPoolName -IdentityCredential $AppPoolCredential
            }
            else {
                $existingAppPool = Get-Item -Path "IIS:\AppPools\$AppPoolName"

                # if app pool exists and credential is passed in / checks if the existing credential is the same / if it differs we fail out
                if ($null -ne $AppPoolCredential) {
                    if ($existingAppPool.processModel.userName -ne $AppPoolCredential.UserName -or $existingAppPool.processModel.password -ne $AppPoolCredential.GetNetworkCredential().Password) {
                        Write-DosMessage -Level "Fatal" -Message "The '$AppPoolName' app pool has an identity configured that differs from the identity credential provided. Halting deployment."
                    }
                }

                Set-AppPoolSettings -IISAppPoolName $AppPoolName -IdentityCredential $AppPoolCredential
                Write-DosMessage -Level "Information" -Message "Application Pool: $AppPoolName already exists. Deploying '$WebAppPackagePath' to $AppPoolName"
            }
        }
        
        $appPool = Get-Item "IIS:\AppPools\$AppPoolName"
        $appPool.Start()
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error occured getting, creating, or updating a IIS application pool. Exception: $($_.Exception)"
        return
    }

    #Deploy Website to IIS
    if ($WebDeploy.IsPresent){

        if([String]::IsNullOrEmpty($SettingsXmlPath) -and ($null -eq $WebDeployParameters)){
            Write-DosMessage -Level "Error" -Message "Must provide parameters through a settings xml file or webdeployparameters when deploying a web deploy application"
            return
        }

        if($SettingsXmlPath) {
            try {
                [xml] $parsedXml = Get-Content $SettingsXmlPath
            }
            catch {
                Write-DosMessage -Level "Error" -Message "Error occured parsing xml settings file. Exception: $($_.Exception)"
                return
            }
        }

        try {
            Write-DosMessage -Level "Information" -Message "Attempting to retrieve the IIS web application information."
            $siteName,$appNameFromSettings = Get-IisWebAppInfo -SettingsXmlPath $SettingsXmlPath -WebDeployParameters $WebDeployParameters -ParsedXml $parsedXml
            Write-DosMessage -Level "Information" -Message "Successfully retrieved the IIS web application information."
            Write-DosMessage -Level "Information" -Message "Deploying '$appNameFromSettings' through WebDeploy."
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Unable to find node containing IIS Web Application Name values"
            return
        }
        
        Publish-WebDeployWebApp -WebDeployPackageFilePath $WebAppPackagePath -WebDeployParameterFilePath $SettingsXmlPath -WebParameters $WebDeployParameters -AppName $appNameFromSettings -IISWebSite $IISWebSite -AppPoolName $AppPoolName -PathsToPreserve $PathsToPreserve

        Set-ApplicationPool -SiteName $siteName -AppName $appNameFromSettings -AppPoolName $AppPoolName
        Set-AuthenticationType -SiteName $siteName -AppName $appNameFromSettings -AuthenticationType $AuthenticationType

        Write-DosTelemetry -Message "Publish-DosWebApplication called and published using Publish-WebDeployWebApp."
    }
    else {
        if([string]::IsNullOrEmpty($AppName)){
            Write-DosMessage -Level "Fatal" -Message "AppName must be non-null and non empty"
        }

        Publish-DotNetCoreWebApp -WebApplicationPackagePath $WebAppPackagePath -AppName $AppName -IISWebSite $IISWebSite -AppPoolName $AppPoolName -PathsToPreserve $PathsToPreserve
        
        Set-ApplicationPool -SiteName $IISWebSite -AppName $AppName -AppPoolName $AppPoolName

        if (-Not ([string]::IsNullOrEmpty($AuthenticationType))) {
            Install-UrlRewrite
            Set-AuthenticationType -SiteName $IISWebSite -AppName $AppName -AuthenticationType $AuthenticationType
        }

        Write-DosTelemetry -Message "Publish-DosWebApplication called and published using Publish-DotNetCoreWebApp."
    }
}

function Get-IisWebAppInfo {
    param (
        [string] $SettingsXmlPath,
        [System.Collections.ArrayList] $WebDeployParameters,
        [xml] $ParsedXml
    )

    if($SettingsXmlPath){
        # Parameter childnodes can be different
        $iisParameter = $ParsedXml.parameters.ChildNodes | Where-Object { $_.name -eq "IIS Web Application Name" }

        # Parameter attribute names differ (eg. defaultValue and value)
        $iisAppPath = $iisParameter.Attributes | Where-Object { $_ -like "*value" }
        $siteName,$appNameFromSettings = $iisAppPath.'#text'.split('/')
    }else{
        # Parameter attribute names differ (eg. defaultValue and value)
        foreach ($param in $WebDeployParameters)
        {
            if($param.Name -eq "IIS Web Application Name"){
                $iisAppPath = $param.Value
            }
        }
        $siteName,$appNameFromSettings = $iisAppPath.split('/')
    }

    return $siteName,$appNameFromSettings
}

function Set-ApplicationPool {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    param (
        [string] $SiteName,
        [string] $AppName,
        [string] $AppPoolName
    )

    try {
        # Set-Application Pool with specific app
        Push-Location -Path IIS:\Sites\$SiteName\
        Set-ItemProperty -Path $AppName -Name applicationPool -Value $AppPoolName
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error occured associating application to the app pool. Exception: $($_.Exception)"
        return
    }
    finally {
        Pop-Location
    }
}

function Set-AuthenticationType {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    param (
        [string] $SiteName,
        [string] $AppName,
        [string[]] $AuthenticationType
    )

    try{
        Push-Location -Path IIS:\Sites\$SiteName\$AppName
        # Set-Authentication and transform the web config
        Set-IISAuthentication -AuthenticationType $AuthenticationType -SiteName $SiteName -ApplicationName $AppName
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error occured altering authentication types. Exception: $($_.Exception)"
        return
    }
    finally {
        Pop-Location
    }
}

function Install-UrlRewrite {
    # Check if URL Rewrite is installed before setting authentication
    $urlRewriteRegistry = "HKLM:\SOFTWARE\Microsoft\IIS Extensions\URL Rewrite"

    if (-Not (Test-Path $urlRewriteRegistry)) {
        Write-DosMessage -Level "Information" -Message "UrlRewrite not installed"

        # Install Web Platform Installer if not present
        if (-Not (Test-Path "$($env:ProgramFiles)\Microsoft\Web Platform Installer")) {
            Write-DosMessage -Level "Information" -Message "Web Platform Installer not found"
            Get-WebRequestDownload "https://go.microsoft.com/fwlink/?LinkId=287166" -OutFile "$PSScriptRoot\Web-Platform-Install.msi"
            Write-DosMessage -Level "Information" -Message "Installing Web Platform Installer"
            Start-Process "$PSScriptRoot\Web-Platform-Install.msi" '/qn' -PassThru | Wait-Process
            Remove-Item "$PSScriptRoot\Web-Platform-Install.msi"
        }

        # Install UrlRewrite using Web Platform Installer
        if (Test-Path "$($env:ProgramFiles)\Microsoft\Web Platform Installer\WebpiCmd.exe") {
            Write-DosMessage -Level "Information" -Message "Installing UrlRewrite"
            Start-Process "$($env:ProgramFiles)\Microsoft\Web Platform Installer\WebpiCmd.exe" "/Install /Products:'UrlRewrite2' /AcceptEULA" -PassThru | Wait-Process

            if (-Not (Test-Path $urlRewriteRegistry)) {
                Write-DosMessage -Level "Warning" -Message "UrlRewrite did not install correctly"
            }
        }
        else {
            Write-DosMessage -Level "Warning" -Message "Unable to install UrlRewrite, WebpiCmd.exe not found at $($env:ProgramFiles)\Microsoft\Web Platform Installer\WebpiCmd.exe"
        }
    }
}


<#
    .SYNOPSIS
    Removes an installation scope/section from the provided config store
     
    .DESCRIPTION
    Removes an installation scope/section from the provided config store
     
    .PARAMETER configSection
    Config scope that the contains the configSetting that will be removed
 
    .PARAMETER ConfigStore
    Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}.
    Current iteration only supports XML Files.
         
    .EXAMPLE
    Remove-DosConfigSection -configSection "common" -configStore $configStore
     
    .NOTES
    General notes
#>


function Remove-DosConfigSection {
    
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [String] $configSection,
        [Parameter(Mandatory=$true)]
        [hashtable] $configStore
    )
    
    if(!(Confirm-ConfigStore -ConfigStore $configStore)) {
        Write-DosMessage -Level Fatal -Message "configStore is invalid (Type: $($configstore.Type); Format: $($configstore.Format); Path: $($configstore.Path)"
        return
    }

    if(-not($configStore.Format -eq 'XML' -and $configStore.Type -eq 'File')){
        Write-DosMessage -Level Fatal -Message "Remove-DosConfigSection is not implemented for this type of configstore (Type: $($configstore.Type); Format: $($configstore.Format); Path: $($configstore.Path)"
        return
    }
    
    $installConfigXml = [xml](Get-Content "$($configStore.Path)")

    $sectionSettings = $installConfigXml.installation.settings.scope | Where-Object {$_.name -eq $configSection}

    if (-not($sectionSettings) -or ($sectionSettings.Count -eq 0)) {
        #if the scope doesn't exist no problem, do nothing
        Write-DosMessage -Level Information -Message "$($configStore.Path) didn't have a $configSection scope. No action taken."
        return
    }

    #make the config section lowercase and then get the node to delete (case insensitive)
    $configSection = $configSection.ToLower()
    $nodeToDelete = $installConfigXml.selectnodes("/installation/settings/scope[translate(@name,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='$configSection']")
    
    if($nodeToDelete){
        $nodeToDelete | Foreach-Object{$_.parentnode.removechild($_)} | out-null
        if($PSCmdlet.ShouldProcess("Delete '$configSection' scope in install config")){
            Write-DosMessage -Level Information -Message "Removing $configSection from $($configStore.Path)."
            Save-DosConfigStore -configStoreObject $installConfigXml -configStore $configStore
        }
    } else {
        Write-DosMessage -Level 'Warning' -Message "$($configStore.Path) had a '$configSection' scope but we couldn't find it with XPath. No action taken."
    }
    
}


<#
    .SYNOPSIS
    Removes an installation variable from the provided config scope
     
    .DESCRIPTION
    Removes an installation variable and value from the provided config scope
     
    .PARAMETER configSection
    Config scope that the contains the configSetting that will be removed
     
    .PARAMETER configSetting
    Variable that will be removed
     
    .PARAMETER ConfigStore
    Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}.
    Current iteration only supports XML Files.
     
    .EXAMPLE
    Remove-DosConfigValue -InstallConfigPath $path -configSection "common" -configSetting "sqlServerAddress"
     
    .NOTES
    General notes
#>


function Remove-DosConfigValue {
    
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [String] $configSection,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [String] $configSetting,
        [hashtable] $configStore = @{Type = "File"; Format = "XML"; Path = "install.config"}
    )
    
    if(!(Confirm-ConfigStore -ConfigStore $configStore)) {
        Write-DosMessage -Level Fatal -Message "configStore is invalid (Type: $($configstore.Type); Format: $($configstore.Format); Path: $($configstore.Path)"
        return
    }

    if(-not($configStore.Format -eq 'XML' -and $configStore.Type -eq 'File')){
        Write-DosMessage -Level Fatal -Message "Remove-DosConfigValue is not implemented for this type of configstore (Type: $($configstore.Type); Format: $($configstore.Format); Path: $($configstore.Path)"
        return
    }
    

    #This doesn't work because Get-DosConfigValues doesn't return empty values
    ##$sectionSettings = Get-DosConfigValues -ConfigStore $configStore -Scope $configSection

    $installConfigXml = [xml](Get-Content "$($configStore.Path)")

    $sectionSettings = $installConfigXml.installation.settings.scope | Where-Object {$_.name -eq $configSection}

    if (-not($sectionSettings) -or ($sectionSettings.Count -eq 0)) {
        #if the scope doesn't exist no problem, do nothing
        Write-DosMessage -Level Information -Message "$($configStore.Path) didn't have a $configSection scope. No action taken."
        return
    }

    $existingSetting = $sectionSettings.variable | Where-Object {$_.name -eq $configSetting}

    if(!$existingSetting){
        #if the existing variable in the scope doesn't exist, do nothing
        Write-DosMessage -Level Information -Message "$($configStore.Path) didn't have a $configSection.$configSetting value. No action taken."
        return
    }


    #get the node to delete
    $nodeToDelete = $installConfigXml.selectnodes("/installation/settings/scope[@name='$configSection']/variable[@name='$configSetting']")

    $configSection = $configSection.ToLower()
    $configSetting = $configSetting.ToLower()
    $nodeToDelete = $installConfigXml.selectnodes("/installation/settings/scope[translate(@name,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='$configSection']/variable[translate(@name,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='$configSetting']")

    
    if($nodeToDelete){
        $nodeToDelete | Foreach-Object{$_.parentnode.removechild($_)} | out-null
        if($PSCmdlet.ShouldProcess("Delete $configSection.$configSetting in install config")){
            Write-DosMessage -Level Information -Message "Removing $configSection.$configSetting from $($configStore.Path)."
            Save-DosConfigStore -configStoreObject $installConfigXml -configStore $configStore
        }
    } else {
        Write-DosMessage -Level Warning -Message "$($configStore.Path) had a $configSection.$configSetting value but we couldn't find it with XPath. No action taken."
    }
    
   
}


function Remove-DosWebApplication {
    <#
        .SYNOPSIS
        Remove a DOS web application from IIS
 
        .DESCRIPTION
        Removes a given DOS web application, physical directory, and application pool
        if no other applications are associated with it.
 
        .PARAMETER ApplicationName
        The name of the DOS Web application in IIS.
 
        .PARAMETER IISWebSite
        Specifies the IIS site from which to remove the application. Defaults to "Default Web Site"
 
        .EXAMPLE
        Remove-DosWebApplication -ApplicationName "Atlas4" -IISWebSite "Default Web Site"
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]
    Param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $ApplicationName,
        [string] $IISWebSite = "Default Web Site"
    )
    Import-Module WebAdministration -Force

    $iisPath = "IIS:\Sites\$($IISWebSite)\$($ApplicationName)"
    $webApp = Get-WebApplication -Name $ApplicationName -Site $IISWebSite

    if ($webApp) {
        $webApplicationFolder = Get-WebFilePath -PSPath $iisPath
        Write-DosMessage -Level "Information" -Message "Removing web application '$iisPath'"
        Remove-WebApplication -Name $ApplicationName -Site $IISWebSite
        Write-DosMessage -Level "Information" -Message "Removing folder '$webApplicationFolder'"
        Remove-Item -Path $webApplicationFolder -Recurse

        # Remove app pool if application count is zero
        $appPoolName = $webApp.applicationPool
        $appCount = (Get-WebConfigurationProperty "/system.applicationHost/sites/site/application[@applicationPool='$appPoolName']" "machine/webroot/apphost" -name path).Count
        if ($appCount -eq 0) {
            Write-DosMessage -Level "Information" -Message "Removing application pool '$appPoolName'"
            Remove-WebAppPool -Name $appPoolName
        }
        else {
            Write-DosMessage -Level "Warning" -Message "Application pool '$appPoolName' was not removed since other applications are currently bound to it."
        }
    }
}



function Remove-IISUrlRewriteRule {
    <#
        .SYNOPSIS
        Removes URL rewrite rule
 
        .DESCRIPTION
        Removes a given URL rewrite rule from IIS.
 
        .PARAMETER RuleName
        The unique name of the rule to remove.
 
        .PARAMETER IISWebSite
        The IIS site from which to remove the application. Defaults to "Default Web Site"
 
        .EXAMPLE
        Remove-IISUrlRewriteRule -RuleName "Atlas4-Atlas-Redirect" -IISWebSite "Default Web Site"
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="WhatIf support not implemented.")]

    Param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $RuleName,
        [string] $IISWebSite = "Default Web Site"
    )
    Import-Module WebAdministration -Force
    $iisPath = "IIS:\Sites\$($IISWebSite)"
    $ruleFilter = "/system.webserver/rewrite/rules/rule[@name='$RuleName']"

    $exists = Get-WebConfigurationProperty -PSPath $iisPath -Filter $ruleFilter -Name *
    if ($exists) {
        Write-DosMessage -Level "Information" -Message "Removing URL Rewrite rule '$RuleName'."
        Clear-WebConfiguration -PSPath $iisPath -Filter $ruleFilter
    }
}



<#
    .SYNOPSIS
    Saves the config store
     
    .DESCRIPTION
    Saves the config store (currently only works for XML file types)
     
    .PARAMETER configStoreObject
    The object that represents the configuration settings that need to be saved.
 
    .EXAMPLE
    Save-DosConfigStore -configStoreObject $installConfigXml -configStore @{Type = "File"; Format = "XML"; Path = "install.config"}
    Save-DosConfigStore -configStoreObject $installConfigXml -configStore $configStore
     
    .NOTES
    General notes
#>

function Save-DosConfigStore {
    
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [System.Object]$configStoreObject
        ,[Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $configStore
    )

    if($configStore.Type -eq "File" -and $configStore.Format -eq "XML"){
        Save-DosConfigStoreXml -installConfigXml $configStoreObject -pathToInstallConfig $configStore.Path
    } else {
        Write-DosMessage -Level 'Fatal' -Message "Save-DosConfigStore not implemented yet for type: $($configStore.Type), format: $($configStore.Format)"
    }

}


<#
    .SYNOPSIS
    NON PUBLIC - Saves the xml file that represents the install.config
     
    .DESCRIPTION
    Saves the xml file that represents the install.config
     
    .PARAMETER installConfigXml
    The xml object that represents the install.config
     
    .PARAMETER pathToInstallConfig
    Full file path to the install.config file that will be saved
 
     
    .EXAMPLE
    Save-DosConfigStoreXml -installConfigXml $installConfigXml -pathToInstallConfig 'C:\Program Files\Health Catalyst\install.config'
     
    .NOTES
    General notes
    #>

function Save-DosConfigStoreXml {
    
    param (
        [Parameter(Mandatory=$true)]
        [xml]$installConfigXml,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [String]$pathToInstallConfig
    )
    $installConfigXml.Save("$pathToInstallConfig")
    
}


<#
    .SYNOPSIS
    Adds an installation variable to the provided config scope
     
    .DESCRIPTION
    Adds or updates an installation variable and value to the provided config scope
     
    .PARAMETER configSection
    Config scope that the variable and value will be saved to
     
    .PARAMETER configSetting
    Variable that will be saved
     
    .PARAMETER configValue
    Value of the variable to be saved
     
    .PARAMETER KeepExisting
    Will not overwrite the existing value if present
     
    .PARAMETER ConfigStore
    Accepts a powershell hashtable or dictionary with key value pairs following the example, $configStore = @{Type = "File"; Format = "XML"; Path = "C:\Path\To\File"}.
    Current iteration only supports XML Files.
     
    .EXAMPLE
    Set-DosConfigValue -PathToInstallConfig $path -configSection "common" -configSetting "sqlServerAddress" -configValue $dbFQN -configstore $configstore
    Set-DosConfigValue -PathToInstallConfig $path -configSection "common" -configSetting "sqlServerAddress" -configValue $dbFQN -configstore $configstore -KeepExisting
     
    .NOTES
    General notes
    #>


function Set-DosConfigValue {
    
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [String] $configSection,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [String] $configSetting,
        [Parameter(Mandatory=$true)]
        [AllowEmptyString()]
        [String] $configValue,
        [Switch] $KeepExisting,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [hashtable] $configStore
    )
    
    if(!(Confirm-ConfigStore -ConfigStore $configStore)) {
        Write-DosMessage -Level Fatal -Message "configStore is invalid"
        return
    }

    if($configStore.Format -ne 'XML' -and $configStore.Type -ne 'File'){
        Write-DosMessage -Level "Fatal" -Message "Set-DosConfigValue is not implemented yet for type: $($configStore.Type), format: $($configStore.Format)"
        return
    }
    $somethingChanged = $false
    
    Write-DosMessage -Level "Debug" -Message "Attempting to add $configSection.$configSetting=$configValue with KeepExisting=($KeepExisting) into $($configStore.Path)."

    $installConfigXml = [xml](Get-Content "$($configStore.Path)")
    $sectionSettings = $installConfigXml.installation.settings.scope | Where-Object {$_.name -eq $configSection}
    if (!$sectionSettings) {
        #if the scope doesn't exist, create it
        Write-DosMessage -Level "Debug" -Message "Scope ""$configSection"" doesn't exist, creating it."
        $sectionSettings = $installConfigXml.CreateElement("scope")
        $nameAttribute = $installConfigXml.CreateAttribute("name")
        $nameAttribute.Value = $configSection

        $sectionSettings.Attributes.Append($nameAttribute) | Out-Null
        $installConfigXml.installation.SelectSingleNode("settings").AppendChild($sectionSettings) | Out-Null
        $somethingChanged = $true
    }

    $existingSetting = $sectionSettings.variable | Where-Object {$_.name -eq $configSetting}
    if(!$existingSetting){
        #if the existing variable in the scope doesn't exist, create it.
        $setting = $installConfigXml.CreateElement("variable")
        
        $nameAttribute = $installConfigXml.CreateAttribute("name")
        $nameAttribute.Value = $configSetting
        $setting.Attributes.Append($nameAttribute) | Out-Null

        #now that the existing variable is created, set the value.
        $valueAttribute = $installConfigXml.CreateAttribute("value")
        $valueAttribute.Value = $configValue
        $setting.Attributes.Append($valueAttribute) | Out-Null
        Write-DosMessage -Level "Debug" -Message "Adding setting ""$configSetting"" with value ""$configValue"" to the ""$configSection"" scope"

        $sectionSettings.AppendChild($setting) | Out-Null
        $somethingChanged = $true
    } elseif([string]::IsNullOrEmpty($existingSetting.value)){
        #the current value is null or empty, so we are going to overwrite it, regardless if it says keepexisting or not.
        Write-DosMessage -Level "Debug" -Message "No existing value found for setting ""$configSetting"" in scope ""$configSection"", populating with ""$configValue"""
        $existingSetting.value = $configValue
        $somethingChanged = $true
    } elseif (-not([string]::IsNullOrEmpty($existingSetting.value))) {
        #There is an existing setting
        #That existing setting has a value.


        if($KeepExisting){
            #Don't change the value and let the user know.
            Write-DosMessage -Level "Debug" -Message "Existing value ""$($existingSetting.value)"" found for setting ""$configSetting"" in scope ""$configSection"" but KeepExisting was passed in, leaving value as-is."
        } else {
            #Do change the value and let the user know.
            Write-DosMessage -Level "Debug" -Message "Existing value ""$($existingSetting.value)"" found for setting ""$configSetting"" in scope ""$configSection"", replacing with ""$configValue"""
            $existingSetting.value = $configValue
            $somethingChanged = $true
        }
        
    } else {
        Write-DosMessage -Level "Fatal" -Message "You've reached an else block that you shouldn't have been able to reach. The cake is a lie."
    }
    if($somethingChanged){
        if($PSCmdlet.ShouldProcess("Save changes to install config")){
            Save-DosConfigStore -configStoreObject $installConfigXml -configStore $configStore
            Write-DosMessage -Level "Debug" -Message "Successfully added $configSection.$configSetting=$configValue with KeepExisting=($KeepExisting) into $($configStore.Path)."
        }
        
    } else {
        Write-DosMessage -Level "Debug" -Message "No changes made to $($configStore.Path) for $configSection.$configSetting=$configValue with KeepExisting=($KeepExisting)."
    }
    
}


<#
    .SYNOPSIS
 
    Sets the the global IIS configuration authentication settings to allow each application to override authentication with it's own configuration settings
 
    .DESCRIPTION
 
    For more reading, see: https://docs.microsoft.com/en-us/iis/get-started/planning-for-security/how-to-use-locking-in-iis-configuration and https://docs.microsoft.com/en-us/iis/configuration/system.webserver/security/authentication/
     
    .EXAMPLE
 
    Set-GlobalIISAuthentication
#>


function Set-DosGlobalIISAuthentication {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param()

    BEGIN {
        Add-Assembly -Assemblies "$env:systemroot\system32\inetsrv\Microsoft.Web.Administration.dll"
    }
    PROCESS{

        Write-DosMessage -Level "Verbose" -Message "Fetching configuration sections"
        $manager = new-object Microsoft.Web.Administration.ServerManager
        $config = $manager.GetApplicationHostConfiguration()
        $section = $config.GetSection("system.webServer/security/authentication/windowsAuthentication")
        $section.OverrideMode = "Allow"

        $section = $config.GetSection("system.webServer/security/authentication/anonymousAuthentication")
        $section.OverrideMode = "Allow" 

        if($PSCmdlet.ShouldProcess("Committing IIS global authentication settings")){
            Write-DosMessage -Level "Verbose" -Message "Committing changes"
            $manager.CommitChanges()  
        }
        Write-DosTelemetry -Message "Set-DosGlobalIISAuthentication called."
    }
}


<#
    .SYNOPSIS
 
    Configure application logger with specified set of parameters..
 
    .DESCRIPTION
 
    Configures and creates a Serilog logger, capable of logging to the console, a file, or both.
 
    .PARAMETER LoggingMode
 
    Logger message output. Valid modes include: Console, File and Both.
 
    .PARAMETER MinimumLoggingLevel
 
    The minimum logging level that will be written to the logger (file and console).
 
    .PARAMETER LogFilePath
 
    Path to logging file.
 
    .EXAMPLE
 
    Set-DosMessageConfiguration -LoggingMode "Both" -LogFilePath "C:\Path\To\log.txt"
#>


function Set-DosMessageConfiguration {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="Logging messages will not change system state.")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "", Justification="We need a global variable to avoid weird scope issues when turning on serilog selflog. It will normally be null")]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateSet("File", "Console", "Both")]
        [string] $LoggingMode,
        [Parameter(Mandatory=$true)]
        [ValidateSet("Verbose", "Debug", "Information", "Warning", "Error", "Fatal")]
        [string] $MinimumLoggingLevel,
        [ValidateScript({
            if (!(Test-Path $_)) {
                try {
                    New-Item $_ -Type File -Force -ErrorAction Stop
                }
                catch {
                    Write-DosMessage -Level "Error" -Message "$_"
                }
                $true
            }
            else {
                $true
            }
        })]
        [string] $LogFilePath
    )
    
    # Parameter Validation added because if $LogFilePath isn't provided the [ValidateScript] will not run
    # Test case. If user tries to configure a file logger without providing a logfilepath. Associated unit test in Set-DosMessageConfiguration.tests.ps1 line 51-59
    if (($LoggingMode -eq "File" -or $LoggingMode -eq "Both") -and [string]::IsNullOrEmpty($LogFilePath)) {
        Write-DosMessage -Level "Error" -Message "You cannot configure a file logger without providing a the LogFilePath parameter."
    } 

    try {
        [SerilogBridge.SerilogBridge]::CreateDosLogger($LoggingMode, $MinimumLoggingLevel, $LogFilePath, $global:serilogSelfLogEnabled)
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error creating logger. Exception: $($_.Exception)"
    }

    Write-DosTelemetry -Message "Set-DosMessageConfiguration called."
}


<#
    .SYNOPSIS
 
    Configures the telemetry logger with a specific telemetry key (current defaults to DOS Install Application Insights key).
 
    .DESCRIPTION
 
    Pass in the application insights key and a optional opt out parameter.
 
    .PARAMETER TelemetryKey
 
    Currently associate the telemetry logger with an application insights key.
 
    .PARAMETER TelemetryOptOut
 
    Switch parameter, if included will opt out of the telemetry logger.
 
    .EXAMPLE
 
    Set-DosTelemetry -TelemetryKey "testkey" -TelemetryOptOut
#>


function Set-DosTelemetry {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="Logging messages will not change system state.")]
    [CmdletBinding()]
    param(
        [ValidateNotNullorEmpty()]
        [string] $TelemetryKey,
        [switch] $TelemetryOptOut
    )
    
    # Converts to the appropriate boolean type used for the C# serilogbridge
    Write-DosMessage -Level "Verbose" -Message "Converting Powershell boolean into primitive type to be used in Serilog C# class"
    $telemetryConfirmation = [System.Management.Automation.LanguagePrimitives]::ConvertTo($TelemetryOptOut.IsPresent,[System.Type]::GetType($TelemetryOptOut.IsPresent.GetType().FullName))

    try {
        Write-DosMessage -Level "Information" -Message "Creating Telemetry Logger using '$TelemetryKey' key."
        [SerilogBridge.TelemetryBridge]::CreateDosTelemetryLogger($TelemetryKey, $telemetryConfirmation)
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error creating telemetry logger. Exception: $($_.Exception)"
    }

    Write-DosTelemetry -Message "Set-DosTelemetry called."
}


<#
    .SYNOPSIS
 
    Given a certificate thumbprint and an encrypted value, Unprotect-DosInstallerSecret will return a decrypted secret value.
 
    .DESCRIPTION
 
    Pass in a certificate thumbprint (generally stored in a configuration file) and the encrypted secret value, and Unprotect-DosInstallerSecret will return the decrypted value in string format.
 
    .PARAMETER CertificateThumprint
 
    Certificate Thumbprint of the certificate that will be used for decryption.
 
    .PARAMETER EncryptedInstallerSecretValue
 
    Encrypted value that will be unprotected.
 
    .EXAMPLE
 
    $decryptedValue = Unprotect-DosInstallerSecret -CertificateThumprint $certThumbprint -EncryptedInstallerSecretValue $encryptedSecret
#>


function Unprotect-DosInstallerSecret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ParameterSetName = "CertificateSecret")]
        [ValidateNotNullorEmpty()]
        [string] $CertificateThumprint,
        [Parameter(Mandatory=$true, ParameterSetName = "CertificateSecret")]
        [ValidateNotNullorEmpty()]
        [string] $EncryptedInstallerSecretValue
    )

    $secret = ''

    if ($PSCmdlet.ParameterSetName -eq "CertificateSecret") {
        try{
            Write-DosMessage -Level "Debug" -Message "Attempting to retrieve encryption certificate using the certificate thumbprint provided."
            $encryptionCertificate = Get-EncryptionCertificate $CertificateThumprint
            Write-DosMessage -Level "Debug" -Message "Successfully retrieved encryption certificate."
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Could not get encryption certificte with thumbprint $CertificateThumprint. Exception: $($_.Exception)"
        }
    
        try {
            Write-DosMessage -Level "Debug" -Message "Using encryption certificate to decrypt the installer secret provided."

            $encryptedValue = $EncryptedInstallerSecretValue
            if ($encryptedValue.StartsWith("!!enc!!:")) {
                $encryptedValue = $encryptedValue.Replace("!!enc!!:", "")
            }

            $secret = Get-DecryptedString -Certificate $encryptionCertificate -EncryptedValue $encryptedValue
            Write-DosMessage -Level "Debug" -Message "Successfully decrypted the installer secret provided."
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Error attempting to decrypt installer secret. Exception: $($_.Exception)."
        }
    }

    return $secret
}

function Get-EncryptionCertificate {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $CertificateThumprint
    )

    $localCertPath = "Cert:\LocalMachine\My"

    Write-DosMessage -Level "Verbose" -Message "Cleaning certificate thumbprint for any invalid characters."
    $cleanCertificateThumbprint = $CertificateThumprint -replace '[^a-zA-Z0-9]', ''
    try {
        Write-DosMessage -Level "Debug" -Message "Pulling certificate from $localCertPath."
        $certificate = Get-Item "$localCertPath\$cleanCertificateThumbprint" -ErrorAction Stop
        Write-DosMessage -Level "Debug" -Message "Successfully retrieved certificate from $localCertPath."
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error retrieving certificate from $localCertPath. Confirm that certificate with $cleanCertificateThumbprint exists on the machine. Exception: $($_.Exception)."
    }

    return $certificate
}

function Get-DecryptedString {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [X509Certificate] $Certificate,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $EncryptedValue
    )
    
    try {
        Write-DosMessage -Level "Debug" -Message "Decrypting value..."
        $clearTextValue = Get-DecryptedValueDotNet -Certificate $Certificate -EncryptedValue $EncryptedValue
        Write-DosMessage -Level "Debug" -Message "Successfully decrypted installer secret."
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error decrypting value provided. Please verify that encryption certificate is valid. Exception: $($_.Exception)."
    }
    return $clearTextValue
}

function Get-DecryptedValueDotNet {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [X509Certificate] $Certificate,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $EncryptedValue
    )

    if ($null -eq $Certificate.PrivateKey) {
        Write-DosMessage -Level "Debug" -Message "CNG certificate detected."
        try {
            Write-DosMessage -Level "Debug" -Message "Pulling RSA private key from certificate with thumbprint provided."
            $privateKey = Get-CNGRSAPrivateKey -Certificate $Certificate
            Write-DosMessage -Level "Debug" -Message "Successfully retrieved RSA private key from certificate."
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Error pulling RSA private key from certificate with thumbprint $($Certificate.Thumbprint). Exception: $($_.Exception)."
        }

        try {
            Write-DosMessage -Level "Debug" -Message "Decrypting value..."
            $clearTextValue = Get-DecryptedValueCNGKey -privateKey $privateKey -EncryptedValue $EncryptedValue
            Write-DosMessage -Level "Debug" -Message "Successfully decrypted value."
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Error decrypting value provided. Please verify that encryption certificate is valid. Exception: $($_.Exception)."
        }
    }
    else {
        Write-DosMessage -Level "Debug" -Message "CSP certificate detected."
        try {
            Write-DosMessage -Level "Debug" -Message "Decrypting value..."
            $clearTextValue = Get-DecryptedValueCSPCertificateKey -Certificate $Certificate -EncryptedValue $EncryptedValue
            Write-DosMessage -Level "Debug" -Message "Successfully decrypted value."
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Error decrypting value provided. Please verify that encryption certificate is valid. Exception: $($_.Exception)."
        }
    }

    return $clearTextValue
}

function Get-CNGRSAPrivateKey {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [X509Certificate] $Certificate
    )

    return [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)
}

function Get-DecryptedValueCNGKey {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [System.Security.Cryptography.RSA] $privateKey,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $EncryptedValue
    )

    return [System.Text.Encoding]::UTF8.GetString($privateKey.Decrypt([System.Convert]::FromBase64String($EncryptedValue), [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA1))
}

function Get-DecryptedValueCSPCertificateKey {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [X509Certificate] $Certificate,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $EncryptedValue
    )

    return [System.Text.Encoding]::UTF8.GetString($Certificate.PrivateKey.Decrypt([System.Convert]::FromBase64String($EncryptedValue), $true))
}


<#
    .SYNOPSIS
 
    Formats the given input string by performing a search/replace.
 
    .DESCRIPTION
 
    Formats the given input string by performing a search/replace against each delimited targetpattern and replacing it with the replacement pattern in the same array position
 
    .PARAMETER TargetPatterns
 
    Array of strings representing the target patterns to search for and replace. Combines with delimiter to prevent aliasing. Must be equal in length to the replacement array
 
    .PARAMETER ReplacementPattern
 
    Array of replacement strings. Must be equal in length to the target array
 
    .PARAMETER Content
 
    String representing the content to perform a search/replace on. Must be non-null and not empty
 
    .PARAMETER Delimiter
 
    Delimiter to combine with the target replacement patterns to perform search/replace against. May be an empty string if no delimiter is necessary
 
    .EXAMPLE
 
    $formattedContent = Update-DosConfigContent -TargetPatterns $TargetPatterns -ReplacementPattern $ReplacementPattern -Delimiter $Delimiter -Content $configFileContent
#>

function Update-DosConfigContent{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [ValidateNotNull()]
        [Array] $TargetPatterns,
        [ValidateNotNull()]
        [Array] $ReplacementPattern,
        [ValidateNotNullOrEmpty()]
        [string] $Content,
        [ValidateNotNull()]
        [string] $Delimiter = ""
    )
    
    if($TargetPatterns.Length -ne $ReplacementPattern.Length){
        Write-DosMessage -Level "Error" -Message "Target patterns and replacement pattern length must be equal"
        return 
    }

    for($i = 0 ; $i -lt $TargetPatterns.Length; $i++){
        Write-DosMessage -Level "Verbose" -Message "Replacing $($TargetPatterns[$i]) with $($ReplacementPattern[$i])"
        $searchValue = "$Delimiter$($TargetPatterns[$i])$Delimiter"
        if($PSCmdlet.ShouldProcess("Modifying content via search and replace")){
            $Content = $Content.Replace($searchValue, $ReplacementPattern[$i])
        }
    }

    return $Content

    Write-DosTelemetry -Message "Update-DosConfigFile called."
}


<#
    .SYNOPSIS
 
    Formats the given input file by performing a search/replace. Writes the updated content back into the same file
 
    .DESCRIPTION
 
    Formats the given input string by performing a search/replace against each delimited targetpattern and replacing it with the replacement pattern in the same array position
 
    .PARAMETER TargetPatterns
 
    Array of strings representing the target patterns to search for and replace. Combines with delimiter to prevent aliasing. Must be equal in length to the replacement array
 
    .PARAMETER ReplacementPattern
 
    Array of replacement strings. Must be equal in length to the target array
 
    .PARAMETER FilePath
 
    Path to the file to update with the search/replace
 
    .PARAMETER Delimiter
 
    Delimiter to combine with the target replacement patterns to perform search/replace against. May be an empty string if no delimiter is necessary
 
    .EXAMPLE
 
    Update-DosConfigFile -TargetPatterns $targets -ReplacementPattern $replacements -Delimiter $delimiter -FilePath $ConfigFilePath
#>

function Update-DosConfigFile{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [ValidateNotNull()]
        [Array] $TargetPatterns,
        [ValidateNotNull()]
        [Array] $ReplacementPattern,
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Error" -Message "FilePath $_ does not exist. Please enter valid path." -ErrorAction Stop
            }
            else {
                $true
            }
        })]
        [string] $FilePath,
        [ValidateNotNull()]
        [string] $Delimiter = ""
    )
    
    Write-DosMessage -Level "Information" -Message "Performing search and replace on $FilePath"

    $configFileContent = Get-Content -Path $FilePath -Raw

    Write-DosMessage -Level "Debug" -Message "Pre-formatted content: $configFileContent"

    $formattedContent = Update-DosConfigContent -TargetPatterns $TargetPatterns -ReplacementPattern $ReplacementPattern -Delimiter $Delimiter -Content $configFileContent

    Write-DosMessage -Level "Debug" -Message "Formatted content: $formattedContent"

    if($PSCmdlet.ShouldProcess("Saving updated content back to file $FilePath")){
        Set-Content -Path $FilePath -Value $formattedContent
    }

}


<#
    .SYNOPSIS
 
    Formats the given input file by performing a search/replace.
 
    .DESCRIPTION
 
    Formats the given input string by performing a search/replace agaist pairs in the specified XML file
 
    .PARAMETER ConfigFilePath
 
    Configuration file to update with a search/replacement of patterns in the ReplacementPatternFilePath
 
    .PARAMETER ReplacementPatternFilePath
 
    File containing the delimiter and search/replace pairs to use to transform the config file
 
    .EXAMPLE
 
    Update-DosConfigFileFromInputFile -ConfigFilePath "C:\inetput\wwwroot\testapp\web.config" -ReplacementPatternFilePath ".\testappupdates.xml"
#>

function Update-DosConfigFileFromInputFile {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Error" -Message "ConfigFilePath $_ does not exist. Please enter valid path." -ErrorAction Stop
            }
            else {
                $true
            }
        })]
        [string] $ConfigFilePath,
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if (!(Test-Path $_)) {
                Write-DosMessage -Level "Error" -Message "ReplacementPatternFilePath $_ does not exist. Please enter valid path." -ErrorAction Stop
            }
            else {
                $true
            }
        })]
        [string] $ReplacementPatternFilePath
    )

    Write-DosMessage -Level "Verbose" -Message "Reading replacements from $ReplacementPatternFilePath"
    try{
        $replacementsXmlFile = [xml] (Get-Content $ReplacementPatternFilePath)
    }
    catch{
        Write-DosMessage -Level "Error" -Message "Unable to load xml in file $ReplacementPatternFilePath"
        return
    }

    if($null -eq $replacementsXmlFile.replacements) {
        Write-DosMessage -Level "Error" -Message "No replacements root found in specified replacement file $ReplacementPatternFilePath"
        return
    }
    if ($null -eq $replacementsXmlFile.replacements.pairs){
        Write-DosMessage -Level "Error" -Message "No replacements pairs found in specified replacement file $ReplacementPatternFilePath"
        return
    }

    $targets = $replacementsXmlFile.replacements.pairs.ChildNodes | ForEach-Object {$_.target}

    $replacements = $replacementsXmlFile.replacements.pairs.ChildNodes | ForEach-Object {$_.replacement}


    $delimiter = ""
    if($null -eq  $replacementsXmlFile.replacements.delimiter){
        Write-DosMessage -Level "Warning" -Message "No delimiter specified in replacement file $ReplacementPatternFilePath, assuming no delimiter"   
    }
    else{
        $delimiter = $replacementsXmlFile.replacements.delimiter
    }

    Write-DosMessage -Level "Debug" -Message "Have $($targets.Count) replacements and the delimiter is $delimiter"

    if($PSCmdlet.ShouldProcess("Updating config file $ConfigFilePath")){
        Update-DosConfigFile -TargetPatterns $targets -ReplacementPattern $replacements -Delimiter $delimiter -FilePath $ConfigFilePath
    }

}


<#
    .SYNOPSIS
 
    Updates the target DOS dac publish file to the specified database mount points
 
    .DESCRIPTION
 
    Take the passed in mount points and update the DOS dac publish xml file
 
    .PARAMETER PublishOptionsFilePath
 
    Path to publish options file - Required - See tests/SampleFiles/DefaultDacDeployOptions.xml for an example
 
    .PARAMETER dataMountPointFolder
 
    Path to the base data mount point folder
 
    .PARAMETER indexMountPointFolder
 
    Path to the base index mount point folder
 
    .PARAMETER logMountPointFolder
 
    Path to the base log mount point folder
 
    .EXAMPLE
 
    Update-DosMountPoint -PublishOptionsFilePath ".\test.publish.xml" -dataMountPointFolder "C:\SQLData" -indexMountPointFolder "C:\SQLData" -logMountPointFolder "C:\SQLData"
 
#>

function Update-DosMountPoint {
    [cmdletbinding(SupportsShouldProcess=$true)]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string]$PublishOptionsFilePath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string]$dataMountPointFolder,
        [string]$indexMountPointFolder,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string]$logMountPointFolder
    )
    
    [xml] $parsedPublishOptions = [xml] (Get-Content $PublishOptionsFilePath -ErrorAction Stop)
    # Mount Points are stored in the SqlCmdVariables
    $sqlCmdVariables = $parsedPublishOptions.Project.ItemGroup.SqlCmdVariable

    Write-DosMessage -Level "Verbose" -Message "Validating that data mount point folder exists at $dataMountPointFolder"
    if(!(Test-Path "$dataMountPointFolder")){
        try {
            Write-DosMessage -Level "Information" -Message "Data mount point folder does not exist, creating folder at $dataMountPointFolder"
            New-Item -ItemType directory -Path "$dataMountPointFolder"
            Write-DosMessage -Level "Verbose" -Message "Created data mount point folder at $dataMountPointFolder for $PublishOptionsFilePath"
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Could not create data mount point folder at $dataMountPointFolder for $PublishOptionsFilePath. Exception: $($_.Exception)"
        }
    }
    else {
        Write-DosMessage -Level "Information" -Message "Data mount point folder already exists at $dataMountPointFolder"
    }
    foreach ($dataMountPoint in $sqlCmdVariables | Where-Object {$_.Include -like "*Data*MountPoint"}) {
        $dataMountPoint.Value = $dataMountPointFolder
    }
    
    foreach ($dataMountPoint in $sqlCmdVariables | Where-Object {$_.Include -like "PrimaryMountPoint"}) {
        $dataMountPoint.Value = $dataMountPointFolder
    }

    Write-DosMessage -Level "Verbose" -Message "Validating that log mount point folder exists at $logMountPointFolder"
    if(!(Test-Path "$logMountPointFolder")){
        try {
            Write-DosMessage -Level "Information" -Message "Log mount point folder does not exist, creating folder at $logMountPointFolder"
            New-Item -ItemType directory -Path "$logMountPointFolder"
            Write-DosMessage -Level "Verbose" -Message "Created log mount point folder at $logMountPointFolder for $PublishOptionsFilePath"
        }
        catch {
            Write-DosMessage -Level "Error" -Message "Could not create log mount point folder at $logMountPointFolder for $PublishOptionsFilePath. Exception: $($_.Exception)"
        }
    }
    else {
        Write-DosMessage -Level "Information" -Message "Log mount point folder already exists at $logMountPointFolder"
    }
    foreach ($logMountPoint in $sqlCmdVariables | Where-Object {$_.Include -like "*Log*MountPoint"}) {
        $logMountPoint.Value = $logMountPointFolder
    }

    if($indexMountPointFolder){
        Write-DosMessage -Level "Verbose" -Message "Validating that index mount point folder exists at $indexMountPointFolder"
        if(!(Test-Path "$indexMountPointFolder")){
            try {
                Write-DosMessage -Level "Information" -Message "Index mount point folder does not exist, creating folder at $indexMountPointFolder"
                New-Item -ItemType directory -Path "$indexMountPointFolder"
                Write-DosMessage -Level "Verbose" -Message "Created index mount point folder at $indexMountPointFolder for $PublishOptionsFilePath"
            }
            catch {
                Write-DosMessage -Level "Error" -Message "Could not create index mount point folder at $indexMountPointFolder for $PublishOptionsFilePath. Exception: $($_.Exception)"
            }
        }
        else {
            Write-DosMessage -Level "Information" -Message "Index mount point folder already exists at $indexMountPointFolder"
        }
        foreach ($indexMountPoint in $sqlCmdVariables | Where-Object {$_.Include -like "*Index*MountPoint"}) {
            $indexMountPoint.Value = $indexMountPointFolder
        }
    }

    try {
        Write-DosMessage -Level "Information" -Message "Saving publish profile mount point settings"
        if ($pscmdlet.ShouldProcess($PublishOptionsFilePath,"Saving modifications to profile mount point settings")){
            $parsedPublishOptions.Save($PublishOptionsFilePath)
        }
    }
    catch {
        Write-DosMessage -Level "Fatal" -Message "Error occured while attempting to save mount point settings. Error $($_.Exception)"
        Write-DosTelemetry -Message "Finished attempt to update database mount point settings"
    }

}


<#
    .SYNOPSIS
 
    Write a logging message with a given severity to either a file, the console, or both.
 
    .DESCRIPTION
 
    Uses current logger configuration to log application messages based on severity.
 
    .PARAMETER Level
 
    Logger message level. Valid levels include: Verbose, Debug, Information, Warning, Error, and Fatal.
 
    .PARAMETER Message
 
    Message to be written in the log.
 
    .PARAMETER HeaderType
 
    Optional Header type that can be used to create dividers in a log file. Valid header types include: H1 and H2.
 
    .EXAMPLE
 
    Write-DosMessage -Level "Information" -Message "***[BEGIN]***" -HeaderType H2
    Write-DosMessage -Level "Information" -Message "Main Header" -HeaderType H1 # typically used at the beginning of a script
    Write-DosMessage -Level "Information" -Message "Step1 Header" -HeaderType H2 # typically used in the middle of script
    Write-DosMessage -Level "Information" -Message "Regular log message1."
    Write-DosMessage -Level "Information" -Message "Regular log message2."
    Write-DosMessage -Level "Information" -Message "Step2 Header" -HeaderType H2 # typically used in the middle of script
    Write-DosMessage -Level "Information" -Message "Regular log message3."
    Write-DosMessage -Level "Information" -Message "Regular log message4."
    Write-DosMessage -Level "Information" -Message "***[END]***" -HeaderType H2
 
    Write-DosMessage -Level "Fatal" -Message "Fatal Error Occured."
#>


function Write-DosMessage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet("Verbose", "Debug", "Information", "Warning", "Error", "Fatal")]
        [string] $Level,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $Message,
        [Parameter(Mandatory = $false)]
        [ValidateSet("H1", "H2")]
        [string] $HeaderType
    )
    $errorAction = $ErrorActionPreference

    # Used for mocking/testing
    if ($HeaderType) {
        $Width = 60;
        $Margin = 5;
        $Spacer = "-";
        $Message = "$($Spacer*$Margin)$Message$($Spacer*$Margin)";
        if ($Message.Length -gt ($Width - ($Margin * 2))) {
            $Width = $Message.Length + ($Margin * 2);
        }
        switch ($HeaderType) {
            "H1" { $Padding = 2 }
            "H2" { $Padding = 0 }
            default { $Padding = 0 }
        }
            
        LoadSerilog -Level "Information" -Message " "
        if ($Padding) {1..$Padding | ForEach-Object { LoadSerilog -Level $Level -Message ($Spacer * $Width)}}
        LoadSerilog -Level $Level -Message "$($Message)$($Spacer * ($Width - $Message.Length))"
        if ($Padding) {1..$Padding | ForEach-Object { LoadSerilog -Level $Level -Message ($Spacer * $Width)}}
    }
    else {
        LoadSerilog -Level $Level -Message $Message
    }
        
    # Silenty Continue do nothing
    # Default Throws on Fatal
    if ($errorAction -eq "Continue" -and $Level -eq "Fatal") {
        Throw $Message
    }
        
    # Stop throws on Error and Fatal Levels
    if ($errorAction -eq "Stop" -and ($Level -eq "Error" -or $Level -eq "Fatal")) {
        Throw $Message
    }
            
}

function LoadSerilog {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet("Verbose", "Debug", "Information", "Warning", "Error", "Fatal")]
        [string] $Level,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullorEmpty()]
        [string] $Message
    )
    [SerilogBridge.SerilogBridge]::WriteDosMessage($Level, $Message)
}


<#
    .SYNOPSIS
 
    Write a telemetry logging message with a severity to application insights.
 
    .DESCRIPTION
 
    Uses Information severity level by default. Requires a message to be passed in as well.
 
    .PARAMETER Message
 
    Message to be written in the telemetry log.
 
    .EXAMPLE
 
    Write-DosTelemetry -Message "Telemetry Message Here."
#>


function Write-DosTelemetry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $Message
    )

    try {
        # Seperate function call for mocking/testing
        LoadTelemetryBridge -Message $Message
    }
    catch {
        Write-DosMessage -Level "Error" -Message "Error writing telemetry message. Exception: $($_.Exception)"
    }
}

function LoadTelemetryBridge {
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [string] $Message
    )
    [SerilogBridge.TelemetryBridge]::WriteDosTelemetry($Message)
}
# SIG # Begin signature block
# MIIcSgYJKoZIhvcNAQcCoIIcOzCCHDcCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD1NIQ2CFNsQTkX
# 3JJwn3aE/MdKu8dQrhTJR3KxshZcVaCCCqMwggUwMIIEGKADAgECAhAECRgbX9W7
# ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEwMjIxMjAwMDBa
# Fw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lD
# ZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqGSIb3
# DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7RZmxOttE9X/l
# qJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p0WfTxvspJ8fT
# eyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj6YgsIJWuHEqH
# CN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grkV7tKtel05iv+
# bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHyDxL0xY4PwaLo
# LFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMBAAGjggHNMIIB
# yTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAK
# BggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9v
# Y3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHow
# eDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl
# ZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp
# Z2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgGCmCGSAGG/WwA
# AgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAK
# BghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHwYDVR0j
# BBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQELBQADggEBAD7s
# DVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q3yBVN7Dh9tGS
# dQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/kLEbBw6RFfu6
# r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dcIFzZcbEMj7uo
# +MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6dGRrsutmQ9qz
# sIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT+hKUGIUukpHq
# aGxEMrJmoecYpJpkUe8wggVrMIIEU6ADAgECAhAMMCpTLsjxo9FR9hag8ePUMA0G
# CSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0
# IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwHhcNMjAwMzMxMDAwMDAw
# WhcNMjMwNTEwMTIwMDAwWjCBpzELMAkGA1UEBhMCVVMxDTALBgNVBAgTBFV0YWgx
# FzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYDVQQKExVIZWFsdGggQ2F0YWx5
# c3QsIEluYy4xHjAcBgNVBAMTFUhlYWx0aCBDYXRhbHlzdCwgSW5jLjEwMC4GCSqG
# SIb3DQEJARYhYWRtaW5uaXN0cmF0b3JAaGVhbHRoY2F0YWx5c3QuY29tMIIBIjAN
# BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2fY0HWdxDJezDOsbHp7f9u/lrrD5
# nuZ1mENMgvixlrtC/KXgBRXlcWH7ajIOKljKnWCSAZwlZy4nFGbMagKmMzohXUXg
# xo94u5nCdiBa/kgPazNGpL0AyGgX2VARMbcpm8Gdy+/uH3Kc7L91lcoGZVVBnVIt
# 1oj5iXURqmhL83TrMyYqyj3XOH0So8Y10FVLPSukocMzMqBIRgvn/7EP0iWtOjXx
# +o1wB5Ql+z9G3NCqF6CKE/Pn355XYbbmjF7BPzKoOjocHO6VU2uEflJWq1ZFb0QY
# /tAosyyLYi9kFfO1damtJfRbbsVqavwg2UeQkzhg9CpB6eSsmBXPlFHudQIDAQAB
# o4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0O
# BBYEFFjfHOOIre2C4m9NCk8TFJlDwMxUMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE
# DDAKBggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2Ny
# bDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUw
# QzA3BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNl
# cnQuY29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcw
# AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8v
# Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNp
# Z25pbmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAsBxn
# 9yJAQi+9cJPZpJvOEV6iHaOBGv8898wNJCc4eB5g8WPziEY70GZVeqEdx3z0wS8U
# QQIr19Hkju2NFZjDtzB9z1jAc/9EgqFGoCZbPijv1EYAa2oOVAp1BPbLjqBSdXqu
# 2mzqo14CJ30oNom9ep9F6LGZ5zEoPsMrJejSbJGr4EacrksX8C8qeFklc7FzwiGk
# GX7IQxidrrhOm2fOvGGAAxnvNYAR0FqJK0LiWWPSt5R/j63H/6HQtqD2sLevI3+O
# bRP74TPchDobFmWlSogX9oB63E7fsbDAqecY0cRPQ6tVWK53Ke2sB514nahFjZDa
# mxsa3/acZWL659ly3jGCEP0wghD5AgEBMIGGMHIxCzAJBgNVBAYTAlVTMRUwEwYD
# VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAv
# BgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EC
# EAwwKlMuyPGj0VH2FqDx49QwDQYJYIZIAWUDBAIBBQCgfDAQBgorBgEEAYI3AgEM
# MQIwADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4w
# DAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgSWPtnvryOtDQY2z9n0ZtarXe
# diANJr8PE35hxDsBygUwDQYJKoZIhvcNAQEBBQAEggEABJrWiL2y95/TpwAmYKt4
# sVXB6IM/1FWBEnqA3vxWAhSbGxxVneRiwRUwdIsgbQF2ro+9rlkgl1Z/r6vihrvX
# j/RpDi0PjDZgX5+V5EjDBJpFNdQXLLh6P3OHugWiLks2DwI1zFL13NrG2DQmFFwN
# ejIXQqTGJg+1yS2w4UCZAm6Grlpxzr0YoNA7DFfIFwRxiJ/p6TVf87PqcGP4p6PD
# q0XUZUKx6Rirp5p1VahiajlMmKwFc8xnoJjPrmYjTxgLdH4mu3C56a3cbw1dkLqN
# 7/dhj5KJt5nMgff8qv2ZMAY0gsyZkdI1sT7NMYanyNqAY/bOUL9WP01gJHloE59j
# laGCDskwgg7FBgorBgEEAYI3AwMBMYIOtTCCDrEGCSqGSIb3DQEHAqCCDqIwgg6e
# AgEDMQ8wDQYJYIZIAWUDBAIBBQAweAYLKoZIhvcNAQkQAQSgaQRnMGUCAQEGCWCG
# SAGG/WwHATAxMA0GCWCGSAFlAwQCAQUABCDkx8je6EtOtrJ/seJIOGmSRy0Fsday
# 4vVHijoIo7u5PAIRAJaj1ROTWLYUNvRkVsA8Y18YDzIwMjAwOTEwMjIzMzA1WqCC
# C7swggaCMIIFaqADAgECAhAEzT+FaK52xhuw/nFgzKdtMA0GCSqGSIb3DQEBCwUA
# MHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsT
# EHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJl
# ZCBJRCBUaW1lc3RhbXBpbmcgQ0EwHhcNMTkxMDAxMDAwMDAwWhcNMzAxMDE3MDAw
# MDAwWjBMMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJDAi
# BgNVBAMTG1RJTUVTVEFNUC1TSEEyNTYtMjAxOS0xMC0xNTCCASIwDQYJKoZIhvcN
# AQEBBQADggEPADCCAQoCggEBAOlkNZz6qZhlZBvkF9y4KTbMZwlYhU0w4Mn/5Ts8
# EShQrwcx4l0JGML2iYxpCAQj4HctnRXluOihao7/1K7Sehbv+EG1HTl1wc8vp6xF
# fpRtrAMBmTxiPn56/UWXMbT6t9lCPqdVm99aT1gCqDJpIhO+i4Itxpira5u0yfJl
# EQx0DbLwCJZ0xOiySKKhFKX4+uGJcEQ7je/7pPTDub0ULOsMKCclgKsQSxYSYAtp
# IoxOzcbVsmVZIeB8LBKNcA6Pisrg09ezOXdQ0EIsLnrOnGd6OHdUQP9PlQQg1OvI
# zocUCP4dgN3Q5yt46r8fcMbuQhZTNkWbUxlJYp16ApuVFKMCAwEAAaOCAzgwggM0
# MA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsG
# AQUFBwMIMIIBvwYDVR0gBIIBtjCCAbIwggGhBglghkgBhv1sBwEwggGSMCgGCCsG
# AQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIIBZAYIKwYBBQUH
# AgIwggFWHoIBUgBBAG4AeQAgAHUAcwBlACAAbwBmACAAdABoAGkAcwAgAEMAZQBy
# AHQAaQBmAGkAYwBhAHQAZQAgAGMAbwBuAHMAdABpAHQAdQB0AGUAcwAgAGEAYwBj
# AGUAcAB0AGEAbgBjAGUAIABvAGYAIAB0AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAg
# AEMAUAAvAEMAUABTACAAYQBuAGQAIAB0AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQ
# AGEAcgB0AHkAIABBAGcAcgBlAGUAbQBlAG4AdAAgAHcAaABpAGMAaAAgAGwAaQBt
# AGkAdAAgAGwAaQBhAGIAaQBsAGkAdAB5ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBj
# AG8AcgBwAG8AcgBhAHQAZQBkACAAaABlAHIAZQBpAG4AIABiAHkAIAByAGUAZgBl
# AHIAZQBuAGMAZQAuMAsGCWCGSAGG/WwDFTAfBgNVHSMEGDAWgBT0tuEgHf4prtLk
# YaWyoiWyyBc1bjAdBgNVHQ4EFgQUVlMPwcYHp03X2G5XcoBQTOTsnsEwcQYDVR0f
# BGowaDAyoDCgLoYsaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJl
# ZC10cy5jcmwwMqAwoC6GLGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFz
# c3VyZWQtdHMuY3JsMIGFBggrBgEFBQcBAQR5MHcwJAYIKwYBBQUHMAGGGGh0dHA6
# Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBPBggrBgEFBQcwAoZDaHR0cDovL2NhY2VydHMu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkFzc3VyZWRJRFRpbWVzdGFtcGluZ0NB
# LmNydDANBgkqhkiG9w0BAQsFAAOCAQEALoOhRAVKBOO5MlL62YHwGrv4CY0juT3Y
# kqHmRhxKL256PGNuNxejGr9YI7JDnJSDTjkJsCzox+HizO3LeWvO3iMBR+2VVIHg
# gHsSsa8Chqk6c2r++J/BjdEhjOQpgsOKC2AAAp0fR8SftApoU39aEKb4Iub4U5Ix
# X9iCgy1tE0Kug8EQTqQk9Eec3g8icndcf0/pOZgrV5JE1+9uk9lDxwQzY1E3Vp5H
# BBHDo1hUIdjijlbXST9X/AqfI1579JSN3Z0au996KqbSRaZVDI/2TIryls+JRtwx
# spGQo18zMGBV9fxrMKyh7eRHTjOeZ2ootU3C7VuXgvjLqQhsUwm09zCCBTEwggQZ
# oAMCAQICEAqhJdbWMht+QeQF2jaXwhUwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UE
# BhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2lj
# ZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4X
# DTE2MDEwNzEyMDAwMFoXDTMxMDEwNzEyMDAwMFowcjELMAkGA1UEBhMCVVMxFTAT
# BgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEx
# MC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVzdGFtcGluZyBD
# QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3QMu5LzY9/3am6gpnF
# OVQoV7YjSsQOB0UzURB90Pl9TWh+57ag9I2ziOSXv2MhkJi/E7xX08PhfgjWahQA
# OPcuHjvuzKb2Mln+X2U/4Jvr40ZHBhpVfgsnfsCi9aDg3iI/Dv9+lfvzo7oiPhis
# EeTwmQNtO4V8CdPuXciaC1TjqAlxa+DPIhAPdc9xck4Krd9AOly3UeGheRTGTSQj
# MF287DxgaqwvB8z98OpH2YhQXv1mblZhJymJhFHmgudGUP2UKiyn5HU+upgPhH+f
# MRTWrdXyZMt7HgXQhBlyF/EXBu89zdZN7wZC/aJTKk+FHcQdPK/P2qwQ9d2srOlW
# /5MCAwEAAaOCAc4wggHKMB0GA1UdDgQWBBT0tuEgHf4prtLkYaWyoiWyyBc1bjAf
# BgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzASBgNVHRMBAf8ECDAGAQH/
# AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDCDB5BggrBgEF
# BQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBD
# BggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0
# QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2Ny
# bDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDig
# NoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v
# dENBLmNybDBQBgNVHSAESTBHMDgGCmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYc
# aHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzALBglghkgBhv1sBwEwDQYJKoZI
# hvcNAQELBQADggEBAHGVEulRh1Zpze/d2nyqY3qzeM8GN0CE70uEv8rPAwL9xafD
# DiBCLK938ysfDCFaKrcFNB1qrpn4J6JmvwmqYN92pDqTD/iy0dh8GWLoXoIlHsS6
# HHssIeLWWywUNUMEaLLbdQLgcseY1jxk5R9IEBhfiThhTWJGJIdjjJFSLK8pieV4
# H9YLFKWA1xJHcLN11ZOFk362kmf7U2GJqPVrlsD0WGkNfMgBsbkodbeZY4UijGHK
# eZR+WfyMD+NvtQEmtmyl7odRIeRYYJu6DC0rbaLEfrvEJStHAgh8Sa4TtuF8QkIo
# xhhWz0E0tmZdtnR79VYzIi8iNrJLokqV2PWmjlIxggJNMIICSQIBATCBhjByMQsw
# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
# ZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQg
# VGltZXN0YW1waW5nIENBAhAEzT+FaK52xhuw/nFgzKdtMA0GCWCGSAFlAwQCAQUA
# oIGYMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcN
# MjAwOTEwMjIzMzA1WjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBQDJb1QXtqWMC3C
# L0+gHkwovig0xTAvBgkqhkiG9w0BCQQxIgQg5b36YmlxsTKLmvYp/C7AoZI7QXvv
# 8EEJh+yiYNrna7QwDQYJKoZIhvcNAQEBBQAEggEAREwQ3Rj3ezd6uZFbvLPTN7DY
# HoEGwsPneCB4YTDE/FzN2DFhuu+eahDKmoq+3D5K7Wmnsvy2M+8rGTnSZrm5tWyM
# ohIArbsS70imV1xPW5RZ6ScAho+9reTBzVXVIEbUmKvWu7TZgPDqaXNgXuOZEq8e
# kLlA3/EsYgdhL5voN0zhisz1sFRqI4nyfa/UraZWMy0ulsopw02WNizebmrVi2jI
# PQQ2TtA4Ri6GA8ouszem9zbvEnwKjaEur90m0Dccvif0jHNOHo8BWQgcYOPYYQ02
# IkvDpyBt0qfYyEhRaOFIbY5cQwHS+7TI9FyatbE/S6+xmCe7eqldlimtdSYBEQ==
# SIG # End signature block