IISSiteInstall.psm1

Write-Verbose 'Importing from [C:\MyProjects\IISSiteInstall\IISSiteInstall\private]'
# .\IISSiteInstall\private\ConvertTo-BuiltInUsername.ps1
function ConvertTo-BuiltInUsername
{
    [CmdletBinding()]
    [OutputType([String])]
    param (
        [Parameter(Mandatory)]
        [ValidateSet('ApplicationPoolIdentity', 'LocalService', 'LocalSystem', 'NetworkService')]
        [string] $IdentityType,

        [Parameter(Mandatory)]
        [string] $AppPoolName
    )
    
    begin
    {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'

        if ($IdentityType -eq 'ApplicationPoolIdentity' -and [string]::IsNullOrWhiteSpace($AppPoolName))
        {
            throw "IdentityType of 'ApplicationPoolIdentity' requires that an -AppPoolName"
        }
    }
    
    process
    {
        try
        {

            switch ($IdentityType)
            {
                'ApplicationPoolIdentity'
                { 
                    "IIS AppPool\$AppPoolName"
                }
                'NetworkService'
                { 
                    'NT AUTHORITY\NETWORK SERVICE'
                }
                'LocalSystem'
                { 
                    'NT AUTHORITY\SYSTEM'
                }
                'LocalService'
                { 
                    'NT AUTHORITY\LOCAL SERVICE'
                }
                Default
                {
                    throw "IdentityType '$IdentityType' does not represent a built-in user"
                }
            }
        }
        catch
        {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\private\Get-IISSiteAclPathHelper.ps1
function Get-IISSiteAclPathHelper
{
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        [string] $Name,

        [switch] $Recurse
    )
    
    begin
    {
        Set-StrictMode -Version 'Latest'
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'

        $allSiteInfos = Get-IISSiteHierarchyInfo
        $tempAspNetFilesPaths = Get-CaccaTempAspNetFilesPath
    }
    
    process
    {
        try
        {
            $siteInfos = if ([string]::IsNullOrWhiteSpace($Name))
            {
                $allSiteInfos
            }
            else
            {
                $allSiteInfos | Where-Object Site_Name -eq $Name
            }

            $siteNames = @()
            $siteNames += $siteInfos | Select-Object -Exp Site_Name -Unique

            foreach ($siteName in $siteNames)
            {
                $siteInfo = $siteInfos | Where-Object Site_Name -eq $siteName
                $otherSiteInfos = $allSiteInfos | Where-Object Site_Name -ne $siteName

                $candidatePaths = @()
    
                $sitePaths = @()
                $sitePaths += $siteInfo | Select-Object -Exp App_PhysicalPath | Where-Object { Test-Path $_ }
    
                $siteSubPaths = @()
                if ($Recurse)
                {
                    $excludedPaths = @()
                    $excludedPaths += $otherSiteInfos | Select-Object -Exp App_PhysicalPath | ForEach-Object { Join-Path $_ '\*' }
                    # note: excluding node_modules for perf reasons (hopefully no site adds permissions to specific node modules!)
                    $siteSubPaths += Get-ChildItem $sitePaths -Recurse -Directory -Depth 5 -Exclude 'node_modules' |
                        Select-Object -Exp FullName -PV candidatePath | 
                        Where-Object { 
                        $excludedPaths.Count -eq 0 -or $excludedPaths.Where( { (Join-Path $candidatePath '\') -NotLike $_}) 
                    }
                }
    
                $uniqueFolderPaths = $sitePaths + $siteSubPaths | Select-Object -Unique
    
                $siteFilePaths = @()
                if ($Recurse)
                {
                    $siteFilePaths += @('*.bat', '*.exe', '*.ps1') | ForEach-Object {
                        Get-ChildItem $uniqueFolderPaths -Recurse -File -Depth 5 -Filter $_ |
                            Select-Object -Exp FullName
                    }
                }
                $uniqueSitePaths = $uniqueFolderPaths + $siteFilePaths | Select-Object -Unique
    
                $candidatePaths += $uniqueSitePaths
                $candidatePaths += $tempAspNetFilesPaths

                $appPoolUsernames = @()
                $appPoolUsernames += $siteInfo | Select-Object -Exp AppPool_Username | Select-Object -Unique
                foreach ($username in $appPoolUsernames)
                {
                    $candidatePaths | ForEach-Object {
                        $path = $_
                        $select = @(
                            @{n = 'SiteName'; e = {$siteName}},
                            @{n = 'Path'; e = {$path}}, 
                            @{n = 'IdentityReference'; e = {$username}}
                        )
                        (Get-Item $path).GetAccessControl('Access').Access |
                            Where-Object { $_.IsInherited -eq $false -and $_.IdentityReference -eq $username } | 
                            Select-Object $select
                    }
                }
            }

        }
        catch
        {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\private\Get-IISSiteBackConnectionHelper.ps1
function Get-IISSiteBackConnectionHelper {
    [CmdletBinding(DefaultParameterSetName = 'None')]
    param (
        [Parameter(ValueFromPipeline, ParameterSetName = 'Name', Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter(ValueFromPipeline, ParameterSetName = 'Object', Position = 0)]
        [Microsoft.Web.Administration.Site] $InputObject
        
    )
    
    begin {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'

        $allHostnameEntries = @(Get-TecBoxBackConnectionHostNames) | ForEach-Object {
            [PsCustomObject] @{ Hostname = $_ }
        }
    }
    
    process {
        try {
            if (![string]::IsNullOrWhiteSpace($Name)) {
                $Name = $Name.Trim()
            }
            $selectedSites = @()
            $selectedSites += if ($InputObject) {
                $InputObject
            }
            elseif (![string]::IsNullOrWhiteSpace($Name)) {
                Get-IISSite $Name
            }
            else {
                Get-IISSite
            }

            foreach ($site in $selectedSites) {

                $siteEntries = $site.Bindings | Select-Object -Exp Host -Unique |
                    Select-object @{n = 'Hostname'; e = { $_ }}, @{n = 'SiteName'; e = { $site.Name }}

                $siteEntries | ForEach-Object {
                    $siteEntry = $_
                    $allHostnameEntries | Where-Object { $_.Hostname -eq $siteEntry.Hostname } |
                        Select-Object -Property *, @{ n = 'SiteName'; e = { $siteEntry.SiteName } }
                }
            }

        }
        catch {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\private\Get-IISSiteHostsFileEntryHelper.ps1
function Get-IISSiteHostsFileEntryHelper {
    [CmdletBinding(DefaultParameterSetName = 'None')]
    param (
        [Parameter(ValueFromPipeline, ParameterSetName = 'Name', Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter(ValueFromPipeline, ParameterSetName = 'Object', Position = 0)]
        [Microsoft.Web.Administration.Site] $InputObject
        
    )
    
    begin {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'

        $allHostnameEntries = @()
        $allHostnameEntries += Get-TecBoxHostnames
    }
    
    process {
        try {
            if (![string]::IsNullOrWhiteSpace($Name)) {
                $Name = $Name.Trim()
            }
            $selectedSites = @()
            $selectedSites += if ($InputObject) {
                $InputObject
            }
            elseif (![string]::IsNullOrWhiteSpace($Name)) {
                Get-IISSite $Name
            }
            else {
                Get-IISSite
            }

            foreach ($site in $selectedSites) {

                $siteEntries = $site.Bindings | Select-Object -Exp Host -Unique |
                    Select-object @{n = 'Hostname'; e = { $_ }}, @{n = 'SiteName'; e = { $site.Name }}

                $siteEntries | ForEach-Object {
                    $siteEntry = $_
                    $allHostnameEntries | Where-Object { $_.Hostname -eq $siteEntry.Hostname } |
                        Select-Object -Property *, @{ n = 'SiteName'; e = { $siteEntry.SiteName } }
                }
            }

        }
        catch {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\private\GetAppPoolOtherSiteCount.ps1
function GetAppPoolOtherSiteCount
{
    [CmdletBinding()]
    param (
        [string] $ExcludeSiteName,
        [string] $AppPoolName
    )
    
    begin
    {
        Set-StrictMode -Version 'Latest'
        $ErrorActionPreference = 'Stop'
    }
    
    process
    {
        Get-IISSiteHierarchyInfo | 
            Where-Object { $_.AppPool_Name -eq $AppPoolName -and $_.Site_Name -ne $ExcludeSiteName } |
            Select-Object Site_Name -Unique |
            Measure-Object | Select-Object -Exp Count
    }
}
Write-Verbose 'Importing from [C:\MyProjects\IISSiteInstall\IISSiteInstall\public]'
# .\IISSiteInstall\public\Get-IISAppPoolUsername.ps1
function Get-IISAppPoolUsername {
    <#
    .SYNOPSIS
    Returns the Username used as the Identity for an IIS AppPool
    
    .DESCRIPTION
    Returns the Username used as the Identity for an IIS AppPool. This will either be
    the domain qualified name of a specific Windows User account or the qualified name
    of the built-in accounts (eg 'NT Authority\System'), which ever is assigned as the
    Identity of the AppPool
    
    .PARAMETER InputObject
    The AppPool to return a username
    
    .EXAMPLE
    Get-IISAppPool DefaultAppPool | Get-CaccaIISAppPoolUsername
    
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNull()]
        [Microsoft.Web.Administration.ApplicationPool] $InputObject
    )
    
    begin {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'
    }
    
    process {
        try {
            try {
                ConvertTo-BuiltInUsername ($InputObject.ProcessModel.IdentityType) ($InputObject.Name)
            }
            catch {
                $InputObject.ProcessModel.UserName
            }
        }
        catch {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\public\Get-IISSiteAclPath.ps1
function Get-IISSiteAclPath {
    <#
    .SYNOPSIS
    Return the file/folder paths where the identity of the AppPool(s) used by the
    IIS site has been assigned one or more file/folder permission
    
    .DESCRIPTION
    Return the file/folder paths where the identity of the AppPool(s) used by the
    IIS site has been assigned one or more file/folder permission.

    The physical path of the website along with any child applications will be inspected.

    Optionally, supply the name of a IIS Website to return just those paths for a single site.

    Optionally, supply -Recurse to search all nested paths under the physical paths inspected.

    Items in the output have a 'IsShared' property. A path is considered shared if it has a
    permission assigned to an Identity that is used on more than one IIS website.

    This 'IsShared' property will typically be used to determine which file permissions can
    be safely removed when an IIS Website is removed
    
    .PARAMETER Name
    Name of an IIS Website
    
    .PARAMETER Recurse
    Search subfolders

    .EXAMPLE
    Get-CaccaIISSiteAclPath Series5 -Recurse

    SiteName Path IdentityReference IsShared
    -------- ---- ----------------- --------
    Series5 C:\inetpub\sites\Series5 IIS AppPool\Series5-AppPool False
    Series5 C:\Git\Series5\src\Ram.Series5.Spa IIS AppPool\Series5-AppPool False
    Series5 C:\Git\Series5\src\Ram.Series5.WinLogin IIS AppPool\Series5-AppPool False
    Series5 C:\Git\Series5\src\Ram.Series5.Spa\App_Data IIS AppPool\Series5-AppPool False
    Series5 C:\Git\Series5\src\Ram.Series5.WinLogin\App_Data IIS AppPool\Series5-AppPool False
    Series5 C:\Windows\Microsoft.NET\Framework64\v2.0.50727\Temporary ASP.NET Files IIS AppPool\Series5-AppPool False
    Series5 C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files IIS AppPool\Series5-AppPool False
    
    .NOTES
    Currently, Virtual Directories will not be inspected
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [switch] $Recurse
    )
    
    begin {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'

        $allSiteInfos = @()
        $allSiteInfos += Get-IISSiteAclPathHelper
    }
    
    process {
        try {
            if (![string]::IsNullOrWhiteSpace($Name)) {
                $Name = $Name.Trim()
            }


            $siteInfos = if ([string]::IsNullOrWhiteSpace($Name)) {
                $allSiteInfos
            }
            else {
                $allSiteInfos | Where-Object SiteName -eq $Name
            }

            $siteNames = @()
            $siteNames += $siteInfos | Select-Object -Exp SiteName -Unique

            foreach ($siteName in $siteNames) {
                $siteAclPaths = Get-IISSiteAclPathHelper $siteName -Recurse:$Recurse
                $otherSiteAclPaths = $allSiteInfos | Where-Object SiteName -ne $siteName

                Write-Debug "Acl Paths: $siteAclPaths"
                
                $siteAclPaths | ForEach-Object {
                    $path = $_.Path
                    $identityReference = $_.IdentityReference
                    $isShared = ($otherSiteAclPaths | 
                        Where-Object { $_.Path -eq $path -and $_.IdentityReference -eq $identityReference } |
                        Measure-Object).Count -ne 0
                    $_ | Select-Object -Property *, @{ n='IsShared'; e={$isShared}}
                }                
            }

        }
        catch {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\public\Get-IISSiteBackConnection.ps1
function Get-IISSiteBackConnection {
    <#
    .SYNOPSIS
    Gets the list of host names that are assigned to IIS Site bindings that also appear
    in the BackConnectionHostNames registry value
    
    .DESCRIPTION
    The BackConnectionHostNames registry value is used to bypass the loopback
    security check for specific host names.

    This function returns those host names in the BackConnectionHostNames registry value
    that are assigned to IIS Site bindings.

    Items in the output have a 'IsShared' property. A hostname is considered shared if it
    assigned to bindings on more than one Website.

    This 'IsShared' property will typically be used to determine which host names can
    be safely removed from the BackConnectionHostNames list when an IIS Website is removed
    
    .PARAMETER Name
    The name of the IIS Website to filter results
    
    .PARAMETER InputObject
    The instance the IIS Website to filter results
    
    .EXAMPLE
    Get-CaccaIISSiteBackConnection Series5

    Hostname SiteName IsShared
    -------- -------- --------
    local-series5 Series5 False
    
    #>

    [CmdletBinding(DefaultParameterSetName = 'Name')]
    param (
        [Parameter(ValueFromPipeline, ParameterSetName = 'Name', Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter(ValueFromPipeline, ParameterSetName = 'Object', Position = 0)]
        [Microsoft.Web.Administration.Site] $InputObject
        
    )
    
    begin {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'

        $allEntries = @()
        $allEntries += Get-IISSiteBackConnectionHelper
    }
    
    process {
        try {

            $selectedSites = @()
            $selectedSites += if ($InputObject) {
                $InputObject
            }
            elseif (![string]::IsNullOrWhiteSpace($Name)) {
                Get-IISSite $Name
            }
            else {
                Get-IISSite
            }

            $siteEntries = @($selectedSites | Get-IISSiteBackConnectionHelper)

            $siteEntries | ForEach-Object {
                $entry = $_
                $isShared = ($allEntries | 
                        Where-Object Hostname -eq $entry.Hostname | 
                        Select-Object SiteName -Unique | Measure-Object).Count -gt 1
                $entry | Select-Object -Property *, @{ n='IsShared'; e={ $isShared }}
            }

        }
        catch {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\public\Get-IISSiteHierarchyInfo.ps1
function Get-IISSiteHierarchyInfo
{
    <#
    .SYNOPSIS
    Returns the physical paths and the User Identity assigned to IIS websites and their child Applications
    
    .DESCRIPTION
    Returns the physical paths and the User Identity assigned to IIS websites and their child Applications
    
    .PARAMETER Name
    The name of the IIS Website to filter results
    
    .PARAMETER AppName
    The name of the IIS Web application to filter results
    
    .EXAMPLE
    Get-CaccaIISSiteHierarchyInfo Series5

    Output
    ------
    Site_Name : Series5
    App_Path : /
    App_PhysicalPath : C:\inetpub\sites\Series5
    AppPool_Name : Series5-AppPool
    AppPool_IdentityType : ApplicationPoolIdentity
    AppPool_Username : IIS AppPool\Series5-AppPool

    Site_Name : Series5
    App_Path : /Spa
    App_PhysicalPath : C:\Git\Series5\src\Ram.Series5.Spa
    AppPool_Name : Series5-AppPool
    AppPool_IdentityType : ApplicationPoolIdentity
    AppPool_Username : IIS AppPool\Series5-AppPool

    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string] $Name,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string] $AppName
    )
    
    begin
    {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'
    }
    
    process
    {
        try
        {
            $siteParams = if ([string]::IsNullOrWhiteSpace($Name))
            {
                @{}
            }
            else
            {
                @{
                    Name = $Name.Trim()
                }
            }

            if (![string]::IsNullOrWhiteSpace($AppName) -and !$AppName.StartsWith('/'))
            {
                $AppName = '/' + $AppName
            }

            Get-IISSite @siteParams -PV site -WA SilentlyContinue |
                Select-Object -Exp Applications -PV app |
                Where-Object { !$AppName -or $_.Path -eq $AppName } |
                ForEach-Object {
                $existingPool = Get-IISAppPool -Name $_.ApplicationPoolName -WA SilentlyContinue
                if (!$existingPool)
                {
                    ''
                }
                else
                {
                    $existingPool
                }
            } -PV pool |
                Select-Object  `
            @{n = 'Site_Name'; e = {$site.Name}},
            @{n = 'App_Path'; e = {$app.Path}}, 
            @{n = 'App_PhysicalPath'; e = {$app.VirtualDirectories[0].PhysicalPath}}, 
            @{n = 'AppPool_Name'; e = { if ($pool) { $app.ApplicationPoolName } }},
            @{n = 'AppPool_IdentityType'; e = { if ($pool) { $pool.ProcessModel.IdentityType} }},
            @{n = 'AppPool_Username'; e = { if ($pool) { Get-IISAppPoolUsername $pool } }}
        }
        catch
        {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\public\Get-IISSiteHostsFileEntry.ps1
function Get-IISSiteHostsFileEntry {
    <#
    .SYNOPSIS
    Gets the list of host names that are assigned to IIS Site bindings that also appear
    in the Windows hosts file
    
    .DESCRIPTION
    The hosts file (C:\Windows\System32\drivers\etc\hosts) is used to map hostnames to
    IP addresses instead of using a DNS server to provide that resolution.

    This function returns those host names in the hosts file that are assigned to IIS Site
    bindings.

    Items in the output have a 'IsShared' property. A hostname is considered shared if it
    assigned to bindings on more than one Website.

    This 'IsShared' property will typically be used to determine which host names can
    be safely removed from the hosts file when an IIS Website is removed
    
    .PARAMETER Name
    The name of the IIS Website to filter results
    
    .PARAMETER InputObject
    The instance the IIS Website to filter results
    
    .EXAMPLE
    Get-CaccaIISSiteHostsFileEntry Series5

    IpAddress Hostname SiteName IsShared
    --------- -------- -------- --------
    127.0.0.1 local-series5 Series5 False
    

    #>

    [CmdletBinding(DefaultParameterSetName = 'Name')]
    param (
        [Parameter(ValueFromPipeline, ParameterSetName = 'Name', Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter(ValueFromPipeline, ParameterSetName = 'Object', Position = 0)]
        [Microsoft.Web.Administration.Site] $InputObject
        
    )
    
    begin {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'

        $allEntries = @()
        $allEntries += Get-IISSiteHostsFileEntryHelper
    }
    
    process {
        try {

            $selectedSites = @()
            $selectedSites += if ($InputObject) {
                $InputObject
            }
            elseif (![string]::IsNullOrWhiteSpace($Name)) {
                Get-IISSite $Name
            }
            else {
                Get-IISSite
            }

            $siteEntries = @($selectedSites | Get-IISSiteHostsFileEntryHelper)

            $siteEntries | ForEach-Object {
                $entry = $_
                $isShared = ($allEntries | 
                        Where-Object Hostname -eq $entry.Hostname | 
                        Select-Object SiteName -Unique | Measure-Object).Count -gt 1
                $entry | Select-Object -Property *, @{ n='IsShared'; e={ $isShared }}
            }

        }
        catch {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\public\New-IISAppPool.ps1
#Requires -RunAsAdministrator

function New-IISAppPool
{
    <#
    .SYNOPSIS
    Creates a new IIS application pool
    
    .DESCRIPTION
    Creates a new IIS application pool
    
    .PARAMETER Name
    The name of the pool
    
    .PARAMETER Config
    A script block that will receive the instance of the pool being created
    
    .PARAMETER Force
    Overwrite any existing pool?
    
    .PARAMETER Commit
    Save changes to IIS immediately? Defaults to true
    
    .EXAMPLE
    New-CaccaIISAppPool MyNewPool

    Description
    -----------
    Create pool using the defaults configured for all application pools.
    The exception to the defaults is 'Enable32BitAppOnWin64' is set to $true (best practice)

    .EXAMPLE
    New-CaccaIISAppPool MyNewPool -Config {
        $_.Enable32BitAppOnWin64 = $false
    }

    Description
    -----------
    Configures the pool being created with custom settings

    .EXAMPLE
    New-CaccaIISAppPool $tempAppPool -Config {
        $_ | Set-CaccaIISAppPoolUser -IdentityType NetworkService -Commit:$false
    }

    Description
    -----------
    Create the pool with an identity assigned to the Network Service built-in account

    .EXAMPLE
    $pswd = ConvertTo-SecureString '(mypassword)' -AsPlainText -Force
    $creds = [PsCredential]::new("$($env:COMPUTERNAME)\MyLocalUser", $pswd)

    New-CaccaIISAppPool $tempAppPool -Config {
        $_ | Set-CaccaIISAppPoolUser $creds -Commit:$false
    }

    Description
    -----------
    Create the pool with an identity assigned to a specific user account

    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string] $Name,

        [Parameter(ValueFromPipelineByPropertyName)]
        [scriptblock] $Config,

        [switch] $Force,

        [switch] $Commit
    )
    
    begin
    {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'

        if (!$PSBoundParameters.ContainsKey('Commit'))
        {
            $Commit = $true
        }
    }
    
    process
    {
        try
        {
            Write-Information "Create app pool '$Name'"

            if ($null -eq $Config)
            {
                $Config = {}
            }

            [Microsoft.Web.Administration.ServerManager] $manager = Get-IISServerManager

            $existingPool = $manager.ApplicationPools[$Name]

            if (!$Force -and $existingPool)
            {
                throw "App pool '$Name' already exists. Supply -Force to overwrite"
            }

            if ($Commit)
            {
                Start-IISCommitDelay
            }
            try
            {
                if ($existingPool -and $PSCmdlet.ShouldProcess($Name, 'Remove App pool'))
                {
                    # note: not using Remove-IISAppPool as do NOT want to remove file permissions
                    $manager.ApplicationPools.Remove($existingPool)
                }

                if ($PSCmdlet.ShouldProcess($Name, 'Create App pool'))
                {
                    [Microsoft.Web.Administration.ApplicationPool] $pool = $manager.ApplicationPools.Add($Name)

                    # todo: do NOT set this when it's detected that OS is 64bit onlys
                    $pool.Enable32BitAppOnWin64 = $true # this IS the recommended default even for 64bit servers

                    $pool | ForEach-Object $Config
                }
                if ($Commit)
                {
                    Stop-IISCommitDelay
                }                 
            }
            catch
            {
                if ($Commit)
                {
                    Stop-IISCommitDelay -Commit:$false
                }
                throw
            }
            finally
            {
                if ($Commit)
                {
                    # make sure subsequent scripts will not fail because the ServerManger is now readonly
                    Reset-IISServerManager -Confirm:$false
                }
            }

            Get-IISAppPool $Name
        }
        catch
        {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\public\New-IISWebApp.ps1
#Requires -RunAsAdministrator

function New-IISWebApp
{
    <#
    .SYNOPSIS
    Creates a new IIS Web application + App Pool, assigning it least-privilege file permissions
    
    .DESCRIPTION
    Creates a new IIS Web application + App Pool, setting least privilege file permissions to
    the useraccount configured as the identity of the IIS AppPool.

    These bare minium file permissions include:
    - Path: Read 'This folder', file and subfolder permissions (inherited)
    - Temporary ASP.NET Files: Read 'This folder', file and subfolder permissions (inherited)
    - ModifyPaths: modify 'This folder', file and subfolder permissions (inherited)
    - ExecutePaths: read+execute file (no inherit)

    .PARAMETER SiteName
    The name of the IIS Website to add the application to
    
    .PARAMETER Name
    The logical path name of the application (eg MyApp, /MyApp/NestedApp)
    
    .PARAMETER Path
    The physcial path of the application. Defaults to using 'Name' as a sub folder of the physical site path.
    Path will be created if missing.
    
    .PARAMETER Config
    A script block that will receive the instance of the application being created
    
    .PARAMETER AppPoolName
    The name of the AppPool to assign and create if missing.
    If not supplied, will default to use the AppPool of the IIS Website
    
    .PARAMETER AppPoolConfig
    A script block that will receive the instance of the pool to be used by the application
    
    .PARAMETER ModifyPaths
    Additional paths to grant modify (inherited) permissions. Path(s) relative to 'Path' can be supplied

    .PARAMETER ExecutePaths
    Additional paths to grant read+excute permissions. Path(s) relative to 'Path' can be supplied
    
    .PARAMETER Force
    Overwrite any existing application?
    
    .EXAMPLE
    New-CaccaIISWebApp MySite MyNewApp

    Description
    -----------
    Create child Web application of MySite, with the physical path set as a subfolder of the Website
    (eg C:\inetpub\sites\MySite\MyNewApp), and an App Pool assigned to that of the Website

    .EXAMPLE
    New-CaccaIISWebApp MySite MyNewApp -AppPoolName MyNewPool -AppPoolConfig {
        $_ | Set-CaccaIISAppPoolUser -IdentityType ApplicationPoolIdentity -Commit:$false
    }

    Description
    -----------
    As above except assigns, creating as necessary, an App Pool named 'MyNewPool' and
    configuring that pool to use the ApplicationPoolIdentity as it's identity

    .EXAMPLE
    New-CaccaIISWebApp MySite MyNewApp C:\Some\Path\Else -Config {
        Unlock-CaccaIISAnonymousAuth -Location "$SiteName$($_.Path)" -Commit:$false
    }

    Description
    -----------
    Create child Web application of MySite, with the physical path set to C:\Some\Path\Else.

    Uses -Config to supply a script block to perform custom configuration of the application. In this
    example, using the Unlock-CaccaIISAnonymousAuth cmdlet from the IISConfigUnlock module

    .EXAMPLE
    New-CaccaIISWebApp MySite MyNewApp -ModifyPaths 'App_Data', 'logs' -ExecutePaths bin\Some.exe

    Description
    -----------
    Configures additional file permissions to the useraccount configured as the identity of the IIS AppPool
    
    .NOTES
    Exception thrown when:
    * Application 'Name' already exists and -Force is NOT supplied
    * The Application Pool 'AppPoolName' is used by another Website
    * Using 'AppPoolConfig' to configure an Application pool that is already assigned to another Application and/or Website
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string] $SiteName,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string] $Path,

        [Parameter(ValueFromPipelineByPropertyName)]
        [scriptblock] $Config,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string] $AppPoolName,

        [Parameter(ValueFromPipelineByPropertyName)]
        [scriptblock] $AppPoolConfig,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string[]] $ModifyPaths,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string[]] $ExecutePaths,

        [switch] $Force
    )
    
    begin
    {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'

        function GetAppPoolOtherAppCount
        {
            param (
                [string] $SiteName,
                [string] $ThisAppName,
                [string] $AppPoolName
            )
            Get-IISSiteHierarchyInfo | 
                Where-Object { $_.AppPool_Name -eq $AppPoolName -and $_.Site_Name -eq $SiteName -and $_.App_Path -ne $ThisAppName } |
                Select-Object App_Path -Unique |
                Measure-Object | Select-Object -Exp Count
        }
    }
    
    process
    {
        try
        {
            Write-Information "Create Web application '$Name' under site '$SiteName'"

            $SiteName = $SiteName.Trim()
            $Name = $Name.Trim()
            if (!$Name.StartsWith('/'))
            {
                $Name = '/' + $Name
            }
            if ($null -eq $Config)
            {
                $Config = {}
            }
            if ($null -eq $ModifyPaths)
            {
                $ModifyPaths = @()
            }
            if ($null -eq $ExecutePaths)
            {
                $ExecutePaths = @()
            }


            $site = Get-IISSite $SiteName
            if (!$site)
            {
                return
            }

            $qualifiedAppName = "$SiteName$Name"

            $existingApp = $site.Applications[$Name]
            if ($existingApp -and !$Force)
            {
                throw "Web Application '$qualifiedAppName' already exists. To overwrite you must supply -Force"
            }

            $rootApp = $site.Applications['/']
            if ([string]::IsNullOrWhiteSpace($AppPoolName))
            {
                $AppPoolName = $rootApp.ApplicationPoolName
            }

            if ((GetAppPoolOtherSiteCount $SiteName $AppPoolName) -gt 0)
            {
                throw "Cannot create Web Application - AppPool '$AppPoolName' is in use on another site"
            }
            if ($AppPoolConfig -and (GetAppPoolOtherAppCount $SiteName $Name $AppPoolName) -gt 0)
            {
                throw "Cannot configure AppPool '$AppPoolName' - it belongs to another Web Application and/or this site"
            }

            $childPath = if ([string]::IsNullOrWhiteSpace($Path))
            {
                $sitePath = $rootApp.VirtualDirectories['/'].PhysicalPath
                Join-Path $sitePath $Name
            }
            else
            {
                $Path
            }
            
            $isPathExists = Test-Path $childPath
            if (!$isPathExists -and $PSCmdlet.ShouldProcess($childPath, 'Create Web Application physical path'))
            {
                New-Item $childPath -ItemType Directory -WhatIf:$false | Out-Null
            }

            if ($existingApp)
            {
                Write-Information "Existing Web application '$Name' found"
                Remove-IISWebApp $SiteName $Name -ModifyPaths $ModifyPaths -ExecutePaths $ExecutePaths
            }

            # Remove-IISWebApp has just committed changes making our $site instance read-only, therefore fetch another one
            $site = Get-IISSite $SiteName

            Start-IISCommitDelay
            try
            {
                $pool = Get-IISAppPool $AppPoolName -WA SilentlyContinue
                if (!$pool)
                {
                    $pool = New-IISAppPool $AppPoolName -Commit:$false
                }

                if ($AppPoolConfig -and $WhatIfPreference -eq $false)
                {
                    $pool | ForEach-Object $AppPoolConfig | Out-Null
                }

                if ($PSCmdlet.ShouldProcess($qualifiedAppName, 'Create Web Application'))
                {
                    $app = $site.Applications.Add($Name, $childPath)
                    $app.ApplicationPoolName = $AppPoolName

                    $app | ForEach-Object $Config
                }

                Stop-IISCommitDelay
            }
            catch
            {
                Stop-IISCommitDelay -Commit:$false
                throw
            }
            finally
            {
                Reset-IISServerManager -Confirm:$false
            }

            if ($WhatIfPreference -eq $true -and !$isPathExists)
            {
                # Set-CaccaIISSiteAcl requires path to exist
            }
            else
            {
                $appPoolIdentity = Get-IISAppPool $AppPoolName | Get-IISAppPoolUsername
                if ($WhatIfPreference -eq $true -and [string]::IsNullOrWhiteSpace($appPoolIdentity))
                {
                    $appPoolIdentity = "IIS AppPool\$AppPoolName"
                }
                $appAclParams = @{
                    AppPath         = $childPath
                    AppPoolIdentity = $appPoolIdentity
                    ModifyPaths     = $ModifyPaths
                    ExecutePaths    = $ExecutePaths
                }
                Set-CaccaIISSiteAcl @appAclParams
            }

            (Get-IISSite $SiteName).Applications[$Name]
        }
        catch
        {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\public\New-IISWebsite.ps1
#Requires -RunAsAdministrator

function New-IISWebsite
{
    <#
    .SYNOPSIS
    Creates a new IIS Website + App Pool, assigning it least-privilege file permissions
    
    .DESCRIPTION
    Creates a new IIS Web application + App Pool, setting least privilege file permissions to
    the useraccount configured as the identity of the IIS AppPool.

    These bare minium file permissions include:
    - Path: Read 'This folder', file and subfolder permissions (inherited)
        - Note: use 'SiteShellOnly' to reduce these permissions to just the folder and files but NOT subfolders
    - Temporary ASP.NET Files: Read 'This folder', file and subfolder permissions (inherited)
    - ModifyPaths: modify 'This folder', file and subfolder permissions (inherited)
    - ExecutePaths: read+execute file (no inherit)s
    
    .PARAMETER Name
    The name of the IIS Website to add the application to
    
    .PARAMETER Path
    The physcial path of the Website. Defaults to using "C:\inetpub\sites\$Name". Path will be created if missing.
    
    .PARAMETER Port
    The port number to use for the default site binding. Defaults to 80
    
    .PARAMETER Protocol
    The protocol to use for the default site binding. Defaults to 'http'
    
    .PARAMETER HostName
    Optional hostname to use for the default site binding.
    See also 'HostsFileIPAddress' and 'AddHostToBackConnections' parameters
    
    .PARAMETER Config
    A script block that will receive the instance of the Website being created
    
    .PARAMETER ModifyPaths
    Additional paths to grant modify (inherited) permissions. Path(s) relative to 'Path' can be supplied

    .PARAMETER ExecutePaths
    Additional paths to grant read+excute permissions. Path(s) relative to 'Path' can be supplied
    
    .PARAMETER SiteShellOnly
    Grant permissions used for 'Path' to only that folder and it's files but NOT subfolders
    
    .PARAMETER AppPoolName
    The name of the AppPool to assign and create if missing. Defaults to "$Name-AppPool"
    
    .PARAMETER AppPoolConfig
    A script block that will receive the instance of the pool to be used by the application
    
    .PARAMETER HostsFileIPAddress
    Resolve hostname(s) used by the site bindings to an IP address (stores a record in the hosts file on this computer)
    
    .PARAMETER AddHostToBackConnections
    Register hostname(s) used by the site bindings as a BackConnection registry entry to bypass the loopback security
    check for this name(s)
    
    .PARAMETER Force
    Overwrite any existing Website?
    
    .EXAMPLE
    New-CaccaIISWebsite MySite

    Description
    -----------
    Create a Website named MySite, with the physical path set to C:\inetpub\sites\MySite.
    Assigns an App Pool named MySite-AppPool, creating the pool if not already present.
    Binds the site to port 80 over http

    .EXAMPLE
    New-CaccaIISWebsite MySite -AppPoolName MyNewPool -AppPoolConfig {
        $_ | Set-CaccaIISAppPoolUser -IdentityType ApplicationPoolIdentity -Commit:$false
    }

    Description
    -----------
    As above except assigns, creating as necessary, an App Pool named 'MyNewPool' and
    configuring that pool to use the ApplicationPoolIdentity as it's identity
    
    .EXAMPLE
    New-CaccaIISWebsite MySite C:\Some\Path\Else -Config {
        Unlock-CaccaIISAnonymousAuth -Location $_.Name -Commit:$false
    }

    Description
    -----------
    Create Website named MySite, with the physical path set to C:\Some\Path\Else.

    Uses -Config to supply a script block to perform custom configuration of the Website. In this
    example, using the Unlock-CaccaIISAnonymousAuth cmdlet from the IISConfigUnlock module

    .EXAMPLE
    New-CaccaIISWebsite MySite -AppPoolName MyNewPool -AppPoolConfig {
        $_ | Set-CaccaIISAppPoolUser -IdentityType ApplicationPoolIdentity -Commit:$false
    }

    Description
    -----------
    As above except assigns, creating as necessary, an App Pool named 'MyNewPool' and
    configuring that pool to use the ApplicationPoolIdentity as it's identity
    
    .EXAMPLE
    New-CaccaIISWebsite MySite -ModifyPaths 'App_Data', 'logs' -ExecutePaths bin\Some.exe

    Description
    -----------
    Configures additional file permissions to the useraccount configured as the identity of the IIS AppPool

    .EXAMPLE
    New-CaccaIISWebsite MySite -HostsFileIPAddress 127.0.0.1 -Hostname dev-mysite -AddHostToBackConnections -Config {
        New-IISSiteBinding $_.Name ':8080:local-mysite' http
    }

    Description
    -----------
    Configures the site with an additional binding to port 8080, host name 'local-mysite'. Ensures 'dev-mysite'
    and 'local-mysite' resolve to 127.0.0.1 on this computer whilst ensuring these host names bypass the loopback
    security check

    .NOTES
    Exception thrown when:
    * Website 'Name' already exists and -Force is NOT supplied
    * The Application Pool 'AppPoolName' is used by another Website
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string] $Path,

        [ValidateRange(0, 65535)]
        [int] $Port = 80,

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('http', 'https')]
        [string] $Protocol = 'http',

        [Parameter(ValueFromPipelineByPropertyName)]
        [string] $HostName,

        [Parameter(ValueFromPipelineByPropertyName)]
        [scriptblock] $Config,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string[]] $ModifyPaths,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string[]] $ExecutePaths,

        [switch] $SiteShellOnly,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string] $AppPoolName,

        [Parameter(ValueFromPipelineByPropertyName)]
        [scriptblock] $AppPoolConfig,

        [string] $HostsFileIPAddress,

        [switch] $AddHostToBackConnections,

        [switch] $Force
    )
    
    begin
    {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'
    }
    
    process
    {
        try
        {
            Write-Information "Create website '$Name'"

            $Name = $Name.Trim()
            if ([string]::IsNullOrWhiteSpace($Path))
            {
                $Path = "C:\inetpub\sites\$Name"
            }
            if ($null -eq $Config)
            {
                $Config = {}
            }
            if ($null -eq $ModifyPaths)
            {
                $ModifyPaths = @()
            }
            if ($null -eq $ExecutePaths)
            {
                $ExecutePaths = @()
            }
            if ([string]::IsNullOrWhiteSpace($AppPoolName))
            {
                $AppPoolName = "$Name-AppPool"
            }
            if ($null -eq $AppPoolConfig)
            {
                $AppPoolConfig = {}
            }

            
            $existingSite = Get-IISSite $Name -WA SilentlyContinue
            if ($existingSite -and !$Force)
            {
                throw "Cannot create site - site '$Name' already exists. To overwrite you must supply -Force"
            }

            if ((GetAppPoolOtherSiteCount $Name $AppPoolName) -gt 0)
            {
                throw "Cannot create site - AppPool '$AppPoolName' is in use on another site"
            }

            $isPathExists = Test-Path $Path
            if (!$isPathExists -and $PSCmdlet.ShouldProcess($Path, 'Create Web Site physical path'))
            {
                New-Item $Path -ItemType Directory -WhatIf:$false | Out-Null
            }

            if ($existingSite)
            {
                Write-Information "Existing website '$Name' found"
                Remove-IISWebsite $Name -Confirm:$false
            }

            Start-IISCommitDelay
            try
            {
    
                $pool = Get-IISAppPool $AppPoolName -WA SilentlyContinue
                if (!$pool)
                {
                    $pool = New-IISAppPool $AppPoolName -Commit:$false
                }
                
                if ($AppPoolConfig -and $WhatIfPreference -eq $false)
                {
                    $pool | ForEach-Object $AppPoolConfig | Out-Null
                }
    
                $site = $null
                if ($PSCmdlet.ShouldProcess($Name, 'Create Web Site'))
                {
                    $bindingInfo = "*:$($Port):$($HostName)"
                    [Microsoft.Web.Administration.Site] $site = New-IISSite $Name $Path $bindingInfo $Protocol -Passthru
                    $site.Applications["/"].ApplicationPoolName = $AppPoolName

                    $site | ForEach-Object $Config

                    $allHostNames = $site.Bindings | Select-Object -Exp Host -Unique
                    
                    if (![string]::IsNullOrWhiteSpace($HostsFileIPAddress) -and $PSCmdlet.ShouldProcess($allHostNames, 'Add hosts file entry'))
                    {
                        Write-Information "Add '$allHostNames' to hosts file"
                        $allHostNames | Add-TecBoxHostnames -IPAddress $HostsFileIPAddress
                    }
                    
                    if ($AddHostToBackConnections -and $PSCmdlet.ShouldProcess($allHostNames, 'Add back connection'))
                    {
                        Write-Information "Add '$allHostNames' to backconnection registry value"
                        $allHostNames | Add-TecBoxBackConnectionHostNames
                    }
                }
    
                Stop-IISCommitDelay
            }
            catch
            {
                Stop-IISCommitDelay -Commit:$false
                throw
            }
            finally
            {
                Reset-IISServerManager -Confirm:$false
            }

            if ($WhatIfPreference -eq $true -and !$isPathExists)
            {
                # Set-CaccaIISSiteAcl requires path to exist
            }
            else
            {
                $appPoolIdentity = Get-IISAppPool $AppPoolName | Get-IISAppPoolUsername
                if ($WhatIfPreference -eq $true -and [string]::IsNullOrWhiteSpace($appPoolIdentity))
                {
                    $appPoolIdentity = "IIS AppPool\$AppPoolName"
                }

                Write-Information "Granting file permissions to '$appPoolIdentity'"
                $siteAclParams = @{
                    SitePath        = $Path
                    AppPoolIdentity = $appPoolIdentity
                    ModifyPaths     = $ModifyPaths
                    ExecutePaths    = $ExecutePaths
                    SiteShellOnly   = $SiteShellOnly
                }
                Set-CaccaIISSiteAcl @siteAclParams
            }

            Get-IISSite $Name
        }
        catch
        {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\public\Remove-IISAppPool.ps1
#Requires -RunAsAdministrator

function Remove-IISAppPool {
    <#
    .SYNOPSIS
    Removes an IIS AppPool and associated file permissions
    
    .DESCRIPTION
    Removes an IIS AppPool and associated file permissions.

    File permissions on the Temp ASP.Net files will be removed.

    Where the pool uses ApplicationPoolIdentity, file permissions for this identity will be
    removed from all physical paths of all Website/Application that is assigned to this pool
    
    .PARAMETER Name
    The name of the pool to remove
    
    .PARAMETER InputObject
    The instance of the pool to remove
    
    .PARAMETER Force
    Delete the pool even if it's assigned to a Site and/or application
    
    .PARAMETER Commit
    Save changes to IIS immediately? Defaults to true
    
    .EXAMPLE
    Remove-CaccaIISAppPool MyAppPool
    
    .NOTES
    Exception thrown when:
    * Application Pool is assigned to one or more sites/applications and -Force is NOT supplied
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Name')]
    param (
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Name', Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Object', Position = 0)]
        [Microsoft.Web.Administration.ApplicationPool] $InputObject,

        [switch] $Force,

        [switch] $Commit
    )
    
    begin {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'

        if (!$PSBoundParameters.ContainsKey('Commit')) {
            $Commit = $true
        }

        $sitesAclPaths = Get-IISSiteAclPath

        $existingSiteInfo = if ($Force) {
            @()
        }
        else {
            Get-IISSiteHierarchyInfo
        }
    }
    
    process {
        try {

            Write-Information "Remove app pool '$Name'"

            [Microsoft.Web.Administration.ServerManager] $manager = Get-IISServerManager

            $pool = if ($InputObject) {
                $InputObject
            }
            else {
                $instance = $manager.ApplicationPools[$Name]
                if (!$instance) {
                    throw "Cannot delete AppPool, '$Name' does not exist"
                }
                $instance
            }            

            $inUse = $existingSiteInfo | Where-Object AppPool_Name -eq $Name
            if ($inUse) {
                throw "Cannot delete AppPool, '$Name' is used by one or more Web applications/sites"
            }

            $appPoolUsername = Get-IISAppPoolUsername $pool
            
            $appPoolIdentityCount = Get-IISAppPool | Get-IISAppPoolUsername | Where-Object { $_ -eq $appPoolUsername } |
                Measure-Object | Select-Object -Exp Count
            $isNonSharedIdentity = $appPoolIdentityCount -lt 2
            $isAppPoolIdentity = $pool.ProcessModel.IdentityType -eq 'ApplicationPoolIdentity'

            $allAclPaths = @()
            if ($isAppPoolIdentity) {
                $allAclPaths += $sitesAclPaths
            }
            if ($isNonSharedIdentity) {
                $allAclPaths += Get-CaccaTempAspNetFilesPath | ForEach-Object {
                    [PsCustomObject] @{
                        Path = $_
                        IdentityReference = $appPoolUsername
                    }
                }
            }
            $allAclPaths | Where-Object IdentityReference -eq $appPoolUsername | Remove-CaccaUserFromAcl

            if ($Commit) {
                Start-IISCommitDelay
            }
            try {

                if ($PSCmdlet.ShouldProcess($Name, 'Remove App pool')) {
                    $manager.ApplicationPools.Remove($pool)
                }
                
                if ($Commit) {
                    Stop-IISCommitDelay
                }
            }
            catch {
                if ($Commit) {
                    Stop-IISCommitDelay -Commit:$false
                }
                throw
            }
            finally {
                if ($Commit) {
                    # make sure subsequent scripts will not fail because the ServerManger is now readonly
                    Reset-IISServerManager -Confirm:$false
                }
            }

        }
        catch {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\public\Remove-IISSiteBackConnection.ps1
#Requires -RunAsAdministrator

function Remove-IISSiteBackConnection {
    <#
    .SYNOPSIS
    Removes the host name(s) from the back connections registry setting
    
    .DESCRIPTION
    Removes the host name from the back connections registry setting
    
    .PARAMETER InputObject
    The backconnection entry to remove
    
    .PARAMETER Force
    Remove even if the host name is assigned to more than one site
    
    .EXAMPLE
    Get-CaccaIISSiteBackConnection MySite | Remove-IISSiteBackConnection
    
    .NOTES
    Exception thrown when:
    * the host name is assigned to more than one site and -Force is NOT supplied
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [PsCustomObject[]] $InputObject,

        [switch] $Force
        
    )
    
    begin {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'
    }
    
    process {
        try {

            Write-Information "Remove backconnection(s) from registry"

            $shared = $InputObject | Where-Object IsShared
            if ($shared -and !$Force) {
                throw "Cannot remove hostname(s) - one or more entries are shared by multiple sites"
            }

            $hostName = $InputObject | Select-Object -Exp Hostname -Unique
            # todo: add -WhatIf support to Remove-TecBoxBackConnectionHostNames
            if ($PSCmdlet.ShouldProcess($hostName, 'Remove hostname')) {
                $hostName | Remove-TecBoxBackConnectionHostNames
            }
            
        }
        catch {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\public\Remove-IISSiteHostsFileEntry.ps1
#Requires -RunAsAdministrator

function Remove-IISSiteHostsFileEntry {
    <#
    .SYNOPSIS
    Removes the host name(s) from the hosts file
    
    .DESCRIPTION
    Removes the host name(s) from the hosts file
    
    .PARAMETER InputObject
    The host name entry to remove
    
    .PARAMETER Force
    Remove even if the host name is assigned to more than one site
    
    .EXAMPLE
    Get-CaccaIISSiteHostsFileEntry MySite | Remove-CaccaIISSiteHostsFileEntry
    
    .NOTES
    Exception thrown when:
    * the host name is assigned to more than one site and -Force is NOT supplied
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [PsCustomObject[]] $InputObject,

        [switch] $Force
        
    )
    
    begin {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'
    }
    
    process {
        try {

            Write-Information "Remove host name(s) from hosts file"

            $shared = $InputObject | Where-Object IsShared
            if ($shared -and !$Force) {
                throw "Cannot remove hostname(s) - one or more entries are shared by multiple sites"
            }

            $hostName = $InputObject | Select-Object -Exp Hostname -Unique
            # todo: add -WhatIf support to Remove-TecBoxHostnames
            if ($PSCmdlet.ShouldProcess($hostName, 'Remove hostname')) {
                $hostName | Remove-TecBoxHostnames
            }
            
        }
        catch {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\public\Remove-IISWebApp.ps1
#Requires -RunAsAdministrator

function Remove-IISWebApp
{
    <#
    .SYNOPSIS
    Removes an IIS Web Application, it's associated App Pool and file permissions
    
    .DESCRIPTION
    Removes an IIS Web Application, it's associated App Pool and file permissions.

    By default just the file permissions assigned to the phyical file path of the web
    application and Temp ASP.Net files will be removed.

    Use 'ModifyPaths' and/or 'ExecutePaths' to supply additional paths to remove file
    permissions from. Typically these paths are the ones supplied to the New-CaccaIISWebApp
    cmdlet.
    
    .PARAMETER SiteName
    The name of the IIS Website to add the application to
    
    .PARAMETER Name
    The logical path name of the application (eg MyApp, /MyApp/NestedApp)
    
    .PARAMETER ModifyPaths
    Additional paths to remove modify file permissions from. Path(s) relative to 'Path' can be supplied

    .PARAMETER ExecutePaths
    Additional paths to remove read+excute permissions from. Path(s) relative to 'Path' can be supplied
    
    .EXAMPLE
    Remove-CaccaIISWebApp MySite MyApp
    
    .NOTES
    An App Pool that is also assigned to other Web Application's will NOT be removed
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $SiteName,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $Name,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string[]] $ModifyPaths,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string[]] $ExecutePaths
    )
    
    begin
    {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'
    }
    
    process
    {
        try
        {
            Write-Information "Remove Web application '$Name'"

            $SiteName = $SiteName.Trim()
            $Name = $Name.Trim()
    
            if (!$Name.StartsWith('/'))
            {
                $Name = '/' + $Name
            }
            if ($null -eq $ModifyPaths)
            {
                $ModifyPaths = @()
            }
            if ($null -eq $ExecutePaths)
            {
                $ExecutePaths = @()
            }


            # note: NOT throwing to be consistent with IISAdministration\Remove-IISSite
            $site = Get-IISSite $SiteName
            if (!$site)
            {
                return
            }

            # note: NOT throwing to be consistent with IISAdministration\Remove-IISSite
            $app = $site.Applications[$Name]
            if (!$app)
            {
                Write-Warning "Web Application '$SiteName$Name' does not exist"
                return
            }

            $appPoolIdentity = Get-IISAppPool ($app.ApplicationPoolName) | Get-IISAppPoolUsername
            $aclInfo = @{
                AppPath             = $app.VirtualDirectories['/'].PhysicalPath
                AppPoolIdentity     = $appPoolIdentity
                ModifyPaths         = $ModifyPaths
                ExecutePaths        = $ExecutePaths
                SkipMissingPaths    = $true
                # file permissions for Temp AP.Net Files folders *might* be shared so must skip removing these
                # cleaning up of orphaned file permissions will happen below when 'Remove-IISAppPool' is run
                SkipTempAspNetFiles = $true
            }
            Remove-CaccaIISSiteAcl @aclInfo

            Start-IISCommitDelay
            try
            {

                if ($PSCmdlet.ShouldProcess("$SiteName$Name", 'Remove Web Application'))
                {
                    $site.Applications.Remove($app)
                }

                if ($WhatIfPreference -ne $true)
                {
                    # note: skipping errors when deleting app pool when that pool is shared by other sites/apps
                    Remove-IISAppPool ($app.ApplicationPoolName) -EA Ignore -Commit:$false
                }

                Stop-IISCommitDelay
            }
            catch
            {
                Stop-IISCommitDelay -Commit:$false
                throw
            }
            finally
            {
                Reset-IISServerManager -Confirm:$false
            }
        }
        catch
        {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\public\Remove-IISWebsite.ps1
#Requires -RunAsAdministrator

function Remove-IISWebsite {
    <#
    .SYNOPSIS
    Removes an IIS Website, it's associated App Pool, file permissions, hosts file and back connection entries
    
    .DESCRIPTION
    Removes an IIS Website, it's associated App Pool, file permissions, hosts file and back connection entries

    When removing file permissions assigned to the identity of the application pool(s) for the site,
    the following paths will be searched:
    - the phyical file path of the site and all their subfolders and files
    - the phyical file path of all child applications and all their subfolders and files
    - all Temp ASP.Net files folders
    
    .PARAMETER Name
    The name of the IIS Website to add the application to
    
    .PARAMETER KeepBackConnection
    Don't remove the host name(s) for this site from the back connections list?
    
    .PARAMETER KeepHostsFileEntry
    Don't remove the host name(s) for this site from the hosts file?
    
    .EXAMPLE
    Remove-CaccaIISWebsite MySite
    
    .NOTES
    An App Pool that is also assigned to other Websites will NOT be removed
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param (
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $Name,

        [switch] $KeepBackConnection,

        [switch] $KeepHostsFileEntry
    )
    
    begin {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'
    }
    
    process {
        try {

            Write-Information "Remove Website '$Name'"

            $Name = $Name.Trim()

            # note: this will produce a warning if site does not exist (this is the desire behaviour - no need to reproduce here)
            $siteInfo = Get-IISSiteHierarchyInfo $Name

            if (!$siteInfo) {
                return
            }

            Get-IISSiteAclPath $Name -Recurse | Where-Object IsShared -eq $false | Remove-CaccaUserFromAcl

            if (!$KeepHostsFileEntry) {
                Get-IISSiteHostsFileEntry $Name | Where-Object IsShared -eq $false | Remove-IISSiteHostsFileEntry
            }

            if (!$KeepBackConnection) {
                Get-IISSiteBackConnection $Name | Where-Object IsShared -eq $false | Remove-IISSiteBackConnection
            }

            Start-IISCommitDelay
            try {

                Remove-IISSite $Name -Confirm:$false

                if ($WhatIfPreference -ne $true) {
                    # note: skipping errors when deleting app pool when that pool is shared by other sites
                    $siteInfo | Select-Object -Exp AppPool_Name -Unique | 
                        Remove-IISAppPool -EA Ignore -Commit:$false
                }

                Stop-IISCommitDelay     
            }
            catch {
                Stop-IISCommitDelay -Commit:$false
                throw
            }
            finally {
                Reset-IISServerManager -Confirm:$false
            }

        }
        catch {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
# .\IISSiteInstall\public\Set-IISAppPoolUser.ps1
function Set-IISAppPoolUser {
    <#
    .SYNOPSIS
    Set the Windows account identity of the App Pool
    
    .DESCRIPTION
    Set the Windows account identity of the App Pool
    
    .PARAMETER Credential
    The credential of a specific account to assign as the identity
    
    .PARAMETER IdentityType
    The built-in windows account to assign as the identity
    
    .PARAMETER InputObject
    The App Pool whose identity is to be assigned
    
    .PARAMETER Commit
    Save changes to IIS immediately? Defaults to true
    
    .EXAMPLE
    Get-IISAppPool MyAppPool | Set-CaccaIISAppPoolUser -IdentityType ApplicationPoolIdentity

    Description
    -----------
    Set the identity of the app ppol to use ApplicationPoolIdentity. In this example, the virtual
    user 'IIS AppPool\MyAppPool' will be assigned as the Windows identity

    .EXAMPLE
    $pswd = ConvertTo-SecureString '(mypassword)' -AsPlainText -Force
    $creds = [PsCredential]::new("$($env:COMPUTERNAME)\MyLocalUser", $pswd)

    New-CaccaIISAppPool $tempAppPool -Config {
        $_ | Set-CaccaIISAppPoolUser $creds -Commit:$false
    }

    Description
    -----------
    Create an pool with an identity assigned to a specific user account
    
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "", Justification="Dummy credentials")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    param (
        [PsCredential] $Credential,

        [ValidateSet('ApplicationPoolIdentity', 'LocalService', 'LocalSystem', 'NetworkService', 'SpecificUser')]
        [string] $IdentityType = 'SpecificUser',

        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNull()]
        [Microsoft.Web.Administration.ApplicationPool] $InputObject,
        
        [switch] $Commit
    )
    
    begin {
        Set-StrictMode -Version 'Latest'
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $callerEA = $ErrorActionPreference
        $ErrorActionPreference = 'Stop'

        if ($IdentityType -eq 'SpecificUser' -and !$Credential) {
            throw "A Credential for 'SpecificUser' must be supplied"
        }

        if (!$PSBoundParameters.ContainsKey('Commit')) {
            $Commit = $true
        }
    }
    
    process {
        try {

            Write-Information "Assign identity of app pool '$($InputObject.Name)'"

            if ($IdentityType -ne 'SpecificUser') {
                $dummyPassword = ConvertTo-SecureString 'dummy' -AsPlainText -Force
                $username = ConvertTo-BuiltInUsername $IdentityType ($InputObject.Name)
                $Credential = [PsCredential]::new($username, $dummyPassword)
            }

            if ($Commit) {
                Start-IISCommitDelay
            }
            try {
                if ($Credential.UserName -like 'IIS AppPool\*'){
                    $InputObject.ProcessModel.IdentityType = 'ApplicationPoolIdentity'
                } elseif($Credential.UserName -eq 'NT AUTHORITY\NETWORK SERVICE') {
                    $InputObject.ProcessModel.IdentityType = 'NetworkService'
                } elseif ($Credential.UserName -eq 'NT AUTHORITY\SYSTEM') {
                    $InputObject.ProcessModel.IdentityType = 'LocalSystem'
                } elseif ($Credential.UserName -eq 'NT AUTHORITY\LOCAL SERVICE') {
                    $InputObject.ProcessModel.IdentityType = 'LocalService'
                } else {
                    $InputObject.ProcessModel.UserName = $Credential.UserName
                    $InputObject.ProcessModel.Password = $Credential.GetNetworkCredential().Password
                    $InputObject.ProcessModel.IdentityType = 'SpecificUser'
                }
                if ($Commit) {
                    Stop-IISCommitDelay
                }       
            }
            catch {
                if ($Commit) {
                    Stop-IISCommitDelay -Commit:$false
                }
                throw
            }
            finally {
                if ($Commit) {
                    # make sure subsequent scripts will not fail because the ServerManger is now readonly
                    Reset-IISServerManager -Confirm:$false
                }
            }

        }
        catch {
            Write-Error -ErrorRecord $_ -EA $callerEA
        }
    }
}
Write-Verbose 'Importing from [C:\MyProjects\IISSiteInstall\IISSiteInstall\classes]'