Carbon.IIS.psm1

# Copyright WebMD Health Services
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License

using module '.\Carbon.IIS.Enums.psm1'
using namespace System.Management.Automation
using namespace Microsoft.Web.Administration

#Requires -Version 5.1
Set-StrictMode -Version 'Latest'
$InformationPreference = 'Continue'

# Functions should use $script:moduleRoot as the relative root from which to find
# things. A published module has its function appended to this file, while a
# module in development has its functions in the Functions directory.
$script:moduleRoot = $PSScriptRoot
$script:warningMessages = @{}
$script:applicationHostPath =
    Join-Path -Path ([Environment]::SystemDirectory) -ChildPath 'inetsrv\config\applicationHost.config'

Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath 'PSModules\Carbon.Core' -Resolve) `
              -Function @('Add-CTypeData', 'Resolve-CFullPath')

Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath 'PSModules\Carbon.Windows.HttpServer' -Resolve) `
              -Function @('Set-CSslCertificateBinding')

function Test-MSWebAdministrationLoaded
{
    $serverMgrType =
        [AppDomain]::CurrentDomain.GetAssemblies() |
        Where-Object { $_.Location -and ($_.Location | Split-Path -Leaf) -eq 'Microsoft.Web.Administration.dll' }
    return $null -ne $serverMgrType
}

$numErrorsAtStart = $Global:Error.Count
if( -not (Test-MSWebAdministrationLoaded) )
{
    $pathsToTry = & {
            # This is our preferred assembly. Always try it first.
            if( [Environment]::SystemDirectory )
            {
                $msWebAdminPath = Join-Path -Path ([Environment]::SystemDirectory) `
                                            -ChildPath 'inetsrv\Microsoft.Web.Administration.dll'
                Get-Item -Path $msWebAdminPath -ErrorAction SilentlyContinue
            }

            # If any IIS module is installed, it might have a copy. Find them but make sure they are sorted from
            # newest version to oldest version.
            Get-Module -Name 'IISAdministration', 'WebAdministration' -ListAvailable |
                Select-Object -ExpandProperty 'Path' |
                Split-Path -Parent |
                Get-ChildItem -Filter 'Microsoft.Web.Administration.dll' -Recurse -ErrorAction SilentlyContinue |
                Sort-Object { [Version]$_.VersionInfo.FileVersion } -Descending
        }

    foreach( $pathToTry in $pathsToTry )
    {
        try
        {
            Add-Type -Path $pathToTry.FullName
            Write-Debug "Loaded required assembly Microsoft.Web.Administration from ""$($pathToTry)""."
            break
        }
        catch
        {
            Write-Debug "Failed to load assembly ""$($pathToTry)"": $($_)."
        }
    }
}

if( -not (Test-MSWebAdministrationLoaded) )
{
    try
    {
        Add-Type -AssemblyName 'Microsoft.Web.Administration' `
                 -ErrorAction SilentlyContinue `
                 -ErrorVariable 'addTypeErrors'
        if( -not $addTypeErrors )
        {
            Write-Debug "Loaded required assembly Microsoft.Web.Administration from GAC."
        }
    }
    catch
    {
    }
}

if( -not (Test-MSWebAdministrationLoaded) )
{
    Write-Error -Message "Unable to find and load required assembly Microsoft.Web.Administration." -ErrorAction Stop
    return
}

$script:serverMgr = [Microsoft.Web.Administration.ServerManager]::New()
$script:serverMgrCreatedAt = [DateTime]::UtcNow
if( -not $script:serverMgr -or $null -eq $script:serverMgr.ApplicationPoolDefaults )
{
    Write-Error -Message "Carbon.IIS is not supported on this version of PowerShell." -ErrorAction Stop
    return
}

# We successfully loaded Microsoft.Web.Administration assembly, so remove the errors we encountered trying to do so.
for( $idx = $Global:Error.Count ; $idx -gt $numErrorsAtStart ; --$idx )
{
    $Global:Error.RemoveAt(0)
}

Add-CTypeData -TypeName 'Microsoft.Web.Administration.Site' `
              -MemberType ScriptProperty `
              -MemberName 'PhysicalPath' `
              -Value {
                    $this.Applications |
                        Where-Object 'Path' -EQ '/' |
                        Select-Object -ExpandProperty 'VirtualDirectories' |
                        Where-Object 'Path' -EQ '/' |
                        Select-Object -ExpandProperty 'PhysicalPath'
                }

Add-CTypeData -TypeName 'Microsoft.Web.Administration.Application' `
              -MemberType ScriptProperty `
              -MemberName 'PhysicalPath' `
              -Value {
                    $this.VirtualDirectories |
                        Where-Object 'Path' -EQ '/' |
                        Select-Object -ExpandProperty 'PhysicalPath'
                }

# Store each of your module's functions in its own file in the Functions
# directory. On the build server, your module's functions will be appended to
# this file, so only dot-source files that exist on the file system. This allows
# developers to work on a module without having to build it first. Grab all the
# functions that are in their own files.
$functionsPath = & {
    Join-Path -Path $script:moduleRoot -ChildPath 'Functions\*.ps1'
    Join-Path -Path $script:moduleRoot -ChildPath 'Carbon.IIS.ArgumentCompleters.ps1'
}
foreach ($importPath in $functionsPath)
{
    if( -not (Test-Path -Path $importPath) )
    {
        continue
    }

    foreach( $fileInfo in (Get-Item $importPath) )
    {
        . $fileInfo.FullName
    }
}



function Add-CIisDefaultDocument
{
    <#
    .SYNOPSIS
    Adds a default document name to a website.
 
    .DESCRIPTION
    If you need a custom default document for your website, this function will add it. The `FileName` argument should
    be a filename IIS should use for a default document, e.g. home.html.
 
    If the website already has `FileName` in its list of default documents, this function silently returns.
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .EXAMPLE
    Add-CIisDefaultDocument -SiteName MySite -FileName home.html
 
    Adds `home.html` to the list of default documents for the MySite website.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of the site where the default document should be added.
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # The default document to add.
        [Parameter(Mandatory)]
        [String] $FileName
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $section = Get-CIisConfigurationSection -LocationPath $LocationPath -SectionPath 'system.webServer/defaultDocument'
    if( -not $section )
    {
        return
    }

    [Microsoft.Web.Administration.ConfigurationElementCollection] $files = $section.GetCollection('files')
    $defaultDocElement = $files | Where-Object { $_["value"] -eq $FileName }
    if ($defaultDocElement)
    {
        return
    }

    Write-Information "IIS:$($section.LocationPath):$($section.SectionPath) + $($FileName)"
    $defaultDocElement = $files.CreateElement('add')
    $defaultDocElement["value"] = $FileName
    $files.Add( $defaultDocElement )
    Save-CIisConfiguration
}




function ConvertTo-CIisVirtualPath
{
    <#
    .SYNOPSIS
    Turns a virtual path into a canonical virtual path like you would find in IIS's applicationHost.config
 
    .DESCRIPTION
    The `ConvertTo-CIisVirtualPath` takes in a path and converts it to a canonical virtual path as it would be saved to
    IIS's applicationHost.config:
 
    * duplicate directory separator characters are removed
    * relative path segments (e.g. `.` or `..`) are resolved and removed (i.e. `path/one/../two` changes to `path/two`)
    * all `\` characters are converted to `/`
    * Leading and trailing `/' characters are removed.
    * Adds a leading `/` character
 
    If you don't want a leading `/` character, use the `NoLeadingSlash` switch.
 
    .EXAMPLE
    "/some/path/" | ConvertTo-CIisVirtualPath
 
    Would return "/some/path".
 
    .EXAMPLE
 
    "path" | ConvertTo-CIisVirtualPath
 
    Would return "/path"
 
    .EXAMPLE
 
    "\some\path" | ConvertTo-CIisVirtualPath
 
    Would return "/some/path"
    #>

    [CmdletBinding()]
    param(
        # The path to convert/normalize.
        [Parameter(Mandatory, ValueFromPipeline)]
        [AllowNull()]
        [AllowEmptyString()]
        [String] $Path,

        # If true, omits the leading slash on the returned path. The default is to include a leading slash.
        [switch] $NoLeadingSlash
    )

    begin
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        $leadingSlash = '/'
        if( $NoLeadingSlash )
        {
            $leadingSlash = ''
        }

        # GetFullPath removes extra slashes, dots but prefixes a path with a root path (e.g. C:\ or /). We need to get
        # this system's root path so we can use GetFullPath to canonicalize our path, but remove the extra root path
        # prefix.
        $root = [IO.Path]::GetFullPath('/')
    }

    process
    {
        if( -not $Path )
        {
            return $leadingSlash
        }

        $indent = ' ' * $Path.Length
        Write-Debug "$($Path) -->"

        $prevPath = $Path
        $Path = $Path.Trim('/', '\')
        if( $Path -ne $prevPath )
        {
            Write-Debug "$($indent) |- $($Path)"
        }

        if (-not $Path)
        {
            return $leadingSlash
        }

        $prevPath = $Path
        $Path = $Path | Split-Path -NoQualifier
        if( $Path -ne $prevPath )
        {
            Write-Debug "$($indent) |- $($Path)"
        }

        $prevPath = $Path
        if( [IO.Path]::GetFullPath.OverloadDefinitions.Count -eq 1 )
        {
            $Path = Join-Path -Path $root -ChildPath $Path
            $Path = [IO.Path]::GetFullPath($Path)
        }
        else
        {
            $Path = [IO.Path]::GetFullPath($Path, $root)
        }
        $Path = $Path.Substring($root.Length)
        if( $Path -ne $prevPath )
        {
            Write-Debug "$($indent) |- $($Path)"
        }

        $prevPath = $Path
        $Path = $Path.Replace('\', '/')
        if( $Path -ne $prevPath )
        {
            Write-Debug "$($indent) |- $($Path)"
        }

        $prevPath = $Path
        $Path = $Path.Trim('\', '/')
        if( $Path -ne $prevPath )
        {
            Write-Debug "$($indent) |- $($Path)"
        }

        $Path = "$($leadingSlash)$($Path)"
        Write-Debug "$($Path)$(' ' * ([Math]::Max(($indent.Length - $Path.Length), 0))) <--"

        return $Path
    }
}


function Copy-Hashtable
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [Collections.IDictionary] $InputObject,

        [String[]] $Key
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        if( -not $Key )
        {
            return $InputObject.Clone()
        }

        $newHashtable = @{}

        foreach( $keyItem in $Key )
        {
            if( -not $InputObject.ContainsKey($keyItem) )
            {
                continue
            }

            $newHashtable[$keyItem] = $InputObject[$keyItem]
        }

        return $newHashtable
    }
}



function Disable-CIisSecurityAuthentication
{
    <#
    .SYNOPSIS
    Disables anonymous, basic, or Windows authentication for all or part of a website.
 
    .DESCRIPTION
    The `Disable-CIisSecurityAuthentication` function disables anonymous, basic, or Windows authentication for a
    website, application, virtual directory, or directory. Pass the path to the `LocationPath` parameter. Use the
    `Anonymous` switch to disable anonymous authentication, the `Basic` switch to disable basic authentication, or the
    `Windows` switch to disable Windows authentication.
 
    .LINK
    Enable-CIisSecurityAuthentication
 
    .LINK
    Get-CIisSecurityAuthentication
 
    .LINK
    Test-CIisSecurityAuthentication
 
    .EXAMPLE
    Disable-CIisSecurityAuthentication -LocationPath 'Peanuts' -Anonymous
 
    Turns off anonymous authentication for the `Peanuts` website.
 
    .EXAMPLE
    Disable-CIisSecurityAuthentication -LocationPath 'Peanuts/Snoopy/DogHouse' -Basic
 
    Turns off basic authentication for the `Snoopy/DogHouse` directory under the `Peanuts` website.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The location path to the website, directory, application, or virtual directory where authentication should be
        # disabled.
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use `LocationPath` parameter instead.
        [Alias('Path')]
        [String] $VirtualPath,

        # Disable anonymous authentication.
        [Parameter(Mandatory, ParameterSetName='anonymousAuthentication')]
        [switch] $Anonymous,

        # Disable basic authentication.
        [Parameter(Mandatory, ParameterSetName='basicAuthentication')]
        [switch] $Basic,

        # Disable Windows authentication.
        [Parameter(Mandatory, ParameterSetName='windowsAuthentication')]
        [switch] $Windows
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($VirtualPath)
    {
        Write-CIisWarningOnce -ForObsoleteSiteNameAndVirtualPathParameter
    }

    $sectionPath = "system.webServer/security/authentication/$($PSCmdlet.ParameterSetName)"
    Set-CIisConfigurationAttribute -LocationPath ($LocationPath, $VirtualPath | Join-CIisPath) `
                                   -SectionPath $sectionPath `
                                   -Name 'enabled' `
                                   -Value $false
}



function Enable-CIisDirectoryBrowsing
{
    <#
    .SYNOPSIS
    Enables directory browsing under all or part of a website.
 
    .DESCRIPTION
    Enables directory browsing (i.e. showing the contents of a directory by requesting that directory in a web browser) for a website. To enable directory browsing on a directory under the website, pass the virtual path to that directory as the value to the `Directory` parameter.
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .EXAMPLE
    Enable-CIisDirectoryBrowsing -SiteName Peanuts
 
    Enables directory browsing on the `Peanuts` website.
 
    .EXAMPLE
    Enable-CIisDirectoryBrowsing -SiteName Peanuts -Directory Snoopy/DogHouse
 
    Enables directory browsing on the `/Snoopy/DogHouse` directory under the `Peanuts` website.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The location path to the website, directory, application, or virtual directory where directory browsing should
        # be enabled.
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use `LocationPath` parameter instead.
        [Alias('Path')]
        [String] $VirtualPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($VirtualPath)
    {
        Write-CIisWarningOnce -ForObsoleteSiteNameAndVirtualPathParameter
    }

    Set-CIisConfigurationAttribute -LocationPath ($LocationPath, $VirtualPath | Join-CIisPath) `
                                   -SectionPath 'system.webServer/directoryBrowse' `
                                   -Name 'enabled' `
                                   -Value $true
}




function Enable-CIisSecurityAuthentication
{
    <#
    .SYNOPSIS
    Enables anonymous, basic, or Windows authentication for an entire site or a sub-directory of that site.
 
    .DESCRIPTION
    The `Enable-CIisSecurityAuthentication` function enables anonymous, basic, or Windows authentication for a website,
    application, virtual directory, or directory. Pass the location's path to the `LocationPath` parameter. Use the
    `Anonymous` switch to enable anonymous authentication, the `Basic` switch to enable basic authentication, or the
    `Windows` switch to enable Windows authentication.
 
    .LINK
    Disable-CIisSecurityAuthentication
 
    .LINK
    Get-CIisSecurityAuthentication
 
    .LINK
    Test-CIisSecurityAuthentication
 
    .EXAMPLE
    Enable-CIisSecurityAuthentication -LocationPath 'Peanuts' -Anonymous
 
    Turns on anonymous authentication for the `Peanuts` website.
 
    .EXAMPLE
    Enable-CIisSecurityAuthentication -LocationPath 'Peanuts/Snoopy/DogHouse' -Basic
 
    Turns on anonymous authentication for the `Snoopy/DogHouse` directory under the `Peanuts` website.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The location path to the website, application, virtual directory, or directory where the authentication
        # method should be enabled.
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [Alias('Path')]
        [String] $VirtualPath,

        # Enable anonymous authentication.
        [Parameter(Mandatory, ParameterSetName='anonymousAuthentication')]
        [switch] $Anonymous,

        # Enable basic authentication.
        [Parameter(Mandatory, ParameterSetName='basicAuthentication')]
        [switch] $Basic,

        # Enable Windows authentication.
        [Parameter(Mandatory, ParameterSetName='windowsAuthentication')]
        [switch] $Windows
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($VirtualPath)
    {
        Write-CIisWarningOnce -ForObsoleteSiteNameAndVirtualPathParameter
    }

    $sectionPath = "system.webServer/security/authentication/$($PSCmdlet.ParameterSetName)"
    Set-CIisConfigurationAttribute -LocationPath ($LocationPath, $VirtualPath | Join-CIisPath) `
                                   -SectionPath $sectionPath `
                                   -Name 'enabled' `
                                   -Value $true
}



function Enable-CIisSsl
{
    <#
    .SYNOPSIS
    Turns on and configures SSL for a website or part of a website.
 
    .DESCRIPTION
    This function enables SSL and optionally the site/directory to:
 
     * Require SSL (the `RequireSsl` switch)
     * Ignore/accept/require client certificates (the `AcceptClientCertificates` and `RequireClientCertificates` switches).
     * Requiring 128-bit SSL (the `Require128BitSsl` switch).
 
    By default, this function will enable SSL, make SSL connections optional, ignores client certificates, and not
    require 128-bit SSL.
 
    Changing any SSL settings will do you no good if the website doesn't have an SSL binding or doesn't have an SSL
    certificate. The configuration will most likely succeed, but won't work in a browser. So sad.
 
    Beginning with IIS 7.5, the `Require128BitSsl` parameter won't actually change the behavior of a website since
    [there are no longer 128-bit crypto providers](https://forums.iis.net/p/1163908/1947203.aspx) in versions of Windows
    running IIS 7.5.
 
    .LINK
    http://support.microsoft.com/?id=907274
 
    .LINK
    Set-CIisWebsiteSslCertificate
 
    .EXAMPLE
    Enable-CIisSsl -LocationPath 'Peanuts'
 
    Enables SSL on the `Peanuts` website's, making makes SSL connections optional, ignoring client certificates, and making 128-bit SSL optional.
 
    .EXAMPLE
    Enable-CIisSsl -LocationPath 'Peanuts/Snoopy/DogHouse' -RequireSsl
 
    Configures the `/Snoopy/DogHouse` directory in the `Peanuts` site to require SSL. It also turns off any client certificate settings and makes 128-bit SSL optional.
 
    .EXAMPLE
    Enable-CIisSsl -LocationPath 'Peanuts' -AcceptClientCertificates
 
    Enables SSL on the `Peanuts` website and configures it to accept client certificates, makes SSL optional, and makes 128-bit SSL optional.
 
    .EXAMPLE
    Enable-CIisSsl -LocationPath 'Peanuts' -RequireSsl -RequireClientCertificates
 
    Enables SSL on the `Peanuts` website and configures it to require SSL and client certificates. You can't require client certificates without also requiring SSL.
 
    .EXAMPLE
    Enable-CIisSsl -LocationPath 'Peanuts' -Require128BitSsl
 
    Enables SSL on the `Peanuts` website and require 128-bit SSL. Also, makes SSL connections optional and ignores client certificates.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='IgnoreClientCertificates')]
    param(
        # The website whose SSL flags should be modifed.
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [Alias('Path')]
        [String] $VirtualPath,

        # Should SSL be required?
        [Parameter(ParameterSetName='IgnoreClientCertificates')]
        [Parameter(ParameterSetName='AcceptClientCertificates')]
        [Parameter(Mandatory, ParameterSetName='RequireClientCertificates')]
        [switch] $RequireSsl,

        # Requires 128-bit SSL. Only changes IIS behavior in IIS 7.0.
        [switch] $Require128BitSsl,

        # Should client certificates be accepted?
        [Parameter(ParameterSetName='AcceptClientCertificates')]
        [switch] $AcceptClientCertificates,

        # Should client certificates be required? Also requires SSL ('RequireSsl` switch).
        [Parameter(Mandatory, ParameterSetName='RequireClientCertificates')]
        [switch] $RequireClientCertificates
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $SslFlags_Ssl = 8
    $SslFlags_SslNegotiateCert = 32
    $SslFlags_SslRequireCert = 64
    $SslFlags_SslMapCert = 128
    $SslFlags_Ssl128 = 256

    $intFlag = 0
    $flags = @()
    if( $RequireSSL -or $RequireClientCertificates )
    {
        $flags += 'Ssl'
        $intFlag = $intFlag -bor $SslFlags_Ssl
    }

    if( $AcceptClientCertificates -or $RequireClientCertificates )
    {
        $flags += 'SslNegotiateCert'
        $intFlag = $intFlag -bor $SslFlags_SslNegotiateCert
    }

    if( $RequireClientCertificates )
    {
        $flags += 'SslRequireCert'
        $intFlag = $intFlag -bor $SslFlags_SslRequireCert
    }

    if( $Require128BitSsl )
    {
        $flags += 'Ssl128'
        $intFlag = $intFlag -bor $SslFlags_Ssl128
    }

    $sectionPath = 'system.webServer/security/access'
    $section =
        Get-CIisConfigurationSection -LocationPath $LocationPath -VirtualPath $VirtualPath -SectionPath $sectionPath
    if( -not $section )
    {
        return
    }

    $flags = $flags -join ','
    $currentIntFlag = $section['sslFlags']
    $currentFlags = @( )
    if( $currentIntFlag -band $SslFlags_Ssl )
    {
        $currentFlags += 'Ssl'
    }
    if( $currentIntFlag -band $SslFlags_SslNegotiateCert )
    {
        $currentFlags += 'SslNegotiateCert'
    }
    if( $currentIntFlag -band $SslFlags_SslRequireCert )
    {
        $currentFlags += 'SslRequireCert'
    }
    if( $currentIntFlag -band $SslFlags_SslMapCert )
    {
        $currentFlags += 'SslMapCert'
    }
    if( $currentIntFlag -band $SslFlags_Ssl128 )
    {
        $currentFlags += 'Ssl128'
    }

    if( -not $currentFlags )
    {
        $currentFlags += 'None'
    }

    $currentFlags = $currentFlags -join ','

    if( $section['sslFlags'] -ne $intFlag )
    {
        $target = "IIS:$($section.LocationPath):$($section.SectionPath)"
        $infoMsg = "$($target) sslFlags $($section['sslFlags']) -> $($flags)"
        $section['sslFlags'] = $flags
        Save-CIisConfiguration -Target $target -Action 'Enable SSL' -Message $infoMsg
    }
}




function Get-CIisApplication
{
    <#
    .SYNOPSIS
    Gets an IIS application as an `Application` object.
 
    .DESCRIPTION
    Uses the `Microsoft.Web.Administration` API to get an IIS application object. If the application doesn't exist, `$null` is returned.
 
    If you make any changes to any of the objects returned by `Get-CIisApplication`, call `Save-CIisConfiguration` to
    save those changes to IIS.
 
    The objects returned each have a `PhysicalPath` property which is the physical path to the application.
 
    .OUTPUTS
    Microsoft.Web.Administration.Application.
 
    .EXAMPLE
    Get-CIisApplication -SiteName 'DeathStar`
 
    Gets all the applications running under the `DeathStar` website.
 
    .EXAMPLE
    Get-CIisApplication -SiteName 'DeathStar' -VirtualPath '/'
 
    Demonstrates how to get the main application for a website: use `/` as the application name.
 
    .EXAMPLE
    Get-CIisApplication -SiteName 'DeathStar' -VirtualPath 'MainPort/ExhaustPort'
 
    Demonstrates how to get a nested application, i.e. gets the application at `/MainPort/ExhaustPort` under the `DeathStar` website.
    #>

    [CmdletBinding(DefaultParameterSetName='AllApplications')]
    [OutputType([Microsoft.Web.Administration.Application])]
    param(
        # The site where the application is running.
        [Parameter(Mandatory, ParameterSetName='SpecificApplication')]
        [String] $SiteName,

        # The path/name of the application. Default is to return all applications running under the website given by
        # the `SiteName` parameter. Wildcards supported.
        [Parameter(ParameterSetName='SpecificApplication')]
        [Alias('Name')]
        [String] $VirtualPath,

        [Parameter(Mandatory, ParameterSetName='Defaults')]
        [switch] $Defaults
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($PSCmdlet.ParameterSetName -eq 'Defaults')
    {
        return (Get-CIisServerManager).ApplicationDefaults
    }

    $site = Get-CIisWebsite -Name $SiteName
    if( -not $site )
    {
        return
    }

    $VirtualPath = $VirtualPath | ConvertTo-CIisVirtualPath

    $site.Applications |
        Where-Object {
            if ($PSBoundParameters.ContainsKey('VirtualPath'))
            {
                return ($_.Path -like $VirtualPath)
            }
            return $true
        }
}




function Get-CIisAppPool
{
    <#
    .SYNOPSIS
    Gets IIS application pools.
 
    .DESCRIPTION
    The `Get-CIisAppPool` function returns all IIS application pools that are installed on the current computer. To
    get a specific application pool, pass its name to the `Name` parameter. If the application pool doesn't exist,
    an error is written and nothing is returned.
 
    You can get the application pool defaults settings by using the `Defaults` switch. If `Defaults` is true, then
    the `Name` parameter is ignored.
 
    If you make any changes to any of the objects returned by `Get-CIisAppPool`, call the `Save-CIisConfiguration`
    function to save those changes to IIS.
 
    This function disposes the current server manager object that Carbon.IIS uses internally. Make sure you have no
    pending, unsaved changes when calling `Get-CIisAppPool`.
 
    .LINK
    http://msdn.microsoft.com/en-us/library/microsoft.web.administration.applicationpool(v=vs.90).aspx
 
    .OUTPUTS
    Microsoft.Web.Administration.ApplicationPool.
 
    .EXAMPLE
    Get-CIisAppPool
 
    Demonstrates how to get *all* application pools.
 
    .EXAMPLE
    Get-CIisAppPool -Name 'Batcave'
 
    Gets the `Batcave` application pool.
 
    .EXAMPLE
    Get-CIisAppPool -Defaults
 
    Demonstrates how to get IIS application pool defaults settings.
    #>

    [CmdletBinding(DefaultParameterSetName='AllAppPools')]
    [OutputType([Microsoft.Web.Administration.ApplicationPool])]
    param(
        # The name of the application pool to return. If not supplied, all application pools are returned. Wildcards
        # supported.
        [Parameter(Mandatory, ParameterSetName='AppPoolByWildcard', Position=0)]
        [String] $Name,

        # Instead of getting app pools or a specific app pool, return application pool defaults settings. If true, the
        # `Name` parameter is ignored.
        [Parameter(Mandatory, ParameterSetName='Defaults')]
        [switch] $Defaults
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $WhatIfPreference = $false

    $mgr = Get-CIisServerManager

    if( $Defaults )
    {
        return $mgr.ApplicationPoolDefaults
    }

    $appPools = @()
    $mgr.ApplicationPools |
        Where-Object {
            if ($Name)
            {
                return $_.Name -like $Name
            }

            return $true
        } |
        Tee-Object -Variable 'appPools' |
        Write-Output

    if (($Name -and -not [wildcardpattern]::ContainsWildcardCharacters($Name) -and -not $appPools))
    {
        $msg = "IIS application pool ""$($Name)"" does not exist."
        Write-Error $msg -ErrorAction $ErrorActionPreference
    }
}




function Get-CIisConfigurationLocationPath
{
    <#
    .SYNOPSIS
    Gets the paths of all <location> element from applicationHost.config.
 
    .DESCRIPTION
    The `Get-CIisConfigurationLocationPath` function returns the paths for each `<location>` element in the
    applicationHost.config file. These location elements are where IIS stores custom configurations for websites and
    any paths under a website. If this function returns any values, then you know at least one site or site/path has
    custom configuration.
 
    To get the path for a specific website, directory, application, or virtual directory, pass its location path to the
    `LocationPath` parameter.
 
    To get all paths under a website or website/path, use the `-Recurse` switch. If any paths are returned then that
    site has custom configuration somewhere in its hierarchy.
 
    .EXAMPLE
    Get-CIisConfigurationLocationPath
 
    Demonstrates how to get the path for each `<location>` element in the applicationHost.config file, i.e. the paths
    to each website and path under a website that has custom configuration.
 
    .EXAMPLE
    Get-CIisConfigurationLocationPath -LocationPath 'Default Web Site'
 
    Demonstrates how to get the location path for a specific site.
 
    .EXAMPLE
    Get-CIisConfigurationLocationPath -LocationPath 'Default Web Site/some/path'
 
    Demonstrates how to get the location path for a specific virtual path under a specific website.
 
    .EXAMPLE
    Get-CIisConfigurationLocationPath -LocationPath 'Default Web Site' -Recurse
 
    Demonstrates how to get the location paths for all virtual paths including and under a specific website.
 
    .EXAMPLE
    Get-CIisConfigurationLocationPath -LocationPath 'Default Web Site/some/path'
 
    Demonstrates how to get the location paths for all virtual paths including and under a specific virtual path under a
    specific website.
    #>

    [CmdletBinding()]
    param(
        # The name of a website whose location paths to get.
        [Parameter(Position=0)]
        [String] $LocationPath,

        # If true, returns all location paths under the website or website/virtual path provided.
        [switch] $Recurse
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $LocationPath = $LocationPath | ConvertTo-CIisVirtualPath -NoLeadingSlash

    $mgr = Get-CIisServerManager
    $mgr.GetApplicationHostConfiguration().GetLocationPaths() |
        Where-Object { $_ } |
        Where-Object {
            return (-not $LocationPath -or $_ -eq $LocationPath -or ($Recurse -and $_ -like "$($LocationPath)/*"))
        }
}



function Get-CIisConfigurationSection
{
    <#
    .SYNOPSIS
    Gets a Microsoft.Web.Adminisration configuration section for a given site and path.
 
    .DESCRIPTION
    Uses the Microsoft.Web.Administration API to get a `Microsoft.Web.Administration.ConfigurationSection`.
 
    .OUTPUTS
    Microsoft.Web.Administration.ConfigurationSection.
 
    .EXAMPLE
    Get-CIisConfigurationSection -SiteName Peanuts -Path Doghouse -Path 'system.webServer/security/authentication/anonymousAuthentication'
 
    Returns a configuration section which represents the Peanuts site's Doghouse path's anonymous authentication
    settings.
    #>

    [CmdletBinding(DefaultParameterSetName='Global')]
    [OutputType([Microsoft.Web.Administration.ConfigurationSection])]
    param(
        # The site whose configuration should be returned.
        [Parameter(Mandatory, ParameterSetName='ForSite', Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [Parameter(ParameterSetName='ForSite')]
        [Alias('Path')]
        [String] $VirtualPath,

        # The path to the configuration section to return.
        [Parameter(Mandatory, ParameterSetName='ForSite')]
        [Parameter(Mandatory, ParameterSetName='Global')]
        [String] $SectionPath,

        # The type of object to return. Optional.
        [Type] $Type = [Microsoft.Web.Administration.ConfigurationSection]
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $mgr = Get-CIisServerManager
    $config = $mgr.GetApplicationHostConfiguration()

    $section = $null
    try
    {
        if ($PSCmdlet.ParameterSetName -eq 'ForSite')
        {
            if ($VirtualPath)
            {
                $functionName = $PSCmdlet.MyInvocation.MyCommand.Name
                $caller = Get-PSCallStack | Select-Object -Skip 1 | Select-Object -First 1
                if ($caller.FunctionName -like '*-CIis*')
                {
                    $functionName = $caller.FunctionName
                }

                "The $($functionName) function''s ""SiteName"" and ""VirtualPath"" parameters are obsolete and have " +
                'been replaced with a single "LocationPath" parameter, which should be the combined path of the ' +
                'location/object to configure, e.g. ' +
                "``$($functionName) -LocationPath '$($LocationPath)/$($VirtualPath)'``." |
                    Write-CIisWarningOnce

                $LocationPath = Join-CIisPath -Path $LocationPath, $VirtualPath
            }

            $LocationPath = $LocationPath | ConvertTo-CIisVirtualPath
            $section = $config.GetSection( $SectionPath, $Type, $LocationPath )
        }
        else
        {
            $section = $config.GetSection( $SectionPath, $Type )
        }
    }
    catch
    {
    }

    if( $section )
    {
        if (-not ($section | Get-Member -Name 'LocationPath'))
        {
            $section | Add-Member -Name 'LocationPath' -MemberType NoteProperty -Value ''
        }
        if ($LocationPath)
        {
            $section.LocationPath = $LocationPath
        }
        return $section
    }
    else
    {
        $msg = 'IIS:{0}: configuration section {1} not found.' -f $LocationPath,$SectionPath
        Write-Error $msg -ErrorAction $ErrorActionPreference
        return
    }
}




function Get-CIisHttpHeader
{
    <#
    .SYNOPSIS
    Gets the HTTP headers for a website or directory under a website.
 
    .DESCRIPTION
    For each custom HTTP header defined under a website and/or a sub-directory under a website, returns an object with
    these properties:
 
     * Name: the name of the HTTP header
     * Value: the value of the HTTP header
 
    .LINK
    Set-CIisHttpHeader
 
    .EXAMPLE
    Get-CIisHttpHeader -LocationPath SopwithCamel
 
    Returns the HTTP headers for the `SopwithCamel` website.
 
    .EXAMPLE
    Get-CIisHttpHeader -LocationPath 'SopwithCamel/Engine'
 
    Returns the HTTP headers for the `Engine` directory under the `SopwithCamel` website.
 
    .EXAMPLE
    Get-CIisHttpHeader -LocationPath SopwithCambel -Name 'X-*'
 
    Returns all HTTP headers which match the `X-*` wildcard.
    #>

    [CmdletBinding()]
    param(
        # The name of the website whose headers to return.
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [Alias('Path')]
        [String] $VirtualPath,

        # The name of the HTTP header to return. Optional. If not given, all headers are returned. Wildcards
        # supported.
        [String] $Name = '*'
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $sectionPath = 'system.webServer/httpProtocol'

    $httpProtocol =
        Get-CIisConfigurationSection -LocationPath $LocationPath -VirtualPath $VirtualPath -SectionPath $sectionPath

    $httpProtocol.GetCollection('customHeaders') |
        Where-Object { $_['name'] -like $Name } |
        ForEach-Object {
            $header = [pscustomobject]@{ Name = $_['name']; Value = $_['value'] }
            $header.pstypenames.Insert(0, 'Carbon.Iis.HttpHeader')
            $header | Write-Output
        }
}





function Get-CIisHttpRedirect
{
    <#
    .SYNOPSIS
    Gets the HTTP redirect settings for a website or virtual directory/application under a website.
 
    .DESCRIPTION
    Returns a `[Microsoft.Web.Administration.ConfigurationSection]` object with these attributes:
 
     * enabled - `True` if the redirect is enabled, `False` otherwise.
     * destination - The URL where requests are directed to.
     * httpResponseCode - The HTTP status code sent to the browser for the redirect.
     * exactDestination - `True` if redirects are to destination, regardless of the request path. This will send all
     requests to `Destination`.
     * childOnly - `True` if redirects are only to content in the destination directory (not subdirectories).
 
     Use the `GetAttributeValue` and `SetAttributeValue` to get and set values and the `Save-CIisConfiguration` function
     to save the changes to IIS.
 
    .LINK
    http://www.iis.net/configreference/system.webserver/httpredirect
 
    .EXAMPLE
    Get-CIisHttpRedirect -LocationPath 'ExampleWebsite'
 
    Gets the redirect settings for ExampleWebsite.
 
    .EXAMPLE
    Get-CIisHttpRedirect -LocationPath 'ExampleWebsite/MyVirtualDirectory'
 
    Gets the redirect settings for the MyVirtualDirectory virtual directory under ExampleWebsite.
    #>

    [CmdletBinding()]
    [OutputType([Microsoft.Web.Administration.ConfigurationSection])]
    param(
        # The site's whose HTTP redirect settings will be retrieved.
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [Alias('Path')]
        [String] $VirtualPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $sectionPath = 'system.webServer/httpRedirect'
    Get-CIisConfigurationSection -LocationPath $LocationPath -VirtualPath $VirtualPath -SectionPath $sectionPath
}



function Get-CIisMimeMap
{
    <#
    .SYNOPSIS
    Gets the file extension to MIME type mappings.
 
    .DESCRIPTION
    IIS won't serve static content unless there is an entry for it in the web server or website's MIME map
    configuration. This function will return all the MIME maps for the current server. The objects returned have these
    properties:
 
     * `FileExtension`: the mapping's file extension
     * `MimeType`: the mapping's MIME type
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .LINK
    Set-CIisMimeMap
 
    .EXAMPLE
    Get-CIisMimeMap
 
    Gets all the the file extension to MIME type mappings for the web server.
 
    .EXAMPLE
    Get-CIisMimeMap -FileExtension .htm*
 
    Gets all the file extension to MIME type mappings whose file extension matches the `.htm*` wildcard.
 
    .EXAMPLE
    Get-CIisMimeMap -MimeType 'text/*'
 
    Gets all the file extension to MIME type mappings whose MIME type matches the `text/*` wildcard.
 
    .EXAMPLE
    Get-CIisMimeMap -LocationPath 'DeathStar'
 
    Gets all the file extenstion to MIME type mappings for the `DeathStar` website.
 
    .EXAMPLE
    Get-CIisMimeMap -LocationPath 'DeathStar/ExhaustPort'
 
    Gets all the file extension to MIME type mappings for the `DeathStar`'s `ExhausePort` directory.
    #>

    [CmdletBinding(DefaultParameterSetName='ForWebServer')]
    param(
        # The website whose MIME mappings to return. If not given, returns the web server's MIME map.
        [Parameter(Mandatory, ParameterSetName='ForWebsite', Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [Parameter(ParameterSetName='ForWebsite')]
        [Alias('Path')]
        [String] $VirtualPath,

        # The name of the file extensions to return. Wildcards accepted.
        [String] $FileExtension = '*',

        # The name of the MIME type(s) to return. Wildcards accepted.
        [String] $MimeType = '*'
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $getIisConfigSectionParams = @{ }
    if( $PSCmdlet.ParameterSetName -eq 'ForWebsite' )
    {
        $getIisConfigSectionParams['LocationPath'] = $LocationPath
        $getIisConfigSectionParams['VirtualPath'] = $VirtualPath
    }

    $staticContent =
        Get-CIisConfigurationSection -SectionPath 'system.webServer/staticContent' @getIisConfigSectionParams
    $staticContent.GetCollection() |
        Where-Object { $_['fileExtension'] -like $FileExtension -and $_['mimeType'] -like $MimeType } |
        ForEach-Object {
            $mimeMap = [pscustomobject]@{ FileExtension = $_['fileExtension']; MimeType = $_['mimeType'] }
            $mimeMap.pstypenames.Add('Carbon.Iis.MimeMap')
            $mimeMap | Write-Output
        }
}




function Get-CIisSecurityAuthentication
{
    <#
    .SYNOPSIS
    Gets a site's (and optional sub-directory's) security authentication configuration section.
 
    .DESCRIPTION
    You can get the anonymous, basic, digest, and Windows authentication sections by using the `Anonymous`, `Basic`,
    `Digest`, or `Windows` switches, respectively.
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .OUTPUTS
    Microsoft.Web.Administration.ConfigurationSection.
 
    .EXAMPLE
    Get-CIisSecurityAuthentication -LocationPath 'Peanuts' -Anonymous
 
    Gets the `Peanuts` site's anonymous authentication configuration section.
 
    .EXAMPLE
    Get-CIisSecurityAuthentication -LocationPath 'Peanuts/Doghouse' -Basic
 
    Gets the `Peanuts` site's `Doghouse` sub-directory's basic authentication configuration section.
    #>

    [CmdletBinding()]
    [OutputType([Microsoft.Web.Administration.ConfigurationSection])]
    param(
        # The site where anonymous authentication should be set.
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [Alias('Path')]
        [String] $VirtualPath,

        # Gets a site's (and optional sub-directory's) anonymous authentication configuration section.
        [Parameter(Mandatory, ParameterSetName='anonymousAuthentication')]
        [switch] $Anonymous,

        # Gets a site's (and optional sub-directory's) basic authentication configuration section.
        [Parameter(Mandatory, ParameterSetName='basicAuthentication')]
        [switch] $Basic,

        # Gets a site's (and optional sub-directory's) digest authentication configuration section.
        [Parameter(Mandatory, ParameterSetName='digestAuthentication')]
        [switch] $Digest,

        # Gets a site's (and optional sub-directory's) Windows authentication configuration section.
        [Parameter(Mandatory, ParameterSetName='windowsAuthentication')]
        [switch] $Windows
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $sectionPath = 'system.webServer/security/authentication/{0}' -f $PSCmdlet.ParameterSetName
    Get-CIisConfigurationSection -LocationPath $locationPath -VirtualPath $VirtualPath -SectionPath $sectionPath
}



function Get-CIisServerManager
{
    <#
    .SYNOPSIS
    Returns the current instance of the `Microsoft.Web.Administration.ServerManager` class.
 
    .DESCRIPTION
    The `Get-CIisServerManager` function returns the current instance of `Microsoft.Web.Administration.ServerManager`
    that the Carbon.IIS module is using. After committing changes, the current server manager is destroyed (i.e.
    its `Dispose` method is called). In case the current server manager is destroyed, `Get-CIisServerManager` will
    create a new instance of the `Microsoft.Web.Administration.SiteManager` class.
 
    After using the server manager, if you've made any changes to any objects referenced from it, call the
    `Save-CIisConfiguration` function to save/commit your changes. This will properly destroy the server manager after
    saving/committing your changes.
 
    .EXAMPLE
    $mgr = Get-CIisServerManager
 
    Demonstrates how to get the instance of the `Microsoft.Web.Administration.ServerManager` class the Carbon.IIS
    module is using.
    #>

    [CmdletBinding(DefaultParameterSetName='Get')]
    param(
        # Saves changes to the current server manager, disposes it, creates a new server manager object, and returns
        # that new server manager objet.
        [Parameter(Mandatory, ParameterSetName='Commit')]
        [switch] $Commit,

        # Resets and creates a new server manager. Any unsaved changes are lost.
        [Parameter(Mandatory, ParameterSetName='Reset')]
        [switch] $Reset,

        [Parameter(ParameterSetName='Commit')]
        [TimeSpan] $Timeout = [TimeSpan]::New(0, 0, 10)
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    function New-MessagePrefix
    {
        return "$(([DateTime]::UtcNow.ToString('O'))) ServerManager #$('{0,-10} ' -f $script:serverMgr.GetHashCode())"
    }

    $lastWriteTimeUtc = Get-Item -Path $script:applicationHostPath | Select-Object -ExpandProperty 'LastWriteTimeUtc'

    $msgPrefix = New-MessagePrefix

    if ($lastWriteTimeUtc -gt $script:serverMgrCreatedAt)
    {
        $msg = "$($msgPrefix)Stale $($lastWriteTimeUtc.ToString('O')) > $($script:serverMgrCreatedAt.ToString('O'))"
        Write-Debug $msg
        $Reset = $true
    }

    if ($Commit)
    {
        try
        {
            Write-Debug "$($msgPrefix)CommitChanges()"
            $serverMgr.CommitChanges()

            $startedWaitingAt = [Diagnostics.Stopwatch]::StartNew()
            do
            {
                if ($startedWaitingAt.Elapsed -gt $Timeout)
                {
                    $msg = "Your IIS changes haven't been saved after waiting for $($Timeout) seconds. You may need " +
                           'to wait a little longer or restart IIS.'
                    Write-Warning $msg
                    break
                }

                $appHostInfo = Get-Item -Path $script:applicationHostPath -ErrorAction Ignore
                if( $appHostInfo -and $lastWriteTimeUtc -lt $appHostInfo.LastWriteTimeUtc )
                {
                    Write-Debug " $($startedWaitingAt.Elapsed.TotalSeconds.ToString('0.000'))s Changes committed."
                    $Reset = $true
                    break
                }
                Write-Debug " ! $($startedWaitingAt.Elapsed.TotalSeconds.ToString('0.000'))s Waiting."
                Start-Sleep -Milliseconds 100
            }
            while ($true)
        }
        catch
        {
            Write-Error $_ -ErrorAction $ErrorActionPreference
            return
        }
    }

    if ($Reset)
    {
        Write-Debug "$($msgPrefix)Dispose()"
        $script:serverMgr.Dispose()
    }

    # It's been disposed.
    if( -not $script:serverMgr.ApplicationPoolDefaults )
    {
        $script:serverMgr = [Microsoft.Web.Administration.ServerManager]::New()
        $script:serverMgrCreatedAt = [DateTime]::UtcNow
        $msgPrefix = New-MessagePrefix
        Write-Debug "$($msgPrefix)New()"
    }

    Write-Debug "$($msgPrefix)"
    return $script:serverMgr
}



function Get-CIisVersion
{
    <#
    .SYNOPSIS
    Gets the version of IIS.
 
    .DESCRIPTION
    Reads the version of IIS from the registry, and returns it as a `Major.Minor` formatted string.
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .EXAMPLE
    Get-CIisVersion
 
    Returns `7.0` on Windows 2008, and `7.5` on Windows 7 and Windows 2008 R2.
    #>

    [CmdletBinding()]
    param(
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $props = Get-ItemProperty hklm:\Software\Microsoft\InetStp
    return $props.MajorVersion.ToString() + "." + $props.MinorVersion.ToString()
}




function Get-CIisVirtualDirectory
{
    <#
    .SYNOPSIS
    Gets an IIS application as an `Application` object.
 
    .DESCRIPTION
    Uses the `Microsoft.Web.Administration` API to get an IIS application object. If the application doesn't exist, `$null` is returned.
 
    If you make any changes to any of the objects returned by `Get-CIisApplication`, call `Save-CIisConfiguration` to
    save those changes to IIS.
 
    The objects returned each have a `PhysicalPath` property which is the physical path to the application.
 
    .OUTPUTS
    Microsoft.Web.Administration.Application.
 
    .EXAMPLE
    Get-CIisApplication -SiteName 'DeathStar`
 
    Gets all the applications running under the `DeathStar` website.
 
    .EXAMPLE
    Get-CIisApplication -SiteName 'DeathStar' -VirtualPath '/'
 
    Demonstrates how to get the main application for a website: use `/` as the application name.
 
    .EXAMPLE
    Get-CIisApplication -SiteName 'DeathStar' -VirtualPath 'MainPort/ExhaustPort'
 
    Demonstrates how to get a nested application, i.e. gets the application at `/MainPort/ExhaustPort` under the `DeathStar` website.
    #>

    [CmdletBinding()]
    [OutputType([Microsoft.Web.Administration.Application])]
    param(
        # The site where the application is running.
        [Parameter(Mandatory, Position=0)]
        [String] $LocationPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $siteName, $virtualPath = $LocationPath | Split-CIisLocationPath

    $site = Get-CIisWebsite -Name $siteName
    if( -not $site )
    {
        return
    }

    # Argh. Hard. Should handle /path/APP/path/VDIR.
    $site.Applications |
        Select-Object -ExpandProperty 'VirtualDirectories' |
        Where-Object {
            if ($PSBoundParameters.ContainsKey('VirtualPath'))
            {
                return ($_.Path -like $VirtualPath)
            }
            return $true
        }
}




function Get-CIisWebsite
{
    <#
    .SYNOPSIS
    Returns all the websites installed on the local computer, a specific website, or the website defaults.
 
    .DESCRIPTION
    The `Get-CIisWebsite` function returns all websites installed on the local computer, or nothing if no websites are
    installed. To get a specific website, pass its name to the `Name` parameter. If a website with that name exists, it
    is returned as a `Microsoft.Web.Administration.Site` object, from the Microsoft.Web.Administration API. If the
    website doesn't exist, the function will write an error and return nothing.
 
    You can get the website defaults settings by using the `Defaults` switch. If `Defaults` is true, then the `Name`
    parameter is ignored.
 
    If you make any changes to any of the return objects, use `Save-CIisConfiguration` to save your changes.
 
    .OUTPUTS
    Microsoft.Web.Administration.Site.
 
    .LINK
    http://msdn.microsoft.com/en-us/library/microsoft.web.administration.site.aspx
 
    .EXAMPLE
    Get-CIisWebsite
 
    Returns all installed websites.
 
    .EXAMPLE
    Get-CIisWebsite -Name 'WebsiteName'
 
    Returns the details for the site named `WebsiteName`.
 
    .EXAMPLE
    Get-CIisWebsite -Name 'fubar' -ErrorAction Ignore
 
    Demonstrates how to ignore that a website doesn't exist by setting the `ErrorAction` parameter to `Ignore`.
 
    .EXAMPLE
    Get-CIisWebsite -Defaults
 
    Demonstrates how to get IIS website defaults settings.
    #>

    [CmdletBinding()]
    [OutputType([Microsoft.Web.Administration.Site])]
    param(
        # The name of the site to get. Wildcards supported.
        [String] $Name,

        # Instead of getting all websites or a specifid website, return website defaults settings. If true, the `Name`
        # parameter is ignored.
        [switch] $Defaults
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $WhatIfPreference = $false

    if( $Defaults )
    {
        return (Get-CIisServerManager).SiteDefaults
    }

    $sites = @()
    $mgr = Get-CIisServerManager
    $mgr.Sites |
        Where-Object {
            if( $Name )
            {
                return $_.Name -like $Name
            }

            return $true
        } |
        Tee-Object -Variable 'sites' |
        Write-Output

    if ($Name -and -not [wildcardpattern]::ContainsWildcardCharacters($Name) -and -not $sites)
    {
        Write-Error -Message "Website ""$($Name)"" does not exist." -ErrorAction $ErrorActionPreference
    }
}




function Install-CIisApplication
{
    <#
    .SYNOPSIS
    Creates a new application under a website.
 
    .DESCRIPTION
    Creates a new application at `VirtualPath` under website `SiteName` running the code found on the file system under
    `PhysicalPath`, i.e. if SiteName is is `example.com`, the application is accessible at `example.com/VirtualPath`.
    If an application already exists at that path, it is removed first. The application can run under a custom
    application pool using the optional `AppPoolName` parameter. If no app pool is specified, the application runs
    under the same app pool as the website it runs under.
 
    Beginning with Carbon 2.0, returns a `Microsoft.Web.Administration.Application` object for the new application if
    one is created or modified.
 
    Beginning with Carbon 2.0, if no app pool name is given, existing application's are updated to use `DefaultAppPool`.
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .EXAMPLE
    Install-CIisApplication -SiteName Peanuts -VirtualPath CharlieBrown -PhysicalPath C:\Path\To\CharlieBrown -AppPoolName CharlieBrownPool
 
    Creates an application at `Peanuts/CharlieBrown` which runs from `Path/To/CharlieBrown`. The application runs under
    the `CharlieBrownPool`.
 
    .EXAMPLE
    Install-CIisApplication -SiteName Peanuts -VirtualPath Snoopy -PhysicalPath C:\Path\To\Snoopy
 
    Create an application at Peanuts/Snoopy, which runs from C:\Path\To\Snoopy. It uses the same application as the
    Peanuts website.
    #>

    [CmdletBinding()]
    [OutputType([Microsoft.Web.Administration.Application])]
    param(
        # The site where the application should be created.
        [Parameter(Mandatory)]
        [String] $SiteName,

        # The path of the application.
        [Parameter(Mandatory)]
        [Alias('Name')]
        [String] $VirtualPath,

        # The path to the application.
        [Parameter(Mandatory)]
        [Alias('Path')]
        [String] $PhysicalPath,

        # The app pool for the application. Default is `DefaultAppPool`.
        [String] $AppPoolName,

        # Returns IIS application object. This switch is new in Carbon 2.0.
        [Switch] $PassThru
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $site = Get-CIisWebsite -Name $SiteName
    if( -not $site )
    {
        return
    }

    $iisAppPath = Join-CIisPath -Path $SiteName, $VirtualPath

    $PhysicalPath = Resolve-CFullPath -Path $PhysicalPath
    if( -not (Test-Path $PhysicalPath -PathType Container) )
    {
        Write-Verbose ('IIS://{0}: creating physical path {1}' -f $iisAppPath,$PhysicalPath)
        $null = New-Item $PhysicalPath -ItemType Directory
    }

    $apps = $site.GetCollection()

    $msgPrefix = "IIS website ""$($SiteName)"": "
    $VirtualPath = $VirtualPath | ConvertTo-CIisVirtualPath
    $app = Get-CIisApplication -SiteName $SiteName -VirtualPath $VirtualPath
    $modified = $false
    if( -not $app )
    {
        Write-Information "$($msgPrefix)creating application ""$($VirtualPath)""."
        $app = $apps.CreateElement('application')
        $app['path'] = $VirtualPath
        $apps.Add( $app ) | Out-Null
        $modified = $true
    }

    $msgPrefix = "$($msgPrefix)application ""$($VirtualPath)"": "

    if( $AppPoolName -and $app['applicationPool'] -ne $AppPoolName )
    {
        Write-Information "$($msgPrefix)Application Pool $($app['applicationPool']) -> $($AppPoolName)"
        $app['applicationPool'] = $AppPoolName
        $modified = $true
    }

    $vdir = $null
    if( $app | Get-Member 'VirtualDirectories' )
    {
        $vdir = $app.VirtualDirectories | Where-Object 'Path' -EQ '/'
    }

    if( -not $vdir )
    {
        Write-Information "$($msgPrefix)Virtual Directory $('') -> /"
        $vdirs = $app.GetCollection()
        $vdir = $vdirs.CreateElement('virtualDirectory')
        $vdir['path'] = '/'
        $vdirs.Add( $vdir ) | Out-Null
        $modified = $true
    }

    if( $vdir['physicalPath'] -ne $PhysicalPath )
    {
        Write-Information "$($msgPrefix)Physical Path $($vdir['physicalPath']) -> $($PhysicalPath)"
        $vdir['physicalPath'] = $PhysicalPath
        $modified = $true
    }

    if( $modified )
    {
        Save-CIisConfiguration
    }

    if( $PassThru )
    {
        return Get-CIisApplication -SiteName $SiteName -VirtualPath $VirtualPath
    }
}


function Install-CIisAppPool
{
    <#
    .SYNOPSIS
    Creates or updates an IIS application pool.
 
    .DESCRIPTION
    The `Install-CIisAppPool` function creates or updates an IIS application pool. Pass the name of the application pool
    to the `Name` parameter. If that application pool doesn't exist, it is created. If it does exist, its configuration
    is updated to match the values of the arguments passed. If you don't pass an argument, that argument's setting is
    deleted and reset to its default value. You always get an application pool with the exact same configuration, even
    if someone or something has changed an application pool's configuration in some other way.
 
    To configure the application pool's process model (i.e. the application pool's account/identity, idle timeout,
    etc.), use the `Set-CIisAppPoolProcessModel` function.
 
    To configure the application pool's periodic restart settings, use the `Set-CIisAppPoolPeriodicRestart`
    function.
 
    To configure the application pool's periodic restart settings, use the `Set-CIisAppPoolPeriodicRestart`
    can't delete an app pool if there are any websites using it, that's why.)
 
    To configure the application pool's CPU settings, use the `Set-CIisAppPoolCpu` function.
 
    .EXAMPLE
    Install-CIisAppPool -Name Cyberdyne
 
    Demonstrates how to use Install-CIisAppPool to create/update an application pool with reasonable defaults. In this
    example, an application pool named "Cyberdyne" is created that is 64-bit, uses .NET 4.0, and an integrated pipeline.
 
    .EXAMPLE
    Install-CIisAppPool -Name Cyberdyne -Enable32BitAppOnWin64 $true -ManagedPipelineMode Classic -ManagedRuntimeVersion 'v2.0'
 
    Demonstrates how to customize an application pool away from its default settings. In this example, the "Cyberdyne"
    application pool is created that is 32-bit, uses .NET 2.0, and a classic pipeline.
    #>

    [OutputType([Microsoft.Web.Administration.ApplicationPool])]
    [CmdletBinding(DefaultParameterSetName='New')]
    param(
        # The app pool's name.
        [Parameter(Mandatory)]
        [String] $Name,

        # Sets the IIS application pool's `autoStart` setting.
        [Parameter(ParameterSetName='New')]
        [bool] $AutoStart,

        # Sets the IIS application pool's `CLRConfigFile` setting.
        [Parameter(ParameterSetName='New')]
        [String] $CLRConfigFile,

        # Sets the IIS application pool's `enable32BitAppOnWin64` setting.
        [Parameter(ParameterSetName='New')]
        [bool] $Enable32BitAppOnWin64,

        # Sets the IIS application pool's `enableConfigurationOverride` setting.
        [Parameter(ParameterSetName='New')]
        [bool] $EnableConfigurationOverride,

        # Sets the IIS application pool's `managedPipelineMode` setting.
        [ManagedPipelineMode] $ManagedPipelineMode,

        # Sets the IIS application pool's `managedRuntimeLoader` setting.
        [Parameter(ParameterSetName='New')]
        [String] $ManagedRuntimeLoader,

        # Sets the IIS application pool's `managedRuntimeVersion` setting.
        [String] $ManagedRuntimeVersion,

        # Sets the IIS application pool's `passAnonymousToken` setting.
        [Parameter(ParameterSetName='New')]
        [bool] $PassAnonymousToken,

        # Sets the IIS application pool's `queueLength` setting.
        [Parameter(ParameterSetName='New')]
        [UInt32] $QueueLength,

        # Sets the IIS application pool's `startMode` setting.
        [Parameter(ParameterSetName='New')]
        [StartMode] $StartMode,

        # Return an object representing the app pool.
        [switch] $PassThru,

        #Idle Timeout value in minutes. Default is 0.
        [Parameter(ParameterSetName='Deprecated')]
        [ValidateScript({$_ -gt 0})]
        [int] $IdleTimeout = 0,

        # Run the app pool under the given local service account. Valid values are `NetworkService`, `LocalService`,
        # and `LocalSystem`. The default is `ApplicationPoolIdentity`, which causes IIS to create a custom local user
        # account for the app pool's identity. The default is `ApplicationPoolIdentity`.
        [Parameter(ParameterSetName='Deprecated')]
        [ValidateSet('NetworkService', 'LocalService', 'LocalSystem')]
        [String] $ServiceAccount,

        # The credential to use to run the app pool.
        #
        # The `Credential` parameter is new in Carbon 2.0.
        [Parameter(ParameterSetName='Deprecated', Mandatory)]
        [pscredential] $Credential,

        # Enable 32-bit applications.
        [Parameter(ParameterSetName='Deprecated')]
        [switch] $Enable32BitApps,

        # Use the classic pipeline mode, i.e. don't use an integrated pipeline.
        [Parameter(ParameterSetName='Deprecated')]
        [switch] $ClassicPipelineMode
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($PSCmdlet.ParameterSetName -eq 'Deprecated')
    {
        $functionName = $PSCmdlet.MyInvocation.MyCommand.Name

        $installArgs = @{
            'ManagedPipelineMode' = [ManagedPipelineMode]::Integrated
            'ManagedRuntimeVersion' = 'v4.0'
        }

        $installArgs['Enable32BitAppOnWin64'] = $Enable32BitApps.IsPresent

        if ($ClassicPipelineMode)
        {
            "The ""$($functionName)"" function's ""ClassicPipelineMode"" switch is deprecated. Use the " +
            '"ManagedPipelineMode" parameter instead.' | Write-CIIsWarningOnce

            $installArgs['ManagedPipelineMode'] = [ManagedPipelineMode]::Classic
        }

        if ($ManagedRuntimeVersion)
        {
            $installArgs['ManagedRuntimeVersion'] = $ManagedRuntimeVersion
        }

        if ($PassThru)
        {
            $installArgs['PassThru'] = $PassThru
        }

        Install-CIisAppPool -Name $Name @installArgs

        $setProcessModelArgs = @{}

        if ($Credential)
        {
            "The ""$($functionName)"" function's ""Credential"" parameter is deprecated. Use the " +
            '"Set-CIisAppPoolProcessModel" function and its "IdentityType", "UserName", and "Password" parameters ' +
            'instead.' | Write-CIIsWarningOnce

            $setProcessModelArgs['IdentityType'] = [ProcessModelIdentityType]::SpecificUser
            $setProcessModelArgs['UserName'] = $Credential.UserName
            $setProcessModelArgs['Password'] = $Credential.Password
        }
        elseif ($ServiceAccount)
        {
            "The $($functionName) function's ""ServiceAccount"" parameter is deprecated. Use the " +
            '"Set-CIisAppPoolProcessModel" function and its "IdentityType" parameter instead.' | Write-CIIsWarningOnce

            $setProcessModelArgs['IdentityType'] = $ServiceAccount
        }

        if ($IdleTimeout)
        {
            "The $($functionName) function's ""IdleTimeout"" parameter is deprecated. Use the " +
            '"Set-CIisAppPoolProcessModel" function and its "IdleTimeout" parameter instead.' | Write-CIIsWarningOnce
            $setProcessModelArgs['IdleTimeout'] = $IdleTimeout
        }

        if ($setProcessModelArgs.Count -eq 0)
        {
            return
        }

        Set-CIisAppPoolProcessModel -AppPoolName $Name @setProcessModelArgs
        return
    }

    if( -not (Test-CIisAppPool -Name $Name) )
    {
        Write-Information "Creating IIS Application Pool ""$($Name)""."
        $mgr = Get-CIisServerManager
        $mgr.ApplicationPools.Add($Name) | Out-Null
        Save-CIisConfiguration
    }

    $setArgs = @{}
    foreach( $parameterName in (Get-Command -Name 'Set-CIisAppPool').Parameters.Keys )
    {
        if( -not $PSBoundParameters.ContainsKey($parameterName) )
        {
            continue
        }
        $setArgs[$parameterName] = $PSBoundParameters[$parameterName]
    }
    Set-CIisAppPool @setArgs -Reset

    Start-CIisAppPool -Name $Name

    if( $PassThru )
    {
        return (Get-CIisAppPool -Name $Name)
    }
}




function Install-CIisVirtualDirectory
{
    <#
    .SYNOPSIS
    Installs a virtual directory.
 
    .DESCRIPTION
    The `Install-CIisVirtualDirectory` function creates a virtual directory under website `SiteName` at `VirtualPath`,
    serving files out of `PhysicalPath`. If a virtual directory at `VirtualPath` already exists, it is updated in
    place.
 
    .EXAMPLE
    Install-CIisVirtualDirectory -SiteName 'Peanuts' -VirtualPath 'DogHouse' -PhysicalPath C:\Peanuts\Doghouse
 
    Creates a `/DogHouse` virtual directory, which serves files from the C:\Peanuts\Doghouse directory. If the Peanuts
    website responds to hostname `peanuts.com`, the virtual directory is accessible at `peanuts.com/DogHouse`.
 
    .EXAMPLE
    Install-CIisVirtualDirectory -SiteName 'Peanuts' -VirtualPath 'Brown/Snoopy/DogHouse' -PhysicalPath C:\Peanuts\DogHouse
 
    Creates a DogHouse virtual directory under the `Peanuts` website at `/Brown/Snoopy/DogHouse` serving files out of
    the `C:\Peanuts\DogHouse` directory. If the Peanuts website responds to hostname `peanuts.com`, the virtual
    directory is accessible at `peanuts.com/Brown/Snoopy/DogHouse`.
    #>

    [CmdletBinding()]
    param(
        # The site where the virtual directory should be created.
        [Parameter(Mandatory)]
        [String] $SiteName,

        # The virtual path of the virtual directory to install, i.e. the path in the URL to this directory. If creating
        # under an applicaton, this should be the path in the URL *after* the path in the URL to the application.
        [Parameter(Mandatory)]
        [Alias('Name')]
        [String] $VirtualPath,

        # The path of the application under which the virtual directory should get created. The default is to create
        # the virtual directory under website's root application, `/`.
        [String] $ApplicationPath = '/',

        # The file system path to the virtual directory.
        [Parameter(Mandatory)]
        [Alias('Path')]
        [String] $PhysicalPath,

        # Deletes the virtual directory before installation, if it exists.
        #
        # *Does not* delete custom configuration for the virtual directory, just the virtual directory. If you've
        # customized the location of the virtual directory, those customizations will remain in place.
        #
        # The `Force` switch is new in Carbon 2.0.
        [switch] $Force
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $site = Get-CIisWebsite -Name $SiteName
    if( -not $site )
    {
        return
    }

    $ApplicationPath = $ApplicationPath | ConvertTo-CIisVirtualPath

    [Microsoft.Web.Administration.Application] $destinationApp =
        $site.Applications | Where-Object 'Path' -EQ $ApplicationPath
    if( -not $destinationApp )
    {
        Write-Error ("The ""$($SiteName)"" website's ""$($ApplicationPath)"" application does not exist.")
        return
    }

    $PhysicalPath = Resolve-CFullPath -Path $PhysicalPath
    $VirtualPath = $VirtualPath | ConvertTo-CIisVirtualPath

    $vPathMsg = Join-CIisPath -Path $ApplicationPath, $VirtualPath

    $vdir = $destinationApp.VirtualDirectories | Where-Object 'Path' -EQ $VirtualPath
    if( $Force -and $vdir )
    {
        Write-IisVerbose $SiteName -VirtualPath $vPathMsg 'REMOVE' '' ''
        $destinationApp.VirtualDirectories.Remove($vdir)
        Save-CIisConfiguration
        $vdir = $null

        $site = Get-CIisWebsite -Name $SiteName
        $destinationApp = $site.Applications | Where-Object 'Path' -EQ '/'
    }

    $modified = $false


    if( -not $vdir )
    {
        [Microsoft.Web.Administration.ConfigurationElementCollection]$vdirs = $destinationApp.GetCollection()
        $vdir = $vdirs.CreateElement('virtualDirectory')
        Write-IisVerbose $SiteName -VirtualPath $vPathMsg 'virtualPath' '' $VirtualPath
        $vdir['path'] = $VirtualPath
        [void]$vdirs.Add( $vdir )
        $modified = $true
    }

    if( $vdir['physicalPath'] -ne $PhysicalPath )
    {
        Write-IisVerbose $SiteName -VirtualPath $vPathMsg 'physicalPath' $vdir['physicalPath'] $PhysicalPath
        $vdir['physicalPath'] = $PhysicalPath
        $modified = $true
    }

    if( $modified )
    {
        Save-CIIsConfiguration
    }
}




function Install-CIisWebsite
{
    <#
    .SYNOPSIS
    Installs a website.
 
    .DESCRIPTION
    `Install-CIisWebsite` installs an IIS website. Anonymous authentication is enabled, and the anonymous user is set to
    the website's application pool identity. Before Carbon 2.0, if a website already existed, it was deleted and
    re-created. Beginning with Carbon 2.0, existing websites are modified in place.
 
    If you don't set the website's app pool, IIS will pick one for you (usually `DefaultAppPool`), an
     `Install-CIisWebsite` will never manage the app pool for you (i.e. if someone changes it manually, this function
     won't set it back to the default). We recommend always supplying an app pool name, even if it is `DefaultAppPool`.
 
    By default, the site listens on (i.e. is bound to) all IP addresses on port 80 (binding `http/*:80:`). Set custom
    bindings with the `Bindings` argument. Multiple bindings are allowed. Each binding must be in this format (in BNF):
 
        <PROTOCOL> '/' <IP_ADDRESS> ':' <PORT> ':' [ <HOSTNAME> ]
 
     * `PROTOCOL` is one of `http` or `https`.
     * `IP_ADDRESS` is a literal IP address, or `*` for all of the computer's IP addresses. This function does not
     validate if `IPADDRESS` is actually in use on the computer.
     * `PORT` is the port to listen on.
     * `HOSTNAME` is the website's hostname, for name-based hosting. If no hostname is being used, leave off the
     `HOSTNAME` part.
 
    Valid bindings are:
 
     * http/*:80:
     * https/10.2.3.4:443:
     * http/*:80:example.com
 
     ## Troubleshooting
 
     In some situations, when you add a website to an application pool that another website/application is part of, the
     new website will fail to load in a browser with a 500 error saying `Failed to map the path '/'.`. We've been unable
     to track down the root cause. The solution is to recycle the app pool, e.g.
     `(Get-CIisAppPool -Name 'AppPoolName').Recycle()`.
 
    .LINK
    Get-CIisWebsite
 
    .LINK
    Uninstall-CIisWebsite
 
    .EXAMPLE
    Install-CIisWebsite -Name 'Peanuts' -PhysicalPath C:\Peanuts.com
 
    Creates a website named `Peanuts` serving files out of the `C:\Peanuts.com` directory. The website listens on all
    the computer's IP addresses on port 80.
 
    .EXAMPLE
    Install-CIisWebsite -Name 'Peanuts' -PhysicalPath C:\Peanuts.com -Binding 'http/*:80:peanuts.com'
 
    Creates a website named `Peanuts` which uses name-based hosting to respond to all requests to any of the machine's
    IP addresses for the `peanuts.com` domain.
 
    .EXAMPLE
    Install-CIisWebsite -Name 'Peanuts' -PhysicalPath C:\Peanuts.com -AppPoolName 'PeanutsAppPool'
 
    Creates a website named `Peanuts` that runs under the `PeanutsAppPool` app pool
    #>

    [CmdletBinding()]
    [OutputType([Microsoft.Web.Administration.Site])]
    param(
        # The name of the website.
        [Parameter(Mandatory, Position=0)]
        [String] $Name,

        # The physical path (i.e. on the file system) to the website. If it doesn't exist, it will be created for you.
        [Parameter(Mandatory, Position=1)]
        [Alias('Path')]
        [String] $PhysicalPath,

        # The site's network bindings. Default is `http/*:80:`. Bindings should be specified in
        # `protocol/IPAddress:Port:Hostname` format.
        #
        # * Protocol should be http or https.
        # * IPAddress can be a literal IP address or `*`, which means all of the computer's IP addresses. This
        # function does not validate if `IPAddress` is actually in use on this computer.
        # * Leave hostname blank for non-named websites.
        [Parameter(Position=2)]
        [Alias('Bindings')]
        [String[]] $Binding = @('http/*:80:'),

        # The name of the app pool under which the website runs. The app pool must exist. If not provided, IIS picks
        # one for you. No whammy, no whammy! It is recommended that you create an app pool for each website. That's
        # what the IIS Manager does.
        [String] $AppPoolName,

        # Sets the IIS website's `id` setting.
        [Alias('SiteID')]
        [UInt32] $ID,

        # Sets the IIS website's `serverAutoStart` setting.
        [bool] $ServerAutoStart,

        # Return a `Microsoft.Web.Administration.Site` object for the website.
        [switch] $PassThru,

        [TimeSpan] $Timeout = [TimeSpan]::New(0, 0, 30)
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $bindingRegex = '^(?<Protocol>https?):?//?(?<IPAddress>\*|[\d\.]+):(?<Port>\d+):?(?<HostName>.*)$'

    filter ConvertTo-Binding
    {
        param(
            [Parameter(ValueFromPipeline=$true,Mandatory=$true)]
            [string]
            $InputObject
        )

        Set-StrictMode -Version 'Latest'

        $InputObject -match $bindingRegex | Out-Null
        [pscustomobject]@{
                'Protocol' = $Matches['Protocol'];
                'IPAddress' = $Matches['IPAddress'];
                'Port' = $Matches['Port'];
                'HostName' = $Matches['HostName'];
            } |
            Add-Member -MemberType ScriptProperty `
                       -Name 'BindingInformation' `
                       -Value { '{0}:{1}:{2}' -f $this.IPAddress,$this.Port,$this.HostName } `
                       -PassThru
    }

    $PhysicalPath = Resolve-CFullPath -Path $PhysicalPath
    if( -not (Test-Path $PhysicalPath -PathType Container) )
    {
        New-Item $PhysicalPath -ItemType Directory | Out-String | Write-Verbose
    }

    $invalidBindings = $Binding | Where-Object { $_ -notmatch $bindingRegex }
    if( $invalidBindings )
    {
        $invalidBindings = $invalidBindings -join "`n`t"
        $errorMsg = 'The following bindings are invalid. The correct format is "protocol/IPAddress:Port:Hostname". ' +
                    'Protocol and IP address must be separted by a single slash, not "://". IP address can be "*" ' +
                    'for all IP addresses. Hostname is optional. If hostname is not provided, the binding must end ' +
                    "with a colon.$([Environment]::NewLine)$($invalidBindings)"
        Write-Error $errorMsg
        return
    }

    [Microsoft.Web.Administration.Site] $site = $null
    $modified = $false
    if( -not (Test-CIisWebsite -Name $Name) )
    {
        $firstBinding = $Binding | Select-Object -First 1 | ConvertTo-Binding
        $mgr = Get-CIisServerManager
        $msg = "Creating IIS website ""$($Name)"" bound to " +
               "$($firstBinding.Protocol)/$($firstBinding.BindingInformation)."
        Write-Information $msg
        $site = $mgr.Sites.Add( $Name, $firstBinding.Protocol, $firstBinding.BindingInformation, $PhysicalPath )
        Save-CIisConfiguration
    }

    $site = Get-CIisWebsite -Name $Name
    if (-not $site)
    {
        return
    }

    $expectedBindings = [Collections.Generic.Hashset[String]]::New()
    $Binding |
        ConvertTo-Binding |
        ForEach-Object { [void]$expectedBindings.Add( ('{0}/{1}' -f $_.Protocol,$_.BindingInformation) ) }

    $bindingsToRemove =
        $site.Bindings |
        Where-Object { -not $expectedBindings.Contains(  ('{0}/{1}' -f $_.Protocol,$_.BindingInformation ) ) }

    $bindingMsgs = [Collections.Generic.List[String]]::New()

    foreach( $bindingToRemove in $bindingsToRemove )
    {
        $bindingMsgs.Add("- $($bindingToRemove.Protocol)/$($bindingToRemove.BindingInformation)")
        $site.Bindings.Remove( $bindingToRemove )
        $modified = $true
    }

    $existingBindings = [Collections.Generic.Hashset[String]]::New()
    $site.Bindings | ForEach-Object { [void]$existingBindings.Add( ('{0}/{1}' -f $_.Protocol,$_.BindingInformation) ) }

    $bindingsToAdd =
        $Binding |
        ConvertTo-Binding |
        Where-Object { -not $existingBindings.Contains(  ('{0}/{1}' -f $_.Protocol,$_.BindingInformation ) ) }

    foreach( $bindingToAdd in $bindingsToAdd )
    {
        $bindingMsgs.Add("+ $($bindingToAdd.Protocol)/$($bindingToAdd.BindingInformation)")
        $site.Bindings.Add( $bindingToAdd.BindingInformation, $bindingToAdd.Protocol ) | Out-Null
        $modified = $true
    }

    $prefix = "Configuring ""$($Name)"" IIS website's bindings: "
    foreach( $bindingMsg in $bindingMsgs )
    {
        Write-Information "$($prefix)$($bindingMsg)"
        $prefix = ' ' * $prefix.Length
    }

    [Microsoft.Web.Administration.Application] $rootApp = $null
    if( $site.Applications.Count -eq 0 )
    {
        Write-Information "Adding ""$($Name)"" IIS website's default application."
        $rootApp = $site.Applications.Add('/', $PhysicalPath)
        $modified = $true
    }
    else
    {
        $rootApp = $site.Applications | Where-Object 'Path' -EQ '/'
    }

    if( $site.PhysicalPath -ne $PhysicalPath )
    {
        Write-Information "Setting ""$($Name)"" IIS website's physical path to ""$($PhysicalPath)""."
        [Microsoft.Web.Administration.VirtualDirectory] $vdir =
            $rootApp.VirtualDirectories | Where-Object 'Path' -EQ '/'
        $vdir.PhysicalPath = $PhysicalPath
        $modified = $true
    }

    if( $AppPoolName )
    {
        if( $rootApp.ApplicationPoolName -ne $AppPoolName )
        {
            Write-Information "Setting ""$($Name)"" IIS website's application pool to ""$($AppPoolName)""."
            $rootApp.ApplicationPoolName = $AppPoolName
            $modified = $true
        }
    }

    if( $modified )
    {
        Save-CIisConfiguration
    }

    $site = Get-CIisWebsite -Name $Name
    # Can't ever remove a site ID, only change it, so set the ID to the website's current value.
    $setArgs = @{
        'ID' = $site.ID;
    }
    foreach( $parameterName in (Get-Command -Name 'Set-CIisWebsite').Parameters.Keys )
    {
        if( -not $PSBoundParameters.ContainsKey($parameterName) )
        {
            continue
        }
        $setArgs[$parameterName] = $PSBoundParameters[$parameterName]
    }
    Set-CIisWebsite @setArgs -Reset

    # Now, wait until site is actually running. Do *not* use Start-CIisWebsite. If there are any HTTPS bindings that
    # don't have an assigned HTTPS certificate the start will fail.
    $timer = [Diagnostics.Stopwatch]::StartNew()
    $website = $null
    do
    {
        $website = Get-CIisWebsite -Name $Name
        if($website.State -ne 'Unknown')
        {
            break
        }

        Start-Sleep -Milliseconds 100
    }
    while ($timer.Elapsed -lt $Timeout)

    if( $PassThru )
    {
        return $website
    }
}




function Invoke-SetConfigurationAttribute
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [ConfigurationElement] $ConfigurationElement,

        [Parameter(Mandatory)]
        [Alias('PSCmdlet')]
        [PSCmdlet] $SourceCmdlet,

        [Parameter(Mandatory)]
        [String] $Target,

        [hashtable] $Attribute = @{},

        [String[]] $Exclude = @(),

        [switch] $Reset
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $invokation = $SourceCmdlet.MyInvocation
    $cmd = $invokation.MyCommand

    $parameterSet = $cmd.ParameterSets | Where-Object 'Name' -EQ $SourceCmdlet.ParameterSetName
    if( -not $parameterSet )
    {
        $parameterSet = $cmd.ParameterSets | Where-Object 'IsDefault' -EQ $true
    }

    $cmdParameters = $invokation.BoundParameters

    foreach( $attrName in ($ConfigurationElement.Attributes | Select-Object -ExpandProperty 'Name') )
    {
        if( -not $cmdParameters.ContainsKey($attrName) -or $attrName -in $Exclude )
        {
            continue
        }

        $Attribute[$attrName] = $cmdParameters[$attrName]
    }

    Set-CIisConfigurationAttribute -ConfigurationElement $ConfigurationElement `
                                   -Attribute $Attribute `
                                   -Target $Target `
                                   -Exclude $Exclude `
                                   -Reset:$Reset
}



function Join-CIisPath
{
    <#
    .SYNOPSIS
    Combines path segments into an IIS virtual/location path.
 
    .DESCRIPTION
    The `Join-CIisPath` function takes path segments and combines them into a single virtual/location path. You can pass
    the path segments as a list to the `Path` parameter, as multipe unnamed parameters, or pipe them in. The final path
    is normalized by removing extra slashes, relative path signifiers (e.g. `.` and `..`), and converting backward
    slashes to forward slashes.
 
    .EXAMPLE
    Join-CIisPath -Path 'SiteName', 'Virtual', 'Path'
 
    Demonstrates how to join paths together by passing an array of paths to the `Path` parameter.
 
    .EXAMPLE
    Join-CIisPath -Path 'SiteName' 'Virtual' 'Path'
 
    Demonstrates how to join paths together by passing each path as unnamed parameters.
 
    .EXAMPLE
    'SiteName', 'Virtual', 'Path' | Join-CIisPath
 
    Demonstrates how to join paths together by piping each path into the function.
 
    .EXAMPLE
    'SiteName', 'Virtual', 'Path' | Join-CIisPath -NoLeadingSlash
 
    Demonstrates how to omit the leading slash on the returned virtual/location path by using the `NoLeadingSlash`
    switch.
    #>

    [CmdletBinding()]
    param(
        # The parent path.
        [Parameter(Mandatory, Position=0, ValueFromPipeline)]
        [AllowEmptyString()]
        [AllowNull()]
        [String[]]$Path,

        # All remaining arguments are passed to this parameter. Each path passed are also appended to the path. This
        # parameter exists to allow you to call `Join-CIisPath` with each path to join as a positional parameter, e.g.
        # `Join-Path -Path 'one' 'two' 'three' 'four' 'five' 'six'`.
        [Parameter(Position=1, ValueFromRemainingArguments)]
        [String[]] $ChildPath,

        # If set, the returned virtual path will have a leading slash. The default behavior is for the returned path
        # not to have a leading slash.
        [switch] $LeadingSlash
    )

    begin
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        $segments = [Collections.Generic.List[String]]::New()
    }

    process
    {
        if (-not $Path)
        {
            return
        }

        foreach ($pathItem in $Path)
        {
            if (-not $pathItem)
            {
                continue
            }

            $segments.Add($pathItem)
        }
    }

    end
    {
        $fullPath = (& {
                if ($segments.Count)
                {
                    $segments | Write-Output
                }

                if ($ChildPath)
                {
                    $ChildPath | Where-Object { $_ } | Write-Output
                }
        }) -join '/'
        return $fullPath | ConvertTo-CIisVirtualPath -NoLeadingSlash:(-not $LeadingSlash)
    }
}



function Join-CIisVirtualPath
{
    <#
    .SYNOPSIS
    OBSOLETE. Use `Join-CIisPath` instead.
 
    .DESCRIPTION
    OBSOLETE. Use `Join-CIisPath` instead.
    #>

    [CmdletBinding()]
    param(
        # The parent path.
        [Parameter(Mandatory, Position=0)]
        [AllowEmptyString()]
        [AllowNull()]
        [String]$Path,

        #
        [Parameter(Position=1)]
        [String[]] $ChildPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $msg = 'The "Join-CIisVirtualPath" function is OBSOLETE and will be removed in the next major version of ' +
           'Carbon.IIS. Please use the `Join-CIisPath` function instead.'
    Write-CIisWarningOnce -Message $msg

    if( $ChildPath )
    {
        $Path = Join-Path -Path $Path -ChildPath $ChildPath
    }
    $Path.Replace('\', '/').Trim('/')
}



function Lock-CIisConfigurationSection
{
    <#
    .SYNOPSIS
    Locks an IIS configuration section so that it can't be modified/overridden by individual websites.
 
    .DESCRIPTION
    Locks configuration sections globally so they can't be modified by individual websites. For a list of section paths, run
 
        C:\Windows\System32\inetsrv\appcmd.exe lock config /section:?
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .EXAMPLE
    Lock-CIisConfigurationSection -SectionPath 'system.webServer/security/authentication/basicAuthentication'
 
    Locks the `basicAuthentication` configuration so that sites can't override/modify those settings.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The path to the section to lock. For a list of sections, run
        #
        # C:\Windows\System32\inetsrv\appcmd.exe unlock config /section:?
        [Parameter(Mandatory)]
        [String[]] $SectionPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    foreach( $sectionPathItem in $SectionPath )
    {
        $section = Get-CIisConfigurationSection -SectionPath $sectionPathItem
        $section.OverrideMode = 'Deny'
        Save-CIisConfiguration -Target $sectionPathItem -Action 'Locking IIS Configuration Section'
    }
}




function Remove-CIisConfigurationAttribute
{
    <#
    .SYNOPSIS
    Removes an attribute from a configuration section.
 
    .DESCRIPTION
    The `Remove-CIisConfigurationAttribute` function removes/deletes an attribute from a website's configuration in the
    IIS application host configuration file. Pass the website name to the `SiteName` parameter, the path to the
    configuration section from which to remove the attribute to the `SectionPath` parameter, and the name of the
    attribute to remove/delete to the `Name` parameter. The function deletes that attribute. If the attribute doesn't
    exist, nothing happens.
 
    To delete more than one attribute on a specific element at a ttime, either pass multiple names to the `Name`
    parameter, or pipe the list of attributes to `Remove-CIisConfigurationAttribute`.
 
    To delete/remove an attribute from the configuration of an application/virtual directory under a website, pass the
    application/virtual diretory's name/path to the `VirtualPath` parameter.
 
    .EXAMPLE
    Remove-CIisConfigurationAttribute -SiteName 'MySite' -SectionPath 'system.webServer/security/authentication/anonymousAuthentication' -Name 'userName'
 
    Demonstrates how to delete/remove the attribute from a website's configuration. In this example, the `userName`
    attribute on the `system.webServer/security/authentication/anonymousAuthentication` configuration is deleted.
 
    .EXAMPLE
    Remove-CIisConfigurationAttribute -SiteName 'MySite' -VirtualPath 'myapp/appdir' -SectionPath 'system.webServer/security/authentication/anonymousAuthentication' -Name 'userName'
 
    Demonstrates how to delete/remove the attribute from a website's path/application/virtual directory configuration.
    In this example, the `userName` attribute on the `system.webServer/security/authentication/anonymousAuthentication`
    for the '/myapp/appdir` directory is removed.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of the website to configure.
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [String] $VirtualPath = '',

        # The configuration section path to configure, e.g.
        # `system.webServer/security/authentication/basicAuthentication`. The path should *not* start with a forward
        # slash.
        [Parameter(Mandatory)]
        [String] $SectionPath,

        # The name of the attribute to remove/clear. If the attribute doesn't exist, nothing happens.
        #
        # You can pipe multiple names to clear/remove multiple attributes.
        [Parameter(Mandatory, ValueFromPipeline)]
        [Alias('Key')]
        [String[]] $Name
    )

    begin
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        $section =
            Get-CIisConfigurationSection -LocationPath $LocationPath -VirtualPath $VirtualPath -SectionPath $SectionPath
        if( -not $section )
        {
            return
        }

        $attrNameFieldLength =
            $section.Attributes |
            Select-Object -ExpandProperty 'Name' |
            Select-Object -ExpandProperty 'Length' |
            Measure-Object -Maximum |
            Select-Object -ExpandProperty 'Maximum'
        $nameFormat = "{0,-$($attrNameFieldLength)}"

        $attrNames = [Collections.Arraylist]::New()

        $locationPathMsg = $LocationPath
        if ($VirtualPath)
        {
            $locationPathMsg = Join-CIisPath -Path $LocationPath, $VirtualPath
        }
        $basePrefix = "[IIS:/Sites/$($locationPathMsg):$($SectionPath)"
    }

    process
    {
        if( -not $section )
        {
            return
        }

        foreach( $nameItem in $Name )
        {
            $attr = $section.Attributes[$nameItem]
            if( -not $attr )
            {
                $msg = "IIS configuration section ""$($SectionPath)"" doesn't have a ""$($nameItem)"" attribute."
                Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                return
            }

            $nameItem = "$($nameItem.Substring(0, 1).ToLowerInvariant())$($nameItem.Substring(1, $nameItem.Length -1))"
            $msgPrefix = "$($basePrefix)@$($nameFormat -f $nameItem)] "

            Write-Debug "$($msgPrefix)$($attr.IsInheritedFromDefaultValue) $($attr.Value) $($attr.Schema.DefaultValue)"
            $hasDefaultValue = $attr.Value -eq $attr.Schema.DefaultValue
            if( -not $attr.IsInheritedFromDefaultValue -and -not $hasDefaultValue )
            {
                [void]$attrNames.Add($nameItem)
            }

            $pathMsg = ''
            if( $VirtualPath )
            {
                $pathMsg = ", path '$($VirtualPath)'"
            }

            $target =
                "$($nameItem) from IIS website '$($LocationPath)'$($pathMsg), configuration section '$($SectionPath)'"
            $action = 'Remove Attribute'
            if( $PSCmdlet.ShouldProcess($target, $action) )
            {
                $msg =  "Removing attribute $($target -replace '''', '"')"
                Write-Information $msg
                # Fortunately, only actually persists changes to applicationHost.config if there are any changes.
                $attr.Delete()
            }
        }
    }
}


function Remove-CIisConfigurationLocation
{
    <#
    .SYNOPSIS
    Removes a <location> element from applicationHost.config.
 
    .DESCRIPTION
    The `Remove-CIisConfigurationLocation` function removes the entire location configuration for a website or a path
    under a website. When configuration for a website or path under a website is made, those changes are sometimes
    persisted to IIS's applicationHost.config file. The configuration is placed inside a `<location>` element for that
    site and path. This function removes the entire `<location>` section, i.e. all a site's/path's custom configuration
    that isn't stored in a web.config file.
 
    Pass the website whose location configuration to remove to the `LocationPath` parameter. To delete the location
    configuration for a path under the website, pass that path to the `VirtualPath` parameter.
 
    If there is no location configuration, an error is written.
 
    .EXAMPLE
    Remove-CIisConfigurationLocation -LocationPath 'www'
 
    Demonstrates how to remove the `<location path="www">` element from IIS's applicationHost.config, i.e. all custom
    configuration for the www website that isn't in the site's web.config file.
 
    .EXAMPLE
    Remove-CIisConfigurationLocation -LocationPath 'www/some/path'
 
    Demonstrates how to remove the `<location path="www/some/path">` element from IIS's applicationHost.config, i.e.
    all custom configuration for the `some/path` path in the `www` website that isn't in the path's or site's web.config
    file.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [String] $VirtualPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($VirtualPath)
    {
        $LocationPath = Join-CIisPath -Path $LocationPath, $VirtualPath
    }

    if (-not (Get-CIisConfigurationLocationPath -LocationPath $LocationPath))
    {
        $msg = "Configuration location ""$($LocationPath)"" does not exist."
        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        return
    }

    (Get-CIisServerManager).GetApplicationHostConfiguration().RemoveLocationPath($LocationPath)
    $target = "$($LocationPath)"
    $action = "Remove IIS Location"
    $infoMsg = "Removing ""$($LocationPath)"" IIS location configuration."
    Save-CIisConfiguration -Target $target -Action $action -Message $infoMsg
}


function Remove-CIisMimeMap
{
    <#
    .SYNOPSIS
    Removes a file extension to MIME type map from an entire web server.
 
    .DESCRIPTION
    IIS won't serve static files unless they have an entry in the MIME map. Use this function toremvoe an existing MIME map entry. If one doesn't exist, nothing happens. Not even an error.
 
    If a specific website has the file extension in its MIME map, that site will continue to serve files with those extensions.
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .LINK
    Get-CIisMimeMap
 
    .LINK
    Set-CIisMimeMap
 
    .EXAMPLE
    Remove-CIisMimeMap -FileExtension '.m4v' -MimeType 'video/x-m4v'
 
    Removes the `.m4v` file extension so that IIS will no longer serve those files.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='ForWebServer')]
    param(
        # The name of the website whose MIME type to set.
        [Parameter(Mandatory, ParameterSetName='ForWebsite', Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Uset the `LocationPath` parameter instead.
        [Parameter(ParameterSetName='ForWebsite')]
        [String] $VirtualPath = '',

        # The file extension whose MIME map to remove.
        [Parameter(Mandatory)]
        [String] $FileExtension
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $getIisConfigSectionParams = @{ }
    if( $PSCmdlet.ParameterSetName -eq 'ForWebsite' )
    {
        $getIisConfigSectionParams['LocationPath'] = $LocationPath
        $getIisConfigSectionParams['VirtualPath'] = $VirtualPath
    }

    $staticContent =
        Get-CIisConfigurationSection -SectionPath 'system.webServer/staticContent' @getIisConfigSectionParams
    $mimeMapCollection = $staticContent.GetCollection()
    $mimeMapToRemove = $mimeMapCollection | Where-Object { $_['fileExtension'] -eq $FileExtension }
    if( -not $mimeMapToRemove )
    {
        Write-Verbose ('MIME map for file extension {0} not found.' -f $FileExtension)
        return
    }

    $mimeMapCollection.Remove( $mimeMapToRemove )
    Save-CIisConfiguration
}





function Restart-CIisAppPool
{
    <#
    .SYNOPSIS
    Restarts an IIS application pool.
 
    .DESCRIPTION
    The `Restart-CIisAppPool` restarts an IIS application pool. Pass the names of the application pools to restart to
    the `Name` parameter. You can also pipe application pool objects or application pool names.
 
    The application pool is stopped then started. If stopping the application pool fails, the function does not attempt
    to start it. If after 30 seconds, the application pool hasn't stopped, the function writes an error, and returns; it
    does not attempt to start the application pool. Use the `Timeout` parameter to control how long to wait for the
    application pool to stop. When the application pool hasn't stopped, and the `Force` parameter is true, the function
    attempts to kill all of the application pool's worker processes, again waiting for `Timeout` interval for the
    processes to exit. If the function is unable to kill the worker processes, the function will write an error.
 
    .EXAMPLE
    Restart-CIisAppPool -Name 'Default App Pool'
 
    Demonstrates how to restart an application pool by passing its name to the `Name` parameter.
 
    .EXAMPLE
    Restart-CIisAppPool -Name 'Default App Pool', 'Non-default App Pool'
 
    Demonstrates how to restart multiple application pools by passing their names to the `Name` parameter.
 
    .EXAMPLE
    Get-CIisAppPool | Restart-CIisAppPool
 
    Demonstrates how to restart an application pool by piping it to `Restart-CIisAppPool`.
 
    .EXAMPLE
    'Default App Pool', 'Non-default App Pool' | Restart-CIisAppPool
 
    Demonstrates how to restart one or more application pools by piping their names to `Restart-CIisAppPool`.
 
    .EXAMPLE
    Restart-CIisAppPool -Name 'Default App Pool' -Timeout '00:00:10'
 
    Demonstrates how to change the amount of time `Restart-CIisAppPool` waits for the application pool to stop. In this
    example, it will wait 10 seconds.
 
    .EXAMPLE
    Restart-CIisAppPool -Name 'Default App Pool' -Force
 
    Demonstrates how to stop an application pool that won't stop by using the `Force` (switch). After waiting for the
    application pool to stop, if it is still running and the `Force` (switch) is used, `Restart-CIisAppPool` will
    try to kill the application pool's worker processes.
    #>

    [CmdletBinding()]
    param(
        # One or more names of the application pools to restart. You can also pipe one or more names to the function or
        # pipe one or more application pool objects.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [String[]] $Name,

        # The amount of time `Restart-CIisAppPool` waits for an application pool to stop before giving up and writing
        # an error. The default is 30 seconds.
        [TimeSpan] $Timeout = [TimeSpan]::New(0, 0, 30),

        # If set, and an application pool fails to stop on its own, `Restart-CIisAppPool` will attempt to kill the
        # application pool worker processes.
        [switch] $Force
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        $stopErrors = @()

        Stop-CIisAppPool -Name $Name -Timeout $Timeout -Force:$Force -ErrorVariable 'stopErrors'

        if ($stopErrors)
        {
            return
        }

        Start-CIisAppPool -Name $Name -Timeout $Timeout
    }
}




function Restart-CIisWebsite
{
    <#
    .SYNOPSIS
    Restarts an IIS website.
 
    .DESCRIPTION
    The `Restart-CIisWebsite` restarts an IIS website. Pass the names of the websites to restart to the `Name`
    parameter. You can also pipe website objects or website names.
 
    The website is stopped then started. If stopping the website fails, the function does not attempt to start it. If
    after 30 seconds, the website hasn't stopped, the function writes an error, and returns; it does not attempt to
    start the website. Use the `Timeout` parameter to control how long to wait for the website to stop. The function
    writes an error if the website doesn't stop or start.
 
    .EXAMPLE
    Restart-CIisWebsite -Name 'Default Website'
 
    Demonstrates how to restart an website by passing its name to the `Name` parameter.
 
    .EXAMPLE
    Restart-CIisWebsite -Name 'Default Website', 'Non-default Website'
 
    Demonstrates how to restart multiple websites by passing their names to the `Name` parameter.
 
    .EXAMPLE
    Get-CIisWebsite | Restart-CIisWebsite
 
    Demonstrates how to restart an website by piping it to `Restart-CIisWebsite`.
 
    .EXAMPLE
    'Default Website', 'Non-default Website' | Restart-CIisWebsite
 
    Demonstrates how to restart one or more websites by piping their names to `Restart-CIisWebsite`.
 
    .EXAMPLE
    Restart-CIisWebsite -Name 'Default Website' -Timeout '00:00:10'
 
    Demonstrates how to change the amount of time `Restart-CIisWebsite` waits for the website to stop. In this
    example, it will wait 10 seconds.
    #>

    [CmdletBinding()]
    param(
        # One or more names of the websites to restart. You can also pipe one or more names to the function or
        # pipe one or more website objects.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [String[]] $Name,

        # The amount of time `Restart-CIisWebsite` waits for an website to stop before giving up and writing
        # an error. The default is 30 seconds.
        [TimeSpan] $Timeout = [TimeSpan]::New(0, 0, 30)
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        $stopErrors = @()

        Stop-CIisWebsite -Name $Name -Timeout $Timeout -ErrorVariable 'stopErrors'

        if ($stopErrors)
        {
            return
        }

        Start-CIisWebsite -Name $Name -Timeout $Timeout
    }
}



function Save-CIisConfiguration
{
    <#
    .SYNOPSIS
    Saves configuration changes to IIS.
 
    .DESCRIPTION
    The `Save-CIisConfiguration` function saves changes made by Carbon.IIS functions or changes made on any object
    returned by any Carbon.IIS function. After making those changes, you must call `Save-CIisConfiguration` to save
    those changes to IIS.
 
    Carbon.IIS keeps an internal `Microsoft.Web.Administration.ServerManager` object that it uses to get all objects
    it operates on or returns to the user. `Save-CIisConfiguration` calls the `CommitChanges()` method on that
    Server Manager object.
 
    .EXAMPLE
    Save-CIIsConfiguration
 
    Demonstrates how to use this function.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # Optional target object descripotion whose configuration will end up being saved. This is used as the target
        # when `-WhatIf` is true and calling `ShouldProcess(string target, string action)`.
        [String] $Target,

        # Optional action description to use when `-WhatIf` is used and calling
        # `ShouldProcess(string target, string action)`. Only used if `Target` is given.
        [String] $Action,

        # Optional message written to the information stream just before saving changes.
        [String] $Message
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if( $WhatIfPreference )
    {
        if( $Target )
        {
            $Target = $Target -replace '"', ''''
        }

        if( $Target -and $Action )
        {
            $PSCmdlet.ShouldProcess($Target, $Action) | Out-Null
        }

        if( $Target )
        {
            $PSCmdlet.ShouldProcess($Target) | Out-Null
        }

        Get-CIisServerManager -Reset | Out-Null
        return
    }

    if( $Message )
    {
        Write-Information $Message
    }

    Get-CIisServerManager -Commit | Out-Null
}

function Set-CIisAnonymousAuthentication
{
    <#
    .SYNOPSIS
    Configures anonymous authentication for all or part of a website.
 
    .DESCRIPTION
    The `Set-CIisAnonymousAuthentication` function configures anonymous authentication for all or part of a website.
    Pass the name of the site to the `SiteName` parameter. To enable anonymous authentication, use the `Enabled` switch.
    To set the identity to use for anonymous access, pass the identity's username to the `UserName` and password to the
    `Pasword` parameters. To set the logon method for the anonymous user, use the `LogonMethod` parameter.
 
    To configure anonymous authentication on a path/application/virtual directory under a website, pass the virtual path
    to that path/application/virtual directory to the `VirtualPath` parameter.
 
    .EXAMPLE
    Set-CIisAnonymousAuthentication -SiteName 'MySite' -Enabled -UserName 'MY_IUSR' -Password $password -LogonMethod Interactive
 
    Demonstrates how to use `Set-CIisAnonymousAuthentication` to configure all attributes of anonymous authentication:
    it is enabled with the `Enabled` switch, the idenity of anonymous access is set to `MY_IUSR` whose password is
    $password, with a logon method of `Interactive`.
 
    .EXAMPLE
    Set-CIisAnonymousAuthentication -SiteName 'MySite' -VirtualPath 'allowAll' -Enabled
 
    Demonstrates how to use `Set-CIisAnonymousAuthentication` to configure anonymous authentication on a
    path/application/virtual directry under a site. In this example, anonymous authentication is enabled in the `MySite`
    website's `allowAll` path/application/virtual directory.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of the website whose anonymous authentication settings to change.
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [String] $VirtualPath,

        # Enable anonymous authentication. To disable anonymous authentication you must explicitly set `Enabled` to
        # `$false`, e.g. `-Enabled $false`.
        [bool] $Enabled,

        # The logon method to use for anonymous access.
        [AuthenticationLogonMethod] $LogonMethod,

        # The password username of the identity to use to run anonymous requests. Not needed if using system accounts.
        [SecureString] $Password,

        # The username of the identity to use to run anonymous requests.
        [String] $UserName,

        # If set, the anonymous authentication setting for each parameter *not* passed is deleted, which resets it to
        # its default value. Otherwise, anonymous authentication settings whose parameters are not passed are left in
        # place and not modified.
        [switch] $Reset
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($VirtualPath)
    {
        Write-CIisWarningOnce -ForObsoleteSiteNameAndVirtualPathParameter
    }

    $attributes = $PSBoundParameters | Copy-Hashtable -Key @('enabled', 'logonMethod', 'password', 'userName')

    Set-CIisConfigurationAttribute -LocationPath ($LocationPath, $VirtualPath | Join-CIisPath) `
                                   -SectionPath 'system.webServer/security/authentication/anonymousAuthentication' `
                                   -Attribute $attributes `
                                   -Reset:$Reset
}



function Set-CIisAppPool
{
    <#
    .SYNOPSIS
    Configures an IIS application pool's settings.
 
    .DESCRIPTION
    The `Set-CIisAppPool` function configures an IIS application pool's settings. Pass the name of
    the application pool to the `Name` parameter. Pass the configuration you want to one
    or more of the AutoStart, CLRConfigFile, Enable32BitAppOnWin64, EnableConfigurationOverride, ManagedPipelineMode,
    ManagedRuntimeLoader, ManagedRuntimeVersion, Name, PassAnonymousToken, QueueLength, and/or StartMode parameters. See
    [Adding Application Pools <add>](https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/applicationpools/add/)
    for documentation on each setting.
 
    You can configure the IIS application pool defaults instead of a specific application pool by using the
    `AsDefaults` switch.
 
    If you want to ensure that any settings that may have gotten changed by hand are reset to their default values, use
    the `-Reset` switch. When set, the `-Reset` switch will reset each setting not passed as an argument to its default
    value.
 
    .LINK
    https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/applicationpools/add/
 
    .EXAMPLE
    Set-CIisAppPool -AppPoolName 'ExampleTwo' -Enable32BitAppOnWin64 $true -ManagedPipelineMode Classic
 
    Demonstrates how to configure an IIS application pool's settings. In this example, the app pool will be updated to
    run as a 32-bit applicaiton and will use a classic pipeline mode. All other settings are left unchanged.
 
    .EXAMPLE
    Set-CIisAppPool -AppPoolName 'ExampleOne' -Enable32BitAppOnWin64 $true -ManagedPipelineMode Classic -Reset
 
    Demonstrates how to reset an IIS application pool's settings to their default values by using the `-Reset` switch. In
    this example, the `enable32BitAppOnWin64` and `managedPipelineMode` settings are set to `true` and `Classic`, and
    all other application pool settings are deleted, which reset them to their default values.
 
    .EXAMPLE
    Set-CIisAppPool -AsDefaults -Enable32BitAppOnWin64 $true -ManagedPipelineMode Classic
 
    Demonstrates how to configure the IIS application pool defaults settings by using
    the `AsDefaults` switch and not passing application pool name. In this case, all future application pools created
    will be 32-bit applications and use a classic pipeline mode, unless those settings are configured differently upon
    install.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding(DefaultParameterSetName='SetInstance', SupportsShouldProcess)]
    param(
        # The name of the application pool whose settings to configure.
        [Parameter(Mandatory, ParameterSetName='SetInstance', Position=0)]
        [String] $Name,

        # If true, the function configures the IIS application pool defaults instead of a specific application pool.
        [Parameter(Mandatory, ParameterSetName='SetDefaults')]
        [switch] $AsDefaults,

        # Sets the IIS application pool's `autoStart` setting.
        [bool] $AutoStart,

        # Sets the IIS application pool's `CLRConfigFile` setting.
        [String] $CLRConfigFile,

        # Sets the IIS application pool's `enable32BitAppOnWin64` setting.
        [bool] $Enable32BitAppOnWin64,

        # Sets the IIS application pool's `enableConfigurationOverride` setting.
        [bool] $EnableConfigurationOverride,

        # Sets the IIS application pool's `managedPipelineMode` setting.
        [ManagedPipelineMode] $ManagedPipelineMode,

        # Sets the IIS application pool's `managedRuntimeLoader` setting.
        [String] $ManagedRuntimeLoader,

        # Sets the IIS application pool's `managedRuntimeVersion` setting.
        [String] $ManagedRuntimeVersion,

        # Sets the IIS application pool's `passAnonymousToken` setting.
        [bool] $PassAnonymousToken,

        # Sets the IIS application pool's `queueLength` setting.
        [UInt32] $QueueLength,

        # Sets the IIS application pool's `startMode` setting.
        [StartMode] $StartMode,

        # If set, the application pool setting for each parameter *not* passed is deleted, which resets it to its
        # default value. Otherwise, application pool settings whose parameters are not passed are left in place and not
        # modified.
        [switch] $Reset
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $getArgs = @{}
    if ($Name)
    {
        $getArgs['Name'] = $Name
    }
    elseif ($AsDefaults)
    {
        $getArgs['Defaults'] = $true
    }

    $target = Get-CIisAppPool @getArgs
    if( -not $target )
    {
        return
    }

    $targetMsg = 'IIS application pool defaults'
    if( $Name )
    {
        $targetMsg = "IIS application pool ""$($Name)"""
    }

    Invoke-SetConfigurationAttribute -ConfigurationElement $target `
                                     -PSCmdlet $PSCmdlet `
                                     -Target $targetMsg `
                                     -Attribute @{ 'name' = $Name } `
                                     -Exclude @('applicationPoolSid', 'state') `
                                     -Reset:$Reset
}



function Set-CIisAppPoolCpu
{
    <#
    .SYNOPSIS
    Configures IIS application pool CPU settings.
 
    .DESCRIPTION
    The `Set-CIisAppPoolCpu` configures an IIS application pool's CPU settings. Pass the application pool's name to the
    `AppPoolName` parameter. With no other parameters, the `Set-CIisAppPoolCpu` function removes all configuration from
    that application pool's CPU, which resets them to their defaults. To change a setting to a non-default value, pass
    the new value to its corresponding parameter. For each parameter that is *not* passed, its corresponding
    configuration is removed, which reset that configuration to its default value. See
    [CPU Settings for an Application Pool <cpu>](https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/applicationpools/add/cpu)
    for documentation on each setting.
 
    You can configure IIS's application pool defaults instead of a specific application pool's settings by using the
    `AsDefaults` switch.
 
    If you want to ensure that any settings that may have gotten changed by hand are reset to their default values, use
    the `-Reset` switch. When set, the `-Reset` switch will reset each setting not passed as an argument to its default
    value.
 
    .LINK
    https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/applicationpools/add/cpu
 
    .EXAMPLE
    Set-CIisAppPoolCpu -AppPoolName -DefaultAppPool -Limit 50000 -Action Throttle
 
    Demonstrates how to customize some of an application pool's CPU settings, while resetting all other configuration to
    their default values. In this example, the `limit` and `action` settings are set, and all other settings are
    removed, which resets them to their default values.
 
    .EXAMPLE
    Set-CIisAppPoolCpu -AppPoolName 'DefaultAppPool' -Limit 50000 -Action Throttle -Reset
 
    Demonstrates how to set *all* an IIS application pool's CPU settings by using the `-Reset` switch. In this example,
    the `limit` and `throttle` settings are set to custom values, and all other settings are deleted, which resets them
    to their default values.
 
    .EXAMPLE
    Set-CIisAppPool -AsDefaults -Limit 50000 -ActionThrottle
 
    Demonstrates how to configure the IIS application pool defaults CPU settings by using the `-AsDefaults` switch and
    not passing an application pool name.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding(DefaultParameterSetName='SetInstance', SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, ParameterSetName='SetInstance', Position=0)]
        [String] $AppPoolName,

        # If true, the function configures IIS' application pool defaults instead of
        [Parameter(Mandatory, ParameterSetName='SetDefaults')]
        [switch] $AsDefaults,

        # The value for the application pool's `action` CPU setting.
        [ProcessorAction] $Action,

        # The value for the application pool's `limit` CPU setting.
        [UInt32] $Limit,

        # The value for the application pool's `numaNodeAffinityMode` CPU setting.
        [CIisNumaNodeAffinityMode] $NumaNodeAffinityMode,

        # The value for the application pool's `numaNodeAssignment` CPU setting.
        [CIisNumaNodeAssignment] $NumaNodeAssignment,

        # The value for the application pool's `processorGroup` CPU setting.
        [int] $ProcessorGroup,

        # The value for the application pool's `resetInterval` CPU setting.
        [TimeSpan] $ResetInterval,

        # The value for the application pool's `smpAffinitized` CPU setting.
        [bool] $SmpAffinitized,

        # The value for the application pool's `smpProcessorAffinityMask` CPU setting.
        [UInt32] $SmpProcessorAffinityMask,

        # The value for the application pool's `smpProcessorAffinityMask2` CPU setting.
        [UInt32] $SmpProcessorAffinityMask2,

        # If set, the application pool CPU setting for each parameter *not* passed is deleted, which resets it to its
        # default value. Otherwise, application pool CPU settings whose parameters are not passed are left in place and
        # not modified.
        [switch] $Reset
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $getArgs = @{}
    if ($AsDefaults)
    {
        $getArgs['Defaults'] = $true
    }
    elseif ($AppPoolName)
    {
        $getArgs['Name'] = $AppPoolName
    }

    $appPool = Get-CIisAppPool @getArgs
    if( -not $appPool )
    {
        return
    }

    $target = 'IIS application pool defaults CPU'
    if( $AppPoolName )
    {
        $target = """$($AppPoolName)"" IIS application pool's CPU"
    }

    Invoke-SetConfigurationAttribute -ConfigurationElement $appPool.Cpu -PSCmdlet $PSCmdlet -Target $target -Reset:$Reset
}


function Set-CIisAppPoolPeriodicRestart
{
    <#
    .SYNOPSIS
    Configures an IIS application pool's periodic restart settings.
 
    .DESCRIPTION
    The `Set-CIisAppPoolPeriodicRestart` function configures all the settings on an IIS application pool's
    periodic restart settings. Pass the name of the application pool to the `AppPoolName` parameter. Pass the
    configuration to the `Memory`, `PrivateMemory`, `Requests`, and `Time` parameters (see
    [Periodic Restart Settings for Application Pool Recycling <periodicRestart>](https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/applicationpools/add/recycling/periodicrestart/))
    for documentation on what these settings are for.
 
    Use the `Schedule` parameter to add times to the periodic restart configuration for time each day IIS should recycle
    the application pool.
 
    If you want to ensure that any settings that may have gotten changed by hand are reset to their default values, use
    the `-Reset` switch. When set, the `-Reset` switch will reset each setting not passed as an argument to its default
    value.
 
    .LINK
    https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/applicationpools/add/recycling/periodicrestart/
 
    .EXAMPLE
    Set-CIisAppPoolPeriodicRestart -AppPoolName 'Snafu' -Memory 1000000 -PrivateMemory 2000000 -Requests 3000000 -Time '23:00:00'
 
    Demonstrates how to configure all an IIS applicaton pool's periodic restart settings. In this example, `memory` will
    be set to `1000000`, `privateMemory` will be set to `2000000`, `requests` will be sent to `3000000`, and `time` will
    be sent to `23:00:00'.
 
    .EXAMPLE
    Set-CIisAppPoolPeriodicRestart -AppPoolName 'Fubar' -Memory 1000000 -PrivateMemory 2000000 -Reset
 
    Demonstrates how to set *all* an IIS application pool's periodic restart settings by using the `-Reset` switch. Any
    setting not passed will be deleted, which resets it to its default value. In this example, the `memory` and
    `privateMemory` settings are configured, and all other settings are set to their default values.
 
    .EXAMPLE
    Set-CIisAppPoolPeriodicRestart -AsDefaults -Memory 1000000 -PrivateMemory 2000000
 
    Demonstrates how to configure the IIS application pool defaults periodic restart settings by using the `AsDefaults`
    switch and not passing the application pool name.
    #>

    [CmdletBinding(DefaultParameterSetName='SetInstance', SupportsShouldProcess)]
    param(
        # The name of the IIS application pool whose periodic restart settings to configure.
        [Parameter(Mandatory, ParameterSetName='SetInstance', Position=0)]
        [String] $AppPoolName,

        # If true, the function configures IIS' application pool defaults instead of
        [Parameter(Mandatory, ParameterSetName='SetDefaults')]
        [switch] $AsDefaults,

        # Sets the IIS application pool's periodic restart `memory` setting.
        [UInt32] $Memory,

        # Sets the IIS application pool's periodic restart `privateMemory` setting.
        [UInt32] $PrivateMemory,

        # Sets the IIS application pool's periodic restart `requests` setting.
        [UInt32] $Requests,

        # Sets the IIS application pool's periodic restart `time` setting.
        [TimeSpan] $Time,

        # Sets the IIS application pool's periodic restart `schedule` list. The default is to have no scheduled
        # restarts.
        [TimeSpan[]] $Schedule = @(),

        # If set, the application pool periodic restart setting for each parameter *not* passed is deleted, which resets
        # it to its default value. Otherwise, application pool periodic restart settings whose parameters are not passed
        # are left in place and not modified.
        [switch] $Reset
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $getArgs = @{}
    if ($AppPoolName)
    {
        $getArgs['Name'] = $AppPoolName
    }
    elseif ($AsDefaults)
    {
        $getArgs['Defaults'] = $true
    }

    $appPool = Get-CIisAppPool @getArgs
    if( -not $appPool )
    {
        return
    }

    $currentSchedule = $appPool.Recycling.PeriodicRestart.Schedule
    $currentTimes = $currentSchedule | Select-Object -ExpandProperty 'Time' | Sort-Object
    $Schedule = $Schedule | Sort-Object
    $scheduleChanged = $false
    if( ($currentTimes -join ', ') -ne ($Schedule -join ', ') )
    {
        $prefixMsg = "IIS ""$($AppPoolName)"" application pool: periodic restart schedule "
        $clearedPrefix = $false

        foreach( $time in (($currentTimes + $Schedule) | Select-Object -Unique) )
        {
            $icon = ' '
            $action = ''
            if( $Schedule -notcontains $time )
            {
                $icon = '-'
                $action = 'Remove'
            }
            elseif( $currentTimes -notcontains $time )
            {
                $icon = '+'
                $action = 'Add'
            }

            if( $icon -eq ' ' )
            {
                continue
            }

            $action = "$($action) Time"
            $target = "$($time) for '$($AppPoolName)' IIS application pool's periodic restart schedule"
            if( $PSCmdlet.ShouldProcess($target, $action) )
            {
                Write-Information "$($prefixMsg)$($icon) $($time)"
                $scheduleChanged = $true
            }
            if( -not $clearedPrefix )
            {
                $prefixMsg = ' ' * $prefixMsg.Length
                $clearedPrefix = $true
            }
        }

        if ($scheduleChanged)
        {
            $currentSchedule.Clear()
            foreach( $time in $Schedule )
            {
                $add = $currentSchedule.CreateElement('add')
                try
                {
                    $add.SetAttributeValue('value', $time)
                }
                catch
                {
                    $msg = "Failed to add time ""$($time)"" to ""$($AppPoolName)"" IIS application pool's periodic " +
                        "restart schedule: $($_)"
                    Write-Error -Message $msg -ErrorAction Stop
                }

                $currentSchedule.Add($add)
            }

            Save-CIisConfiguration
        }
    }

    $appPool = Get-CIisAppPool @getArgs
    if( -not $appPool )
    {
        return
    }

    $targetMsg = 'IIS appliation pool defaults periodic restart'
    if( $AppPoolName )
    {
        $targetMsg = """$($AppPoolName)"" IIS application pool's periodic restart"
    }

    Invoke-SetConfigurationAttribute -ConfigurationElement $appPool.Recycling.PeriodicRestart `
                                     -PSCmdlet $PSCmdlet `
                                     -Target $targetMsg `
                                     -Reset:$Reset
}



function Set-CIisAppPoolProcessModel
{
    <#
    .SYNOPSIS
    Configures an IIS application pool's process model settings.
 
    .DESCRIPTION
    The `Set-CIisAppPoolProcessModel` function configures an IIS application pool's process model settings. Pass the
    name of the application pool to the `AppPoolName` parameter. Pass the process model configuration you want to one
    or more of the IdentityType, IdleTimeout, IdleTimeoutAction, LoadUserProfile, LogEventOnProcessModel, LogonType, ManualGroupMembership, MaxProcesses, Password, PingingEnabled, PingInterval, PingResponseTime, RequestQueueDelegatorIdentity, SetProfileEnvironment, ShutdownTimeLimit, StartupTimeLimit, and/or UserName parameters. See
    [Process Model Settings for an Application Pool <processModel>](https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/applicationpools/add/processmodel)
    for documentation on each setting.
 
    You can configure the IIS application pool defaults instead of a specific application pool by using the
    `AsDefaults` switch.
 
    If you want to ensure that any settings that may have gotten changed by hand are reset to their default values, use
    the `-Reset` switch. When set, the `-Reset` switch will reset each setting not passed as an argument to its default
    value.
 
    .LINK
    https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/applicationpools/add/processmodel
 
    .EXAMPLE
    Set-CIisAppPoolProcessModel -AppPoolName 'ExampleTwo' -UserName 'user1' -Password $password
 
    Demonstrates how to set an IIS application pool to run as a custom identity. In this example, the application pool
    is updated to run as the user `user1`. All other process model settings are reset to their defaults.
 
    .EXAMPLE
    Set-CIisAppPoolProcessModel -AppPoolName 'ExampleOne' -UserName 'user1' -Password $password -Reset
 
    Demonstrates how to set *all* an IIS application pool's settings by using the `-Reset` switch. Any setting not passed
    as an argument is deleted, which resets it to its default value. In this example, the `ExampleOne` application
    pool's `userName` and `password` settings are updated and all other settings are deleted.
 
    .EXAMPLE
    Set-CIisAppPoolProcessModel -AsDefaults -IdleTimeout '00:00:00'
 
    Demonstrates how to configure the IIS application pool defaults process model settings by using the `AsDefaults`
    switch and not passing application pool name. In this example, the application pool defaults `idleTimeout` setting
    is set to `00:00:00`.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding(DefaultParameterSetName='SetInstance', SupportsShouldProcess)]
    param(
        # The name of the application pool whose process model settings to set.
        [Parameter(Mandatory, ParameterSetName='SetInstance', Position=0)]
        [String] $AppPoolName,

        # If true, the function configures the IIS application pool defaults instead of a specific application pool.
        [Parameter(Mandatory, ParameterSetName='SetDefaults')]
        [switch] $AsDefaults,

        # Sets the IIS application pool's process model `identityType` setting.
        [ProcessModelIdentityType] $IdentityType,

        # Sets the IIS application pool's process model `idleTimeout` setting.
        [TimeSpan] $IdleTimeout,

        # Sets the IIS application pool's process model `idleTimeoutAction` setting.
        [IdleTimeoutAction] $IdleTimeoutAction,

        # Sets the IIS application pool's process model `loadUserProfile` setting.
        [bool] $LoadUserProfile,

        # Sets the IIS application pool's process model `logEventOnProcessModel` setting.
        [ProcessModelLogEventOnProcessModel] $LogEventOnProcessModel,

        # Sets the IIS application pool's process model `logonType` setting.
        [CIisProcessModelLogonType] $LogonType,

        # Sets the IIS application pool's process model `manualGroupMembership` setting.
        [bool] $ManualGroupMembership,

        # Sets the IIS application pool's process model `maxProcesses` setting.
        [UInt32] $MaxProcesses,

        # Sets the IIS application pool's process model `password` setting.
        [securestring] $Password,

        # Sets the IIS application pool's process model `pingingEnabled` setting.
        [bool] $PingingEnabled,

        # Sets the IIS application pool's process model `pingInterval` setting.
        [TimeSpan] $PingInterval,

        # Sets the IIS application pool's process model `pingResponseTime` setting.
        [TimeSpan] $PingResponseTime,

        # Sets the IIS application pool's process model `requestQueueDelegatorIdentity` setting.
        [String] $RequestQueueDelegatorIdentity,

        # Sets the IIS application pool's process model `setProfileEnvironment` setting.
        [bool] $SetProfileEnvironment,

        # Sets the IIS application pool's process model `shutdownTimeLimit` setting.
        [TimeSpan] $ShutdownTimeLimit,

        # Sets the IIS application pool's process model `startupTimeLimit` setting.
        [TimeSpan] $StartupTimeLimit,

        # Sets the IIS application pool's process model `userName` setting.
        [String] $UserName,

        # If set, the application pool process model setting for each parameter *not* passed is deleted, which resets it
        # to its default value. Otherwise, application pool process model settings whose parameters are not passed are
        # left in place and not modified.
        [switch] $Reset
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $getArgs = @{}
    if ($AppPoolName)
    {
        $getArgs['Name'] = $AppPoolName
    }
    elseif ($AsDefaults)
    {
        $getArgs['Defaults'] = $true
    }

    $target = Get-CIisAppPool @getArgs
    if( -not $target )
    {
        return
    }

    $targetMsg = 'IIS application pool defaults process model'
    if( $AppPoolName )
    {
        $targetMsg = """$($AppPoolName)"" IIS application pool's process model"
    }

    Invoke-SetConfigurationAttribute -ConfigurationElement $target.ProcessModel `
                                     -PSCmdlet $PSCmdlet `
                                     -Target $targetMsg `
                                     -Reset:$Reset
}



function Set-CIisAppPoolRecycling
{
    <#
    .SYNOPSIS
    Configures an IIS application pool's recycling settings.
 
    .DESCRIPTION
    The `Set-CIisAppPoolRecycling` function configures an IIS application pool's recycling settings. Pass the name of
    the application pool to the `AppPoolName` parameter. Pass the recycling configuration you want to one or more of the
    DisallowOverlappingRotation, DisallowRotationOnConfigChange, and/or LogEventOnRecycle parameters. See
    [Recycling Settings for an Application Pool <recycling>](https://learn.microsoft.com/en-us/iis/configuration/system.applicationhost/applicationpools/add/recycling/)
    for documentation on each setting.
 
    You can configure the IIS default application pool instead of a specific application pool by using the `AsDefaults`
    switch.
 
    If the `Reset` switch is set, each setting *not* passed as a parameter is deleted, which resets it to its default
    values.
 
    .LINK
    https://learn.microsoft.com/en-us/iis/configuration/system.applicationhost/applicationpools/add/recycling/
 
    .EXAMPLE
    Set-CIisAppPoolRecycling -AppPoolName 'ExampleTwo' -DisallowOverlappingRotation $true -DisallowRotationOnConfigChange $true -LogEventOnRecycle None
 
    Demonstrates how to configure all an IIS application pool's recycling settings.
 
    .EXAMPLE
    Set-CIisAppPoolRecycling -AppPoolName 'ExampleOne' -DisallowOverlappingRotation $true -Reset
 
    Demonstrates how to set *all* an IIS application pool's recycling settings (even if not passing all parameters) by
    using the `-Reset` switch. In this example, the disallowOverlappingRotation setting is set to `$true`, and the
    `disallowRotationOnConfigChange` and `LogEventOnRecycle` settings are deleted, which resets them to their default
    values.
 
    .EXAMPLE
    Set-CIisAppPoolRecycling -AsDefaults -LogEventOnRecycle None
 
    Demonstrates how to configure the IIS application pool defaults recycling settings by using the `AsDefaults` switch
    and not passing the application pool name. In this example, the default application pool `logEventOnRecycle` recycle
    setting will be set to `None`.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding(DefaultParameterSetName='SetInstance', SupportsShouldProcess)]
    param(
        # The name of the application pool whose recycling settings to configure.
        [Parameter(Mandatory, ParameterSetName='SetInstance', Position=0)]
        [String] $AppPoolName,

        # If true, the function configures the IIS default application pool instead of a specific application pool.
        [Parameter(Mandatory, ParameterSetName='SetDefaults')]
        [switch] $AsDefaults,

        # Sets the IIS application pool's recycling `disallowOverlappingRotation` setting.
        [bool] $DisallowOverlappingRotation,

        # Sets the IIS application pool's recycling `disallowRotationOnConfigChange` setting.
        [bool] $DisallowRotationOnConfigChange,

        # Sets the IIS application pool's recycling `logEventOnRecycle` setting.
        [RecyclingLogEventOnRecycle] $LogEventOnRecycle,

        # If set, each application pool recycling setting *not* passed as a parameter is deleted, which resets it to its
        # default value.
        [switch] $Reset
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $getArgs = @{}
    if ($AppPoolName)
    {
        $getArgs['Name'] = $AppPoolName
    }
    elseif ($AsDefaults)
    {
        $getArgs['Defaults'] = $true
    }

    $target = Get-CIisAppPool @getArgs
        if( -not $target )
    {
        return
    }

    $targetMsg = 'default IIS application pool recycling'
    if( $AppPoolName )
    {
        $targetMsg = """$($AppPoolName)"" IIS application pool's recycling"
    }

    Invoke-SetConfigurationAttribute -ConfigurationElement $target.recycling `
                                     -PSCmdlet $PSCmdlet `
                                     -Target $targetMsg `
                                     -Reset:$Reset
}



function Set-CIisConfigurationAttribute
{
    <#
    .SYNOPSIS
    Sets attribute values on an IIS configuration section.
 
    .DESCRIPTION
    The `Set-CIisConfigurationAttribute` function can set a single attribute value or *all* attribute values on an IIS
    configuration section. Pass the virtual/location path of the website, application, virtual directory, or directory
    to configure to the `LocationPath` parameter. Pass the path to the configuration section to update to the
    `SectionPath` parameter. To set a single attribute value, and leave all other attributes unchanged, pass the
    attribute name to the `Name` parameter and its value to the `Value` parameter. If the new value is different than
    the current value, the value is changed and saved in IIS's applicationHost.config file inside a `location` section.
 
    To set *all* attributes on a configuration section, pass the attribute names and values in a hashtable to the
    `Attribute` parameter. Attributes in the hashtable will be updated to match the value in the hashtable. All other
    attributes will be left unchanged. You can delete attributes from the configuration section that aren't in the
    attributes hashtable by using the `Reset` switch. Deleting attributes reset them to their default values.
 
    To configure a global configuration section, omit the `LocationPath` parameter, or pass a
    `Microsoft.Web.Administration.ConfigurationElement` object to the `ConfigurationElement` parameter.
 
    `Set-CIisConfigurationAttribute` writes messages to PowerShell's information stream for each attribute whose value
    is changing, showing the current value and the new value. If an attribute's value is sensitive, use the `Sensitive`
    switch, and the attribute's current and new value will be masked with eight `*` characters.
 
    .EXAMPLE
    Set-CIisConfigurationAttribute -LocationPath 'SiteOne' -SectionPath 'system.webServer/httpRedirect' -Name 'destination' -Value 'http://example.com'
 
    Demonstrates how to call `Set-CIisConfigurationAttribute` to set a single attribute value for a website,
    application, virtual directory, or directory. In this example, the `SiteOne` website's http redirect "destination"
    setting is set `http://example.com`. All other attributes on the website's `system.webServer/httpRedirect` are left
    unchanged.
 
    .EXAMPLE
    Set-CIisConfigurationAttribute -LocationPath 'SiteTwo' -SectionPath 'system.webServer/httpRedirect' -Attribute @{ 'destination' = 'http://example.com'; 'httpResponseStatus' = 302 }
 
    Demonstrates how to set multiple attributes on a configuration section by piping a hashtable of attribute names and
    values to `Set-CIisConfigurationAttribute`. In this example, the `destination` and `httpResponseStatus` attributes
    are set to `http://example.com` and `302`, respectively. All other attributes on `system.webServer/httpRedirect`
    are preserved.
 
    .EXAMPLE
    Set-CIisConfigurationAttribute -LocationPath 'SiteTwo' -SectionPath 'system.webServer/httpRedirect' -Attribute @{ 'destination' = 'http://example.com' } -Reset
 
    Demonstrates how to delete attributes that aren't passed to the `Attribute` parameter by using the `Reset` switch.
    In this example, the "SiteTwo" website's HTTP Redirect setting's destination attribute is set to
    `http://example.com`, and all its other attributes (if they exist) are deleted (e.g. `httpResponseStatus`,
    `childOnly`, etc.), which resets the deleted attributes to their default values.
 
    .EXAMPLE
    Set-CIisConfigurationAttribute -SectionPath 'system.webServer/httpRedirect' -Name 'destination' -Value 'http://example.com'
 
    Demonstrates how to set attribute values on a global configuration section by omitting the `LocationPath`
    parameter. In this example, the global HTTP redirect destination is set to `http://example.com`.
 
    .EXAMPLE
    Set-CIisConfigurationAttribute -ConfigurationElement (Get-CIisAppPool -Name 'DefaultAppPool').Cpu -Name 'limit' -Value 10000
 
    Demonstrates how to set attribute values on a configuration element object by passing the object to the
    `ConfigurationElement` parameter. In this case the "limit" setting for the "DefaultAppPool" application pool will be
    set.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of the website whose attribute values to configure.
        [Parameter(Mandatory, ParameterSetName='AllByConfigPath', Position=0)]
        [Parameter(Mandatory, ParameterSetName='SingleByConfigPath', Position=0)]
        [String] $LocationPath,

        # The configuration section path to configure, e.g.
        # `system.webServer/security/authentication/basicAuthentication`. The path should *not* start with a forward
        # slash. You can also pass
        [Parameter(Mandatory, ParameterSetName='AllByConfigPath')]
        [Parameter(Mandatory, ParameterSetName='SingleByConfigPath')]
        [Parameter(Mandatory, ParameterSetName='AllForSection')]
        [Parameter(Mandatory, ParameterSetName='SingleForSection')]
        [String] $SectionPath,

        [Parameter(Mandatory, ParameterSetName='AllByConfigElement')]
        [Parameter(Mandatory, ParameterSetName='SingleByConfigElement')]
        [Microsoft.Web.Administration.ConfigurationElement] $ConfigurationElement,

        # A hashtable whose keys are attribute names and the values are the attribute values. Any attribute *not* in
        # the hashtable is ignored, unless the `All` switch is present, in which case, any attribute *not* in the
        # hashtable is removed from the configuration section (i.e. reset to its default value).
        [Parameter(Mandatory, ParameterSetName='AllByConfigElement')]
        [Parameter(Mandatory, ParameterSetName='AllByConfigPath')]
        [Parameter(Mandatory, ParameterSetName='AllForSection')]
        [hashtable] $Attribute,

        # The target element the change is being made on. Used in messages written to the console. The default is to
        # use the type and tag name of the ConfigurationElement.
        [Parameter(ParameterSetName='AllByConfigElement')]
        [Parameter(ParameterSetName='AllByConfigPath')]
        [Parameter(ParameterSetName='AllForSection')]
        [String] $Target,

        # Properties to skip and not change. These are usually private settings that we shouldn't be mucking with or
        # settings that capture current state, etc.
        [Parameter(ParameterSetName='AllByConfigElement')]
        [Parameter(ParameterSetName='AllByConfigPath')]
        [Parameter(ParameterSetName='AllForSection')]
        [String[]] $Exclude = @(),

        # If set, each setting on the configuration element whose attribute isn't in the `Attribute` hashtable is
        # deleted, which resets it to its default value. Otherwise, configuration element attributes not in the
        # `Attributes` hashtable left in place and not modified.
        [Parameter(ParameterSetName='AllByConfigElement')]
        [Parameter(ParameterSetName='AllByConfigPath')]
        [Parameter(ParameterSetName='AllForSection')]
        [switch] $Reset,

        # The name of the attribute whose value to set. Setting a single attribute will not affect any other attributes
        # in the configuration section. If you want other attribute values reset to default values, pass a hashtable
        # of attribute names and values to the `Attribute` parameter.
        [Parameter(Mandatory, ParameterSetName='SingleByConfigElement')]
        [Parameter(Mandatory, ParameterSetName='SingleByConfigPath')]
        [Parameter(Mandatory, ParameterSetName='SingleForSection')]
        [String] $Name,

        # The attribute's value. Setting a single attribute will not affect any other attributes in the configuration
        # section. If you want other attribute values reset to default values, pass a hashtable of attribute names and
        # values to the `Attribute` parameter.
        [Parameter(Mandatory, ParameterSetName='SingleByConfigElement')]
        [Parameter(Mandatory, ParameterSetName='SingleByConfigPath')]
        [Parameter(Mandatory, ParameterSetName='SingleForSection')]
        [AllowNull()]
        [AllowEmptyString()]
        [Object] $Value,

        # If the attribute's value is sensitive. If set, the attribute's value will be masked when written to the
        # console.
        [bool] $Sensitive
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    function Set-AttributeValue
    {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)]
            [Microsoft.Web.Administration.ConfigurationElement] $Element,

            [Parameter(Mandatory)]
            [Alias('Key')]
            [String] $Name,

            [AllowNull()]
            [AllowEmptyString()]
            [Object] $Value
        )

        if( $Exclude -and $Name -in $Exclude )
        {
            return
        }

        $Name = "$($Name.Substring(0, 1).ToLowerInvariant())$($Name.Substring(1, $Name.Length - 1))"

        $currentAttr = $Element.Attributes[$Name]

        if (-not $currentAttr)
        {
            $locationPathMsg = ''
            if ($Element.LocationPath)
            {
                $locationPathMsg = " at location ""$($Element.LocationPath)"""
            }
            $msg = "Unable to set attribute ""$($Name)"" on configuration element ""$($Element.SectionPath)""" +
                   "$($locationPathMsg) because that attribute doesn't exist on that element. Valid attributes are: " +
                   "$(($Element.Attributes | Select-Object -ExpandProperty 'Name') -join ', ')."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $parentAttr = $null
        if ($parentConfigElement)
        {
            $parentAttr = $parentConfigElement.Attributes[$Name]
        }

        $defaultValue = $currentAttr.Schema.DefaultValue
        if ($parentAttr)
        {
            $defaultValue = $parentAttr.Value
            if ($parentAttr.IsInheritedFromDefaultValue)
            {
                $defaultValue = $parentAttr.Schema.DefaultValue
            }
        }

        $currentValue = $currentAttr.Value

        $protectValue = $Sensitive -or $currentAttr.Name -eq 'password'
        if( $Value -is [SecureString] )
        {
            $Value = [pscredential]::New('i', $Value).GetNetworkCredential().Password
            $protectValue = $true
        }
        elseif( $Value -is [switch] )
        {
            $Value = $Value.IsPresent
        }

        $appHostAttrExists = ($configElementAppHostNode -and $configElementAppHostNode.HasAttribute($Name))

        $currentValueMsg = "[$($defaultValue)]"
        $currentValueIsDefault = $true
        if ($null -ne $currentValue -and -not $currentAttr.IsInheritedFromDefaultValue -and $appHostAttrExists)
        {
            $currentValueMsg = $currentValue.ToString()
            $currentValueIsDefault = $false
        }

        $valueMsg = "[$($defaultValue)]"
        if ($null -ne $Value)
        {
            $valueMsg = $Value.ToString()
        }

        if ($Value -is [Enum])
        {
            $currentValueAsEnum = [Enum]::Parse($Value.GetType().FullName, $currentValue, $true)
            $currentValueMsg = "$($currentValueAsEnum) ($($currentValueAsEnum.ToString('D')))"

            if ($currentValueIsDefault)
            {
                $currentValueMsg = "[$($currentValueMsg)]"
            }

            $valueMsg = "$($Value) ($($Value.ToString('D')))"
        }
        elseif( $currentAttr.Schema.Type -eq 'timeSpan' -and $Value -is [UInt64] )
        {
            $valueMsg = [TimeSpan]::New($Value)
        }

        if( $protectValue )
        {
            $currentValueMsg = '*' * 8
            $valueMsg = '*' * 8
        }

        $msgPrefix = " @$($nameFormat -f $currentAttr.Name) "
        $noChangeMsg = "$($msgPrefix)$($currentValueMsg) == $($valueMsg)"
        $changedMsg =  "$($msgPrefix)$($currentValueMsg) -> $($valueMsg)"

        if ($null -eq $Value)
        {
            if ($currentAttr.IsInheritedFromDefaultValue)
            {
                # do nothing
                Write-Debug $noChangeMsg
                return
            }

            if ($LocationPath )
            {
                # The `IsInheritedFromDefaultValue` property is `$false` if the applicationHost.config defines the
                # element and the element attribute values are the same value as the default value. So, the only way to
                # know if the location we're working on doesn't have the attribute is to load the applicationHost.config
                # and look for the attribute. :(
                $xpath = "/configuration/location[@path = '$($LocationPath)']/$($Element.SectionPath)/@$($Name)"
                if (-not $appHostConfigXml.SelectSingleNode($xpath))
                {
                    # do nothing
                    Write-Debug $noChangeMsg
                    return
                }
            }

            # Attribute was previously supplied but now it isn't, or, attribute value changed manually. Delete
            # attribute so its value reverts to IIS's default value.
            $infoMessages.Add($changedMsg)
            $action = "Remove Attribute"
            $whatIf = "$($currentAttr.Name) for $($Target -replace '"', '''')"
            if( $PSCmdlet.ShouldProcess($whatIf, $action) )
            {
                try
                {
                    $currentAttr.Delete()
                }
                catch
                {
                    $msg = "Exception resetting ""$($currentAttr.Name)"" on $($Target) to its default value (by " +
                            "deleting it): $($_)"
                    Write-Error -Message $msg
                    return
                }
                [void]$removedNames.Add($currentAttr.Name)
            }
            return
        }

        if ($currentValue -eq $Value)
        {
            if (-not $isConfigSection -or ($isConfigSection -and $appHostAttrExists))
            {
                Write-Debug $noChangeMsg
                return
            }
        }

        [void]$infoMessages.Add($changedMsg)
        try
        {
            $ConfigurationElement.SetAttributeValue($currentAttr.Name, $Value)
        }
        catch
        {
            $msg = "Exception setting ""$($currentAttr.Name)"" on $($Target): $($_)"
            Write-Error -Message $msg -ErrorAction Stop
        }
        [void]$updatedNames.Add($currentAttr.Name)
    }

    if (-not $ConfigurationElement)
    {
        $locationPathArg = @{}
        if ($LocationPath)
        {
            $locationPathArg['LocationPath'] = $LocationPath
        }
        $ConfigurationElement = Get-CIisConfigurationSection -SectionPath $SectionPath @locationPathArg
        if( -not $ConfigurationElement )
        {
            return
        }
    }

    $parentConfigElement = $null
    if ($LocationPath)
    {
        $parentConfigElement = Get-CIisConfigurationSection -SectionPath $SectionPath
    }

    $isConfigSection = $null -ne ($ConfigurationElement | Get-Member -Name 'SectionPath')
    if( -not $SectionPath -and $isConfigSection )
    {
        $SectionPath = $ConfigurationElement.SectionPath
    }

    [xml] $appHostConfigXml = Get-Content -Path $script:applicationHostPath

    $parentAppHostNode = $null
    $locationAppHostNode = $null
    $configElementAppHostNode = $null
    if ($isConfigSection)
    {
        $xpath = "/configuration/$($SectionPath)"
        $parentAppHostNode = $appHostConfigXml.SelectSingleNode($xpath)
        $configElementAppHostNode = $parentAppHostNode
        if ($LocationPath)
        {
            $xpath = "/configuration/location[@path = '$($LocationPath)']/$($SectionPath)"
            $locationAppHostNode = $appHostConfigXml.SelectSingleNode($xpath)
            $configElementAppHostNode = $locationAppHostNode
        }
    }

    $attrNameFieldLength =
        $ConfigurationElement.Attributes |
        Select-Object -ExpandProperty 'Name' |
        Select-Object -ExpandProperty 'Length' |
        Measure-Object -Maximum |
        Select-Object -ExpandProperty 'Maximum'

    $nameFormat = "{0,-$($attrNameFieldLength)}"

    $updatedNames = [Collections.ArrayList]::New()
    $removedNames = [Collections.ArrayList]::New()

    $infoMessages = [Collections.Generic.List[String]]::New()

    if( -not $Target )
    {
        if( $SectionPath )
        {
            $Target = $sectionPath
        }
        else
        {
            $Target = $ConfigurationElement.GetType().Name
        }

        if ($LocationPath)
        {
            $Target = "$($Target) at location $($LocationPath)"
        }
    }

    if ($Name)
    {
        Set-AttributeValue -Element $ConfigurationElement -Name $Name -Value $Value
    }
    else
    {
        $attrsToUpdate = $Attribute.Keys
        if ($Reset)
        {
            $attrsToUpdate = $ConfigurationElement.Attributes | Select-Object -ExpandProperty 'Name'
        }

        foreach ($attrName in ($attrsToUpdate | Sort-Object))
        {
            Set-AttributeValue -Element $ConfigurationElement -Name $attrName -Value $Attribute[$attrName]
        }
    }

    $pluralSuffix = ''
    if( $updatedNames.Count -gt 1 )
    {
        $pluralSuffix = 's'
    }

    $whatIfTarget = "$($updatedNames -join ', ') for $($Target -replace '"', '''')"
    $action = "Set Attribute$($pluralSuffix)"
    $shouldCommit = $updatedNames -and $PSCmdlet.ShouldProcess($whatIfTarget, $action)

    if( $shouldCommit -or $removedNames )
    {
        if( $infoMessages.Count -eq 1 )
        {
            $msg = "Configuring $($Target): $($infoMessages.Trim() -replace ' {2,}', ' ')"
            Write-Information $msg
        }
        elseif( $infoMessages.Count -gt 1)
        {
            Write-Information "Configuring $($Target)."
            $infoMessages | ForEach-Object { Write-Information $_ }
        }
    }

    # Only save if we made any changes.
    if( $updatedNames -or $removedNames )
    {
        Save-CIisConfiguration
    }
}


function Set-CIisHttpHeader
{
    <#
    .SYNOPSIS
    Sets an HTTP header for a website or a directory under a website.
 
    .DESCRIPTION
    If the HTTP header doesn't exist, it is created. If a header exists, its value is replaced.
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .LINK
    Get-CIisHttpHeader
 
    .EXAMPLE
    Set-CIisHttpHeader -LocationPath 'SopwithCamel' -Name 'X-Flown-By' -Value 'Snoopy'
 
    Sets or creates the `SopwithCamel` website's `X-Flown-By` HTTP header to the value `Snoopy`.
 
    .EXAMPLE
    Set-CIisHttpHeader -LocationPath 'SopwithCamel/Engine' -Name 'X-Powered-By' -Value 'Root Beer'
 
    Sets or creates the `SopwithCamel` website's `Engine` sub-directory's `X-Powered-By` HTTP header to the value `Root Beer`.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of the website where the HTTP header should be set/created.
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [Alias('Path')]
        [String] $VirtualPath,

        # The name of the HTTP header.
        [Parameter(Mandatory)]
        [String] $Name,

        # The value of the HTTP header.
        [Parameter(Mandatory)]
        [String] $Value
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $sectionPath = 'system.webServer/httpProtocol'
    $httpProtocol =
        Get-CIisConfigurationSection -LocationPath $locationPath -VirtualPath $VirtualPath -SectionPath $sectionPath
    $headers = $httpProtocol.GetCollection('customHeaders')
    $header = $headers | Where-Object { $_['name'] -eq $Name }

    if( $header )
    {
        $action = 'Set'
        $header['name'] = $Name
        $header['value'] = $Value
    }
    else
    {
        $action = 'Add'
        $addElement = $headers.CreateElement( 'add' )
        $addElement['name'] = $Name
        $addElement['value'] = $Value
        [void] $headers.Add( $addElement )
    }

    if ($VirtualPath)
    {
        $LocationPath = Join-CIisPath -Path $LocationPath, $VirtualPath
    }
    Save-CIisConfiguration -Target "IIS Website '$($LocationPath)'" -Action "$($action) $($Name) HTTP Header"
}




function Set-CIisHttpRedirect
{
    <#
    .SYNOPSIS
    Turns on HTTP redirect for all or part of a website.
 
    .DESCRIPTION
    Configures all or part of a website to redirect all requests to another website/URL. Pass the virtual/location path
    to the website, application, virtual directory, or directory to configure to the `LocationPath` parameter. Pass the
    redirect destination to the `Destination` parameter. Pass the redirect HTTP response status code to the
    `HttpResponseStatus`. Pass `$true` or `$false` to the `ExactDestination` parameter. Pass `$true` or `$false` to the
    `ChildOnly` parameter.
 
    For each parameter that isn't provided, the current value of that attribute is not changed. To delete any attributes
    whose parameter isn't passed, use the `Reset` switch. Deleting an attribute resets it to its default value.
 
    .LINK
    http://www.iis.net/configreference/system.webserver/httpredirect#005
 
    .LINK
    http://technet.microsoft.com/en-us/library/cc732969(v=WS.10).aspx
 
    .EXAMPLE
    Set-CIisHttpRedirect -LocationPath Peanuts -Destination 'http://new.peanuts.com'
 
    Redirects all requests to the `Peanuts` website to `http://new.peanuts.com`.
 
    .EXAMPLE
    Set-CIisHttpRedirect -LocationPath 'Peanuts/Snoopy/DogHouse' -Destination 'http://new.peanuts.com'
 
    Redirects all requests to the `/Snoopy/DogHouse` path on the `Peanuts` website to `http://new.peanuts.com`.
 
    .EXAMPLE
    Set-CIisHttpRedirect -LocationPath Peanuts -Destination 'http://new.peanuts.com' -StatusCode 'Temporary'
 
    Redirects all requests to the `Peanuts` website to `http://new.peanuts.com` with a temporary HTTP status code. You
    can also specify `Found` (HTTP 302), `Permanent` (HTTP 301), or `PermRedirect` (HTTP 308).
 
    .EXAMPLE
    Set-CIisHttpRedirect -LocationPath 'Peanuts' -Destination 'http://new.peanuts.com' -StatusCode 'Temporary' -Reset
 
    Demonstrates how to reset the attributes for any parameter that isn't passed to its default value by using the
    `Reset` switch. In this example, the `exactDestination` and `childOnly` HTTP redirect attributes are deleted and
    reset to their default value because they aren't being passed as arguments.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The site where the redirection should be setup.
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [Alias('Path')]
        [String] $VirtualPath,

        # If true, enables HTTP redirect. Otherwise, disables it.
        [bool] $Enabled,

        # The destination to redirect to.
        [Parameter(Mandatory)]
        [String] $Destination,

        # The HTTP status code to use. Default is `Found` (`302`). Should be one of `Permanent` (`301`),
        # `Found` (`302`), `Temporary` (`307`), or `PermRedirect` (`308`). This is stored in IIS as a number.
        [Alias('StatusCode')]
        [CIisHttpRedirectResponseStatus] $HttpResponseStatus,

        # Redirect all requests to exact destination (instead of relative to destination).
        [bool] $ExactDestination,

        # Only redirect requests to content in site and/or path, but nothing below it.
        [bool] $ChildOnly,

        # If set, the HTTP redirect setting for each parameter *not* passed is deleted, which resets it to its default
        # value. Otherwise, HTTP redirect settings whose parameters are not passed are left in place and not modified.
        [switch] $Reset
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($VirtualPath)
    {
        Write-CIisWarningOnce -ForObsoleteSiteNameAndVirtualPathParameter
    }

    $attrs =
        $PSBoundParameters |
        Copy-Hashtable -Key @('enabled', 'destination', 'httpResponseStatus', 'exactDestination', 'childOnly')

    Set-CIisConfigurationAttribute -LocationPath ($LocationPath, $VirtualPath | Join-CIisPath) `
                                   -SectionPath 'system.webServer/httpRedirect' `
                                   -Attribute $attrs `
                                   -Reset:$Reset
}



function Set-CIisMimeMap
{
    <#
    .SYNOPSIS
    Creates or sets a file extension to MIME type map for an entire web server.
 
    .DESCRIPTION
    IIS won't serve static files unless they have an entry in the MIME map. Use this function to create/update a MIME map entry.
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .LINK
    Get-CIisMimeMap
 
    .LINK
    Remove-CIisMimeMap
 
    .EXAMPLE
    Set-CIisMimeMap -FileExtension '.m4v' -MimeType 'video/x-m4v'
 
    Adds a MIME map to all websites so that IIS will serve `.m4v` files as `video/x-m4v`.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='ForWebServer')]
    param(
        # The name of the website whose MIME type to set.
        [Parameter(Mandatory, ParameterSetName='ForWebsite', Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [Parameter(ParameterSetName='ForWebsite')]
        [String] $VirtualPath = '',

        # The file extension to set.
        [Parameter(Mandatory)]
        [String] $FileExtension,

        # The MIME type to serve the files as.
        [Parameter(Mandatory)]
        [String] $MimeType
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $getIisConfigSectionParams = @{ }
    if( $PSCmdlet.ParameterSetName -eq 'ForWebsite' )
    {
        $getIisConfigSectionParams['LocationPath'] = $LocationPath
        $getIisConfigSectionParams['VirtualPath'] = $VirtualPath
    }

    $staticContent =
        Get-CIisConfigurationSection -SectionPath 'system.webServer/staticContent' @getIisConfigSectionParams
    $mimeMapCollection = $staticContent.GetCollection()

    $mimeMap = $mimeMapCollection | Where-Object { $_['fileExtension'] -eq $FileExtension }

    if( $mimeMap )
    {
        $action = 'Set'
        $mimeMap['fileExtension'] = $FileExtension
        $mimeMap['mimeType'] = $MimeType
    }
    else
    {
        $action = 'Add'
        $mimeMap = $mimeMapCollection.CreateElement("mimeMap");
        $mimeMap["fileExtension"] = $FileExtension
        $mimeMap["mimeType"] = $MimeType
        [void] $mimeMapCollection.Add($mimeMap)
    }

    Save-CIisConfiguration -Target "IIS MIME Map for $($FileExtension) Files" -Action "$($action) MIME Type"
}




function Set-CIisWebsite
{
    <#
    .SYNOPSIS
    Configures an IIS website's settings.
 
    .DESCRIPTION
    The `Set-CIisWebsite` function configures an IIS website. Pass the name of the website to the `Name` parameter.
    Pass the website's ID to the `ID` parameter. If you want the server to not auto start, set `ServerAutoStart` to
    false: `-ServerAutoStart:$false` See [Site <site>](https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/sites/site/)
    for documentation on each setting.
 
    You can configure the IIS default website instead of a specific website by using the `AsDefaults` switch. Only the
    `serverAutoStart` setting can be set on IIS's default website settings.
 
    If any `ServerAutoStart` is not passed, it is not changed.
 
    If you use the `-Reset` switch and omit a `ServerAutoStart` argument, the `serverAutoStart` setting will be deleted,
    which will reset it to its default value.
 
    .LINK
    https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/sites/site/
 
    .EXAMPLE
    Set-CIisWebsite -SiteName 'ExampleTwo' -ID 53 -ServerAutoStart $false
 
    Demonstrates how to configure an IIS website's settings.
 
    .EXAMPLE
    Set-CIisWebsite -SiteName 'ExampleOne' -ID 53 -Reset
 
    Demonstrates how to set *all* an IIS website's settings by using the `-Reset` switch. In this example, the `id`
    setting is set to a custom value, and the `serverAutoStart` (the only other website setting) is deleted, which
    resets it to its default value.
 
    .EXAMPLE
    Set-CIisWebsite -AsDefaults -ServerAutoStart:$false
 
    Demonstrates how to configure the IIS default website's settings by using the `AsDefaults` switch and not passing
    the website name.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding(DefaultParameterSetName='SetInstance', SupportsShouldProcess)]
    param(
        # The name of the website whose settings to configure.
        [Parameter(Mandatory, ParameterSetName='SetInstance', Position=0)]
        [String] $Name,

        # If true, the function configures the IIS default website instead of a specific website.
        [Parameter(Mandatory, ParameterSetName='SetDefaults')]
        [switch] $AsDefaults,

        # Sets the IIS website's `id` setting. Can not be used when setting site defaults.
        [Parameter(ParameterSetName='SetInstance')]
        [UInt32] $ID,

        # Sets the IIS website's `serverAutoStart` setting.
        [bool] $ServerAutoStart,

        # If set, the website setting for each parameter *not* passed is deleted, which resets it to its default value.
        # Otherwise, website settings whose parameters are not passed are left in place and not modified.
        [switch] $Reset
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $target = Get-CIisWebsite -Name $Name -Defaults:$AsDefaults
    if( -not $target )
    {
        return
    }

    $attribute = @{}
    # Can't ever remove a site's ID, only change it (i.e. it must always be set to something). If user doesn't pass it,
    # set it to the website's current ID.
    if( -not $PSBoundParameters.ContainsKey('ID') -and ($target | Get-Member -Name 'Id') )
    {
        $attribute['ID'] = $target.Id
    }

    $targetMsg = 'IIS website defaults'
    if( $Name )
    {
        $targetMsg = "IIS website ""$($Name)"""
    }

    Invoke-SetConfigurationAttribute -ConfigurationElement $target `
                                     -PSCmdlet $PSCmdlet `
                                     -Target $targetMsg `
                                     -Exclude @('state') `
                                     -Attribute $attribute `
                                     -Reset:$Reset
}


function Set-CIisWebsiteID
{
    <#
    .SYNOPSIS
    Sets a website's ID to an explicit number.
    .DESCRIPTION
    IIS handles assigning websites individual IDs. This method will assign a website explicit ID you manage (e.g. to support session sharing in a web server farm).
    If another site already exists with that ID, you'll get an error.
    When you change a website's ID, IIS will stop the site, but not start the site after saving the ID change. This function waits until the site's ID is changed, and then will start the website.
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
    .EXAMPLE
    Set-CIisWebsiteID -SiteName Holodeck -ID 483
    Sets the `Holodeck` website's ID to `483`.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        # The website name.
        [String] $SiteName,

        # The website's new ID.
        [Parameter(Mandatory)]
        [int] $ID
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    "The $($PSCmdlet.MyInvocation.MyCommand.Name) function is obsolete. Use `Set-CIIsWebsite` instead." |
        Write-CIIsWarningOnce

    if( -not (Test-CIisWebsite -Name $SiteName) )
    {
        Write-Error ('Website {0} not found.' -f $SiteName)
        return
    }

    $websiteWithID = Get-CIisWebsite | Where-Object { $_.ID -eq $ID -and $_.Name -ne $SiteName }
    if( $websiteWithID )
    {
        Write-Error -Message ('ID {0} already in use for website {1}.' -f $ID,$SiteName) -Category ResourceExists
        return
    }

    $website = Get-CIisWebsite -SiteName $SiteName
    $startWhenDone = $false
    if( $website.ID -ne $ID )
    {
        if( $PSCmdlet.ShouldProcess( ('website {0}' -f $SiteName), ('set site ID to {0}' -f $ID) ) )
        {
            $startWhenDone = ($website.State -eq 'Started')
            $website.ID = $ID
            $website.CommitChanges()
        }
    }

    if( $PSBoundParameters.ContainsKey('WhatIf') )
    {
        return
    }

    # Make sure the website's ID gets updated
    $website = $null
    $maxTries = 100
    $numTries = 0
    do
    {
        Start-Sleep -Milliseconds 100
        $website = Get-CIisWebsite -SiteName $SiteName
        if( $website -and $website.ID -eq $ID )
        {
            break
        }
        $numTries++
    }
    while( $numTries -lt $maxTries )

    if( -not $website -or $website.ID -ne $ID )
    {
        Write-Error ('IIS:/{0}: site ID hasn''t changed to {1} after waiting 10 seconds. Please check IIS configuration.' -f $SiteName,$ID)
    }

    if( -not $startWhenDone )
    {
        return
    }

    # Now, start the website.
    $numTries = 0
    do
    {
        # Sometimes, the website is invalid and Start() throws an exception.
        try
        {
            if( $website )
            {
                $null = $website.Start()
            }
        }
        catch
        {
            $website = $null
        }

        Start-Sleep -Milliseconds 100
        $website = Get-CIisWebsite -SiteName $SiteName
        if( $website -and $website.State -eq 'Started' )
        {
            break
        }
        $numTries++
    }
    while( $numTries -lt $maxTries )

    if( -not $website -or $website.State -ne 'Started' )
    {
        Write-Error ('IIS:/{0}: failed to start website after setting ID to {1}' -f $SiteName,$ID)
    }
}


function Set-CIisWebsiteLimit
{
    <#
    .SYNOPSIS
    Configures an IIS website's limits settings.
 
    .DESCRIPTION
    The `Set-CIisWebsiteLimit` function configures an IIS website's limits settings. Pass the name of the website to the
     `SiteName` parameter. Pass the limits configuration to one or more of the ConnectionTimeout, MaxBandwidth,
     MaxConnections, and/or MaxUrlSegments parameters. See
    [Limits for a Web Site <limits>](https://learn.microsoft.com/en-us/iis/configuration/system.applicationhost/sites/site/limits)
    for documentation on each setting.
 
    You can configure the IIS default website instead of a specific website by using the
    `AsDefaults` switch.
 
    If the `Reset` switch is set, each setting *not* passed as a parameter is deleted, which resets it to its default
    value.
 
    .LINK
    https://learn.microsoft.com/en-us/iis/configuration/system.applicationhost/sites/site/limits
 
    .EXAMPLE
    Set-CIisWebsiteLimit -SiteName 'ExampleTwo' -ConnectionTimeout '00:01:00' -MaxBandwidth 2147483647 -MaxConnections 2147483647 -MaxUrlSegments 16
 
    Demonstrates how to configure all an IIS website's limits settings.
 
    .EXAMPLE
    Set-CIisWebsiteLimit -SiteName 'ExampleOne' -ConnectionTimeout 1073741823 -Reset
 
    Demonstrates how to set *all* an IIS website's limits settings (even if not passing all parameters) by using the
    `-Reset` switch. In this example, the `connectionTimeout` setting is set to `1073741823` and all other settings
    (`maxBandwidth`, `maxConnections`, and `maxUrlSegments`) are deleted, which resets them to their default values.
 
    .EXAMPLE
    Set-CIisWebsiteLimit -AsDefaults -ConnectionTimeout 536870911
 
    Demonstrates how to configure the IIS website defaults limits settings by using the `AsDefaults` switch and not
    passing the website name.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding(DefaultParameterSetName='SetInstance', SupportsShouldProcess)]
    param(
        # The name of the website whose limits settings to configure.
        [Parameter(Mandatory, ParameterSetName='SetInstance', Position=0)]
        [String] $SiteName,

        # If true, the function configures the IIS default website instead of a specific website.
        [Parameter(Mandatory, ParameterSetName='SetDefaults')]
        [switch] $AsDefaults,

        # Sets the IIS website's limits `connectionTimeout` setting.
        [TimeSpan] $ConnectionTimeout,

        # Sets the IIS website's limits `maxBandwidth` setting.
        [UInt32] $MaxBandwidth,

        # Sets the IIS website's limits `maxConnections` setting.
        [UInt32] $MaxConnections,

        # Sets the IIS website's limits `maxUrlSegments` setting.
        [UInt32] $MaxUrlSegments,

        # If set, each website limits setting *not* passed as a parameter is deleted, which resets it to its default
        # value.
        [switch] $Reset
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $target = Get-CIisWebsite -Name $SiteName -Defaults:$AsDefaults
    if( -not $target )
    {
        return
    }

    $targetMsg = 'default IIS website limits'
    if( $SiteName )
    {
        $targetMsg = """$($SiteName)"" IIS website's limits"
    }

    Invoke-SetConfigurationAttribute -ConfigurationElement $target.limits `
                                     -PSCmdlet $PSCmdlet `
                                     -Target $targetMsg `
                                     -Reset:$Reset
}



function Set-CIisWebsiteLogFile
{
    <#
    .SYNOPSIS
    Configures an IIS website's log file settings.
 
    .DESCRIPTION
    The `Set-CIisWebsiteLogFile` function configures an IIS website's log files settings. Pass the name of the
    website to the `SiteName` parameter. Pass the log files configuration you want to the `CustomLogPluginClsid`,
    `Directory`, `Enabled`, `FlushByEntryCountW3CLog`, `LocalTimeRollover`, `LogExtFileFlags`, `LogFormat`, `LogSiteID`,
    `LogTargetW3C`, `MaxLogLineLength`, `Period`, and `TruncateSize` parameters (see
    [Log Files for a Web Site <logFile>](https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/sites/site/logfile/))
    for documentation on what these settings are for.
 
    If you want to ensure that any settings that may have gotten changed by hand are reset to their default values, use
    the `-Reset` switch. When set, the `-Reset` switch will reset each setting not passed as an argument to its default
    value.
 
    .LINK
    https://docs.microsoft.com/en-us/iis/configuration/system.applicationhost/sites/site/logfile/
 
    .EXAMPLE
    Set-CIisWebsiteLogFile -AppPoolName 'Snafu' -Directory 'C:\logs' -MaxLogLineLength 32768
 
    Demonstrates how to configure an IIS website's log file settings. In this example, `directory` will be set to
    `C:\logs` and `maxLogLineLength` will be set to `32768`. All other settings are unchanged.
 
    .EXAMPLE
    Set-CIisWebsiteLogFile -AppPoolName 'Snafu' -Directory 'C:\logs' -MaxLogLineLength 32768 -Reset
 
    Demonstrates how to set *all* an IIS website's log file settings by using the `-Reset` switch. In this example, the
    `directory` and `maxLogLineLength` settings are set to custom values, and all other settings are deleted, which
    resets them to their default values.
 
    .EXAMPLE
    Set-CIisWebsiteLogFile -AsDefaults -Directory 'C:\logs' -MaxLogLineLength 32768
 
    Demonstrates how to configure the IIS website defaults log file settings by using the `AsDefaults` switch and not
    passing the website name.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding(DefaultParameterSetName='SetInstance', SupportsShouldProcess)]
    param(
        # The name of the website whose log file settings to set.
        [Parameter(Mandatory, ParameterSetName='SetInstance', Position=0)]
        [String] $SiteName,

        # If true, the function configures IIS's application pool defaults instead of a specific application pool.
        [Parameter(Mandatory, ParameterSetName='SetDefaults')]
        [switch] $AsDefaults,

        # Sets the IIS website's log files `customLogPluginClsid` setting.
        [String] $CustomLogPluginClsid,

        # Sets the IIS website's log files `directory` setting.
        [String] $Directory,

        # Sets the IIS website's log files `enabled` setting.
        [bool] $Enabled,

        # Sets the IIS website's log files `flushByEntryCountW3CLog` setting.
        [UInt32] $FlushByEntryCountW3CLog,

        # Sets the IIS website's log files `localTimeRollover` setting.
        [bool] $LocalTimeRollover,

        # Sets the IIS website's log files `logExtFileFlags` setting.
        [LogExtFileFlags] $LogExtFileFlags,

        # Sets the IIS website's log files `logFormat` setting.
        [LogFormat] $LogFormat,

        # Sets the IIS website's log files `logSiteID` setting.
        [bool] $LogSiteID,

        # Sets the IIS website's log files `logTargetW3C` setting.
        [LogTargetW3C] $LogTargetW3C,

        # Sets the IIS website's log files `maxLogLineLength` setting.
        [UInt32] $MaxLogLineLength,

        # Sets the IIS website's log files `period` setting.
        [LoggingRolloverPeriod] $Period,

        # Sets the IIS website's log files `truncateSize` setting.
        [Int64] $TruncateSize,

        # If set, the website log file setting for each parameter *not* passed is deleted, which resets it to its
        # default value. By default, website log file settings whose parameters are not passed are left in place and not
        # modified.
        [switch] $Reset
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $site = Get-CIisWebsite -Name $SiteName -Defaults:$AsDefaults
    if( -not $site )
    {
        return
    }

    $targetMsg = "IIS website defaults log file"
    if( $SiteName )
    {
        $targetMsg = """$($SiteName)"" IIS website's log file"
    }

    Invoke-SetConfigurationAttribute -ConfigurationElement $site.LogFile `
                                     -PSCmdlet $PSCmdlet `
                                     -Target $targetMsg `
                                     -Reset:$Reset
}



function Set-CIisWebsiteSslCertificate
{
    <#
    .SYNOPSIS
    Sets a website's SSL certificate.
 
    .DESCRIPTION
    SSL won't work on a website if an SSL certificate hasn't been bound to all the IP addresses it's listening on. This function binds a certificate to all a website's IP addresses. Make sure you call this method *after* you create a website's bindings. Any previous SSL bindings on those IP addresses are deleted.
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .EXAMPLE
    Set-CIisWebsiteSslCertificate -SiteName Peanuts -Thumbprint 'a909502dd82ae41433e6f83886b00d4277a32a7b' -ApplicationID $PeanutsAppID
 
    Binds the certificate whose thumbprint is `a909502dd82ae41433e6f83886b00d4277a32a7b` to the `Peanuts` website. It's a good idea to re-use the same GUID for each distinct application.
    #>

    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The name of the website whose SSL certificate is being set.
        $SiteName,

        [Parameter(Mandatory=$true)]
        [string]
        # The thumbprint of the SSL certificate to use.
        $Thumbprint,

        [Parameter(Mandatory=$true)]
        [Guid]
        # A GUID that uniquely identifies this website. Create your own.
        $ApplicationID
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $site = Get-CIisWebsite -Name $SiteName
    if( -not $site )
    {
        return
    }

    $site.Bindings | Where-Object { $_.Protocol -eq 'https' } | ForEach-Object {
        $installArgs = @{ }
        if( $_.Endpoint.Address -ne '0.0.0.0' )
        {
            $installArgs.IPAddress = $_.Endpoint.Address.ToString()
        }
        if( $_.Endpoint.Port -ne '*' )
        {
            $installArgs.Port = $_.Endpoint.Port
        }
        Set-CSslCertificateBinding @installArgs -ApplicationID $ApplicationID -Thumbprint $Thumbprint
    }
}




function Set-CIisWindowsAuthentication
{
    <#
    .SYNOPSIS
    Configures the settings for Windows authentication.
 
    .DESCRIPTION
    By default, configures Windows authentication on a website. You can configure Windows authentication at a specific
    path under a website by passing the virtual path (*not* the physical path) to that directory.
 
    The changes only take effect if Windows authentication is enabled (see `Enable-CIisSecurityAuthentication`).
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .LINK
    http://blogs.msdn.com/b/webtopics/archive/2009/01/19/service-principal-name-spn-checklist-for-kerberos-authentication-with-iis-7-0.aspx
 
    .LINK
    Disable-CIisSecurityAuthentication
 
    .LINK
    Enable-CIisSecurityAuthentication
 
    .EXAMPLE
    Set-CIisWindowsAuthentication -LocationPath 'Peanuts/Snoopy/DogHouse' -UseKernelMode $false
 
    Configures Windows authentication on the `Snoopy/Doghouse` directory of the `Peanuts` site to not use kernel mode.
 
    .EXAMPLE
    Set-CIisWindowsAuthentication -LocationPath 'Peanuts' -Reset
 
    Configures Windows authentication on the `Peanuts` website to not use the default kernel mode because the `Reset`
    switch is given and the `UseKernelMode` parameter is not.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='New')]
    param(
        # The site where Windows authentication should be set.
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [Alias('Path')]
        [String] $VirtualPath = '',

        # The value for the `authPersistNonNtlm` setting.
        [bool] $AuthPersistNonNtlm,

        # The value for the `authPersistSingleRequest` setting.
        [bool] $AuthPersistSingleRequest,

        # Enable Windows authentication. To disable Windows authentication you must explicitly set `Enabled` to
        # `$false`, e.g. `-Enabled $false`.
        [bool] $Enabled,

        # The value for the `useAppPoolCredentials` setting.
        [bool] $UseAppPoolCredentials,

        # The value for the `useKernelMode` setting.
        [Parameter(ParameterSetName='New')]
        [bool] $UseKernelMode,

        # OBSOLETE. Use the `UseKernelMode` parameter instead.
        [Parameter(ParameterSetName='Deprecated')]
        [switch] $DisableKernelMode,

        # If set, the anonymous authentication setting for each parameter *not* passed is deleted, which resets it to
        # its default value. Otherwise, anonymous authentication settings whose parameters are not passed are left in
        # place and not modified.
        [switch] $Reset
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $attrs = @{}

    $settingNames = @(
        'authPersistNonNtlm',
        'authPersistSingleRequest',
        'enabled',
        'useAppPoolCredentials',
        'useKernelMode'
    )
    $attrs = $PSBoundParameters | Copy-Hashtable -Key $settingNames

    if ($PSCmdlet.ParameterSetName -eq 'Deprecated')
    {
        "The $($PSCmdlet.MyInvocation.MyCommand.Name) function's ""DisableKernelMode"" switch is obsolete and will " +
        'be removed in the next major version of Carbon.IIS. Use the new `UseKernelMode` parameter instead.' |
            Write-CIisWarningOnce

        $attrs['useKernelMode'] = -not $DisableKernelMode.IsPresent
    }

    if ($VirtualPath)
    {
        Write-CIisWarningOnce -ForObsoleteSiteNameAndVirtualPathParameter
    }

    $sectionPath = 'system.webServer/security/authentication/windowsAuthentication'
    Set-CIisConfigurationAttribute -LocationPath ($LocationPath, $VirtualPath | Join-CIisPath) `
                                   -SectionPath $sectionPath `
                                   -Attribute $attrs `
                                   -Reset:$Reset
}



function Split-CIisLocationPath
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [String[]] $VirtualPath
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        if (-not $VirtualPath)
        {
            return
        }

        return ($VirtualPath | ConvertTo-CIisVirtualPath -NoLeadingSlash).Split('/', 2)
    }

}


function Start-CIisAppPool
{
    <#
    .SYNOPSIS
    Starts IIS application pools.
 
    .DESCRIPTION
    The `Start-CIisAppPool` starts IIS application pools. Pass the names of the application pools to the `Name`
    parameter, or pipe application pool objects or application pool names to `Start-CIisAppPool`. The function then
    starts the application pool and waits 30 seconds for the application pool to report that it has started. You can
    change the amount of time it waits with the `Timeout` parameter. If the application pool doesn't start before the
    timeout expires, the function writes an error.
 
    .EXAMPLE
    Start-CIisAppPool -Name 'Default App Pool'
 
    Demonstrates how to start an application pool by passing its name to the `Name` parameter.
 
    .EXAMPLE
    Start-CIisAppPool -Name 'Default App Pool', 'Non-default App Pool'
 
    Demonstrates how to start multiple application pools by passing their names to the `Name` parameter.
 
    .EXAMPLE
    Get-CIisAppPool | Start-CIisAppPool
 
    Demonstrates how to start an application pool by piping it to `Start-CIisAppPool`.
 
    .EXAMPLE
    'Default App Pool', 'Non-default App Pool' | Start-CIisAppPool
 
    Demonstrates how to start one or more application pools by piping their names to `Start-CIisAppPool`.
 
    .EXAMPLE
    Start-CIisAppPool -Name 'Default App Pool' -Timeout '00:00:10'
 
    Demonstrates how to change the amount of time `Start-CIisAppPool` waits for the application pool to start. In this
    example, it will wait 10 seconds.
    #>

    param(
        # One or more names of the application pools to start. You can also pipe one or more names to the function or
        # pipe one or more application pool objects.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [String[]] $Name,

        # The amount of time `Start-CIisAppPool` waits for an application pool to start before giving up and writing
        # an error. The default is 30 seconds. This doesn't mean the application pool actually has running worker
        # processes, just that it is reporting that is is started and available.
        [TimeSpan] $Timeout = [TimeSpan]::New(0, 0, 30)
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        $appPools = $Name | ForEach-Object { Get-CIisAppPool -Name $_ }
        if (-not $appPools)
        {
            return
        }

        $timer = [Diagnostics.Stopwatch]::New()

        foreach ($appPool in $appPools)
        {
            if ($appPool.State -eq [ObjectState]::Started)
            {
                continue
            }

            Write-Information "Starting IIS application pool ""$($appPool.Name)""."
            $state = $null
            $timer.Restart()
            $lastError = $null
            $numErrorsAtStart = $Global:Error.Count
            while ($null -eq $state -and $timer.Elapsed -lt $Timeout)
            {
                try
                {
                    $state = $appPool.Start()
                }
                catch
                {
                    $lastError = $_
                    Start-Sleep -Milliseconds 100
                    $appPool = Get-CIisAppPool -Name $appPool.Name
                }
            }

            if ($null -eq $state)
            {
                $msg = "Starting IIS application pool ""$($appPool.Name)"" threw an exception: $($lastError)."
                Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                continue
            }
            else
            {
                # Application pool started successfully, so remove the errors.
                $numErrorsToRemove = $Global:Error.Count - $numErrorsAtStart
                for ($idx = 0; $idx -lt $numErrorsToRemove; ++$idx)
                {
                    $Global:Error.RemoveAt(0)
                }
            }

            if ($state -eq [ObjectState]::Started)
            {
                continue
            }

            while ($true)
            {
                if ($timer.Elapsed -gt $Timeout)
                {
                    $msg = "IIS application pool ""$($appPool.Name)"" failed to start in less than $($Timeout)."
                    Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                    break
                }

                $appPool = Get-CIisAppPool -Name $appPool.Name
                if ($appPool.State -eq [ObjectState]::Started)
                {
                    break
                }

                Start-Sleep -Milliseconds 100
            }
        }
    }
}



function Start-CIisWebsite
{
    <#
    .SYNOPSIS
    Starts IIS websites.
 
    .DESCRIPTION
    The `Start-CIisWebsite` starts IIS websites. Pass the names of the websites to the `Name` parameter, or pipe website
    objects or website names to `Start-CIisWebsite`. The function then starts the website and waits 30 seconds for the
    website to report that it has started. You can change the amount of time it waits with the `Timeout` parameter. If
    the website doesn't start before the timeout expires, the function writes an error.
 
    .EXAMPLE
    Start-CIisWebsite -Name 'Default Website'
 
    Demonstrates how to start a website by passing its name to the `Name` parameter.
 
    .EXAMPLE
    Start-CIisWebsite -Name 'Default Website', 'Non-default Website'
 
    Demonstrates how to start multiple websites by passing their names to the `Name` parameter.
 
    .EXAMPLE
    Get-CIisWebsite | Start-CIisWebsite
 
    Demonstrates how to start a website by piping it to `Start-CIisWebsite`.
 
    .EXAMPLE
    'Default Website', 'Non-default Website' | Start-CIisWebsite
 
    Demonstrates how to start one or more websites by piping their names to `Start-CIisWebsite`.
 
    .EXAMPLE
    Start-CIisWebsite -Name 'Default Website' -Timeout '00:00:10'
 
    Demonstrates how to change the amount of time `Start-CIisWebsite` waits for the website to start. In this
    example, it will wait 10 seconds.
    #>

    param(
        # One or more names of the websites to start. You can also pipe one or more names to the function or
        # pipe one or more website objects.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [String[]] $Name,

        # The amount of time `Start-CIisWebsite` waits for a website to start before giving up and writing
        # an error. The default is 30 seconds. This doesn't mean the website actually has running worker
        # processes, just that it is reporting that is is started and available.
        [TimeSpan] $Timeout = [TimeSpan]::New(0, 0, 30)
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        $websites = $Name | ForEach-Object { Get-CIisWebsite -Name $_ }
        if (-not $websites)
        {
            return
        }

        $timer = [Diagnostics.Stopwatch]::New()

        foreach ($website in $websites)
        {
            if ($website.State -eq [ObjectState]::Started)
            {
                continue
            }

            $siteAppPoolName =
                $website.Applications |
                Where-Object 'Path' -eq '/' |
                Select-Object -ExpandProperty 'ApplicationPoolName'
            if (-not (Test-CIisAppPool -Name $siteAppPoolName))
            {
                $msg = "Unable to start website ""$($website.Name)"" because its application pool, " +
                       """$($siteAppPoolName)"", does not exist."
                Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                continue
            }

            Write-Information "Starting IIS website ""$($website.Name)""."
            $state = $null
            $lastError = $null
            $timer.Restart()
            $numErrorsAtStart = $Global:Error.Count
            while ($null -eq $state -and $timer.Elapsed -lt $Timeout)
            {
                try
                {
                    $state = $website.Start()
                }
                catch
                {
                    $lastError = $_
                    Start-Sleep -Milliseconds 100
                    $website = Get-CIisWebsite -Name $website.Name
                }
            }

            if ($null -eq $state)
            {
                $msg = "Starting IIS website ""$($website.Name)"" threw an exception: $($lastError)."
                Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                continue
            }
            else
            {
                # Site started successfully, so remove the errors.
                $numErrorsToRemove = $Global:Error.Count - $numErrorsAtStart
                for ($idx = 0; $idx -lt $numErrorsToRemove; ++$idx)
                {
                    $Global:Error.RemoveAt(0)
                }
            }

            if ($state -eq [ObjectState]::Started)
            {
                continue
            }

            while ($true)
            {
                $website = Get-CIisWebsite -Name $website.Name
                if ($website.State -eq [ObjectState]::Started)
                {
                    break
                }

                if ($timer.Elapsed -gt $Timeout)
                {
                    $msg = "IIS website ""$($website.Name)"" failed to start in less than $($Timeout)."
                    Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                    break
                }

                Start-Sleep -Milliseconds 100
            }
        }
    }
}



function Stop-CIisAppPool
{
    <#
    .SYNOPSIS
    Stops an IIS application pool.
 
    .DESCRIPTION
    The `Stop-CIisAppPool` stops an IIS application pool. Pass the names of the application pools to the `Name`
    parameter, or pipe application pool objects or application pool names to `Stop-CIisAppPool`. The function will
    stop the application pool, then waits 30 seconds for it to stop (you can control this wait period with the
    `Timeout` parameter). If the application pool hasn't stopped, the function writes an error, and returns.
 
    You can use the `Force` (switch) to indicate to `Stop-CIisAppPool` that it should attempt to kill/stop any of the
    application pool's worker processes if the application pool doesn't stop before the timeout completes. If killing
    the worker processes fails, the function writes an error.
 
    This function disposes the current server manager object that Carbon.IIS uses internally. Make sure you have no
    pending, unsaved changes when calling `Stop-CIisAppPool`.
 
    .EXAMPLE
    Stop-CIisAppPool -Name 'Default App Pool'
 
    Demonstrates how to stop an application pool by passing its name to the `Name` parameter.
 
    .EXAMPLE
    Stop-CIisAppPool -Name 'Default App Pool', 'Non-default App Pool'
 
    Demonstrates how to stop multiple application pools by passing their names to the `Name` parameter.
 
    .EXAMPLE
    Get-CIisAppPool | Stop-CIisAppPool
 
    Demonstrates how to stop an application pool by piping it to `Stop-CIisAppPool`.
 
    .EXAMPLE
    'Default App Pool', 'Non-default App Pool' | Stop-CIisAppPool
 
    Demonstrates how to stop one or more application pools by piping their names to `Stop-CIisAppPool`.
 
    .EXAMPLE
    Stop-CIisAppPool -Name 'Default App Pool' -Timeout '00:00:10'
 
    Demonstrates how to change the amount of time `Stop-CIisAppPool` waits for the application pool to stop. In this
    example, it will wait 10 seconds.
 
    .EXAMPLE
    Stop-CIisAppPool -Name 'Default App Pool' -Force
 
    Demonstrates how to stop an application pool that won't stop by using the `Force` (switch). After waiting for the
    application pool to stop, if it is still running and the `Force` (switch) is used, `Stop-CIisAppPool` will
    try to kill the application pool's worker processes.
    #>

    [CmdletBinding()]
    param(
        # One or more names of the application pools to stop. You can also pipe one or more names to the function or
        # pipe one or more application pool objects.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [String[]] $Name,

        # The amount of time `Stop-CIisAppPool` waits for an application pool to stop before giving up and writing
        # an error. The default is 30 seconds.
        [TimeSpan] $Timeout = [TimeSpan]::New(0, 0, 30),

        # If set, and an application pool fails to stop on its own, `Stop-CIisAppPool` will attempt to kill the
        # application pool worker processes.
        [switch] $Force
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        $appPools = $Name | ForEach-Object { Get-CIisAppPool -Name $_ }
        if (-not $appPools)
        {
            return
        }

        $timer = [Diagnostics.Stopwatch]::New()

        foreach ($appPool in $appPools)
        {
            if ($appPool.State -eq [ObjectState]::Stopped)
            {
                continue
            }

            Write-Information "Stopping IIS application pool ""$($appPool.Name)""."
            $state = $null
            $lastError = $null
            $timer.Restart()
            $numErrors = $Global:Error.Count
            while ($null -eq $state -and $timer.Elapsed -lt $Timeout)
            {
                try
                {
                    $state = $appPool.Stop()
                }
                catch
                {
                    $lastError = $_
                    Start-Sleep -Milliseconds 100
                    $appPool = Get-CIisAppPool -Name $appPool.Name
                }
            }

            if ($null -eq $state)
            {
                $msg = "Exception stopping IIS application pool ""$($appPool.Name)"": $($lastError)"
                Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                continue
            }

            # Clear any errors that occurred since the app pool eventually stopped.
            for ($idx = $numErrors; $idx -lt $Global:Error.Count; ++$idx)
            {
                $Global:Error.RemoveAt(0)
            }

            if ($state -eq [ObjectState]::Stopped)
            {
                continue
            }

            while ($true)
            {
                $appPool = Get-CIisAppPool -Name $appPool.Name
                if ($appPool.State -eq [ObjectState]::Stopped)
                {
                    break
                }

                if ($timer.Elapsed -gt $Timeout)
                {
                    if ($Force)
                    {
                        $appPool = Get-CIisAppPool -Name $appPool.Name

                        foreach ($wp in $appPool.WorkerProcesses)
                        {
                            $msg = "IIS application pool ""$($appPool.Name)"" failed to stop in less than " +
                                   "$($Timeout): forcefully stopping worker process $($wp.ProcessId)."
                            Write-Warning $msg
                            Stop-Process -id $wp.ProcessId -Force -ErrorAction Ignore

                            $timer.Restart()
                            while ($true)
                            {
                                if (-not (Get-Process -Id $wp.ProcessId -ErrorAction Ignore))
                                {
                                    break
                                }

                                if ($timer.Elapsed -gt $Timeout)
                                {
                                    $msg = "IIS application pool ""$($appPool.Name)"" failed to stop in less than " +
                                           "$($Timeout) and its worker process $($wp.ProcessId) also failed to stop " +
                                           "in less than $($Timeout)."
                                    Write-Error -Message $msg
                                    break
                                }

                                Start-Sleep -Milliseconds 100
                            }
                        }
                        break
                    }

                    $msg = "IIS application pool ""$($appPool.Name)"" failed to stop in ""$($Timeout)""."
                    Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                    break
                }

                Start-Sleep -Milliseconds 100
            }
        }
    }
}



function Stop-CIisWebsite
{
    <#
    .SYNOPSIS
    Stops an IIS website.
 
    .DESCRIPTION
    The `Stop-CIisWebsite` stops an IIS website. Pass the names of the websites to the `Name` parameter, or pipe website
    objects or website names to `Stop-CIisWebsite`. The function will stop the website, then waits 30 seconds for it to
    stop (you can control this wait period with the `Timeout` parameter). If the website hasn't stopped, the function
    writes an error, and returns.
 
    .EXAMPLE
    Stop-CIisWebsite -Name 'Default Website'
 
    Demonstrates how to stop a website by passing its name to the `Name` parameter.
 
    .EXAMPLE
    Stop-CIisWebsite -Name 'Default Website', 'Non-default Website'
 
    Demonstrates how to stop multiple websites by passing their names to the `Name` parameter.
 
    .EXAMPLE
    Get-CIisWebsite | Stop-CIisWebsite
 
    Demonstrates how to stop a website by piping it to `Stop-CIisWebsite`.
 
    .EXAMPLE
    'Default Website', 'Non-default Website' | Stop-CIisWebsite
 
    Demonstrates how to stop one or more websites by piping their names to `Stop-CIisWebsite`.
 
    .EXAMPLE
    Stop-CIisWebsite -Name 'Default Website' -Timeout '00:00:10'
 
    Demonstrates how to change the amount of time `Stop-CIisWebsite` waits for the website to stop. In this
    example, it will wait 10 seconds.
    #>

    [CmdletBinding()]
    param(
        # One or more names of the websites to stop. You can also pipe one or more names to the function or
        # pipe one or more website objects.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [String[]] $Name,

        # The amount of time `Stop-CIisWebsite` waits for a website to stop before giving up and writing
        # an error. The default is 30 seconds.
        [TimeSpan] $Timeout = [TimeSpan]::New(0, 0, 30)
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        $websites = $Name | ForEach-Object { Get-CIisWebsite -Name $_ }
        if (-not $websites)
        {
            return
        }

        $timer = [Diagnostics.Stopwatch]::New()

        foreach ($website in $websites)
        {
            if ($website.State -eq [ObjectState]::Stopped)
            {
                continue
            }

            Write-Information "Stopping IIS website ""$($website.Name)""."
            $state = $null
            $lastError = $null
            $timer.Restart()
            $numErrorsAtStart = $Global:Error.Count
            while ($null -eq $state -and $timer.Elapsed -lt $Timeout)
            {
                try
                {
                    $state = $website.Stop()
                }
                catch
                {
                    $lastError = $_
                    Start-Sleep -Milliseconds 100
                    $website = Get-CIisWebsite -Name $website.Name
                }
            }

            if ($null -eq $state)
            {
                $msg = "Failed to stop IIS website ""$($website.Name)"": $($lastError)"
                Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                continue
            }
            else
            {
                # Site stopped successfully, so remove the errors.
                $numErrorsToRemove = $Global:Error.Count - $numErrorsAtStart
                for ($idx = 0; $idx -lt $numErrorsToRemove; ++$idx)
                {
                    $Global:Error.RemoveAt(0)
                }
            }

            if ($state -eq [ObjectState]::Stopped)
            {
                continue
            }

            while ($true)
            {
                $website = Get-CIisWebsite -Name $website.Name
                if ($website.State -eq [ObjectState]::Stopped)
                {
                    break
                }

                if ($timer.Elapsed -gt $Timeout)
                {
                    $msg = "IIS website ""$($website.Name)"" failed to stop in ""$($Timeout)""."
                    Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                    break
                }

                Start-Sleep -Milliseconds 100
            }
        }
    }
}



function Test-CIisAppPool
{
    <#
    .SYNOPSIS
    Checks if an app pool exists.
 
    .DESCRIPTION
    Returns `True` if an app pool with `Name` exists. `False` if it doesn't exist.
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .EXAMPLE
    Test-CIisAppPool -Name Peanuts
 
    Returns `True` if the Peanuts app pool exists, `False` if it doesn't.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        # The name of the app pool.
        $Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $appPool = Get-CIisAppPool -Name $Name -ErrorAction Ignore
    if( $appPool )
    {
        return $true
    }

    return $false
}



function Test-CIisConfigurationSection
{
    <#
    .SYNOPSIS
    Tests a configuration section.
 
    .DESCRIPTION
    You can test if a configuration section exists or wheter it is locked.
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .OUTPUTS
    System.Boolean.
 
    .EXAMPLE
    Test-CIisConfigurationSection -SectionPath 'system.webServer/I/Do/Not/Exist'
 
    Tests if a configuration section exists. Returns `False`, because the given configuration section doesn't exist.
 
    .EXAMPLE
    Test-CIisConfigurationSection -SectionPath 'system.webServer/cgi' -Locked
 
    Returns `True` if the global CGI section is locked. Otherwise `False`.
 
    .EXAMPLE
    Test-CIisConfigurationSection -SectionPath 'system.webServer/security/authentication/basicAuthentication' -SiteName `Peanuts` -VirtualPath 'SopwithCamel' -Locked
 
    Returns `True` if the `Peanuts` website's `SopwithCamel` sub-directory's `basicAuthentication` security authentication section is locked. Otherwise, returns `False`.
    #>

    [CmdletBinding(DefaultParameterSetName='CheckExists')]
    param(
        [Parameter(Mandatory)]
        # The path to the section to test.
        [String] $SectionPath,

        # The name of the site whose configuration section to test. Optional. The default is the global configuration.
        [Parameter(Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [Alias('Path')]
        [String] $VirtualPath,

        # Test if the configuration section is locked.
        [Parameter(Mandatory, ParameterSetName='CheckLocked')]
        [switch] $Locked
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $getArgs = @{}
    if ($LocationPath)
    {
        $getArgs['LocationPath'] = $LocationPath
        $getArgs['VirtualPath'] = $VirtualPath
    }

    $section = Get-CIisConfigurationSection -SectionPath $SectionPath @getArgs -ErrorAction SilentlyContinue

    if( $PSCmdlet.ParameterSetName -eq 'CheckExists' )
    {
        if( $section )
        {
            return $true
        }
        else
        {
            return $false
        }
    }

    if( -not $section )
    {
        if ($VirtualPath)
        {
            $LocationPath = Join-CIisPath -Path $LocationPath, $VirtualPath
        }
        Write-Error "IIS:$($LocationPath): section $($SectionPath) not found." -ErrorAction $ErrorActionPreference
        return
    }

    if( $PSCmdlet.ParameterSetName -eq 'CheckLocked' )
    {
        return $section.OverrideMode -eq 'Deny'
    }
}




function Test-CIisSecurityAuthentication
{
    <#
    .SYNOPSIS
    Tests if IIS authentication types are enabled or disabled on a site and/or virtual directory under that site.
 
    .DESCRIPTION
    You can check if anonymous, basic, or Windows authentication are enabled. There are switches for each authentication type.
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .OUTPUTS
    System.Boolean.
 
    .EXAMPLE
    Test-CIisSecurityAuthentication -SiteName Peanuts -Anonymous
 
    Returns `true` if anonymous authentication is enabled for the `Peanuts` site. `False` if it isn't.
 
    .EXAMPLE
    Test-CIisSecurityAuthentication -SiteName Peanuts -VirtualPath Doghouse -Basic
 
    Returns `true` if basic authentication is enabled for`Doghouse` directory under the `Peanuts` site. `False` if it isn't.
    #>

    [CmdletBinding()]
    param(
        # The site where anonymous authentication should be set.
        [Parameter(Mandatory, Position=0)]
        [Alias('SiteName')]
        [String] $LocationPath,

        # OBSOLETE. Use the LocationPath parameter instead.
        [Alias('Path')]
        [String] $VirtualPath,

        # Tests if anonymous authentication is enabled.
        [Parameter(Mandatory, ParameterSetName='Anonymous')]
        [switch] $Anonymous,

        # Tests if basic authentication is enabled.
        [Parameter(Mandatory, ParameterSetName='Basic')]
        [switch] $Basic,

        # Tests if digest authentication is enabled.
        [Parameter(Mandatory, ParameterSetName='Digest')]
        [switch] $Digest,

        # Tests if Windows authentication is enabled.
        [Parameter(Mandatory, ParameterSetName='Windows')]
        [switch] $Windows
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $getConfigArgs = @{ $PSCmdlet.ParameterSetName = $true }
    $authSettings =
        Get-CIisSecurityAuthentication -LocationPath (Join-CIisPath -Path $LocationPath, $VirtualPath) `
                                       @getConfigArgs
    return ($authSettings.GetAttributeValue('enabled') -eq 'true')
}




function Test-CIisWebsite
{
    <#
    .SYNOPSIS
    Tests if a website exists.
 
    .DESCRIPTION
    Returns `True` if a website with name `Name` exists. `False` if it doesn't.
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .EXAMPLE
    Test-CIisWebsite -Name 'Peanuts'
 
    Returns `True` if the `Peanuts` website exists. `False` if it doesn't.
    #>

    [CmdletBinding()]
    param(
        # The name of the website whose existence to check. Wildcards supported.
        [Parameter(Mandatory)]
        [String] $Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $site = Get-CIisWebsite -Name $Name -ErrorAction Ignore
    if( $site )
    {
        return $true
    }
    return $false
}



function Uninstall-CIisAppPool
{
    <#
    .SYNOPSIS
    Removes an IIS application pool.
 
    .DESCRIPTION
    If the app pool doesn't exist, nothing happens.
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .EXAMPLE
    Uninstall-CIisAppPool -Name Batcave
 
    Removes/uninstalls the `Batcave` app pool.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of the app pool to remove.
        [Parameter(Mandatory)]
        [String] $Name
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $appPool = Get-CIisAppPool -Name $Name -ErrorAction Ignore
    if( -not $appPool )
    {
        return
    }

    $target = "IIS Application Pool $($Name)"
    if ($PSCmdlet.ShouldProcess($target, 'Stop'))
    {
        # Stop the app pool first, otherwise it can sometimes still be running after this function returns.
        Stop-CIisAppPool -Name $Name
    }

    $appPool = Get-CIisAppPool -Name $Name
    if ($PSCmdlet.ShouldProcess($target, 'Remove'))
    {
        Write-Information -Message "Removing IIS application pool ""$($Name)""."
        $appPool.Delete()
    }
    Save-CIisConfiguration
}




function Uninstall-CIisWebsite
{
    <#
    .SYNOPSIS
    Removes a website
 
    .DESCRIPTION
    Pretty simple: removes the website named `Name`. If no website with that name exists, nothing happens.
 
    .LINK
    Get-CIisWebsite
 
    .LINK
    Install-CIisWebsite
 
    .EXAMPLE
    Uninstall-CIisWebsite -Name 'MyWebsite'
 
    Removes MyWebsite.
 
    .EXAMPLE
    Uninstall-CIisWebsite 1
 
    Removes the website whose ID is 1.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name or ID of the website to remove.
        [Parameter(Mandatory, Position=0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [String[]] $Name
    )

    begin
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        $sitesToDelete = [Collections.Generic.List[String]]::New()
    }

    process
    {
        $sitesToDelete.AddRange($Name)
    }

    end
    {
        $madeChanges = $false

        $manager = Get-CIisServerManager

        foreach( $siteName in $sitesToDelete )
        {
            $site = $manager.Sites | Where-Object 'Name' -EQ $siteName
            if( -not $site )
            {
                return
            }

            $action = 'Remove IIS Website'
            if( $PSCmdlet.ShouldProcess($siteName, $action) )
            {
                Write-Information "Removing IIS website ""$($siteName)""."
                $manager.Sites.Remove( $site )
                $madeChanges = $true
            }
        }

        if( $madeChanges )
        {
            Save-CIisConfiguration
        }
    }
}



function Unlock-CIisConfigurationSection
{
    <#
    .SYNOPSIS
    Unlocks a section in the IIS server configuration.
 
    .DESCRIPTION
    Some sections/areas are locked by IIS, so that websites can't enable those settings, or have their own custom
    configurations. This function will unlocks those locked sections. You have to know the path to the section. You
    can see a list of locked sections by running:
 
        C:\Windows\System32\inetsrv\appcmd.exe unlock config /section:?
 
    Beginning with Carbon 2.0.1, this function is available only if IIS is installed.
 
    .EXAMPLE
    Unlock-IisConfigSection -Name 'system.webServer/cgi'
 
    Unlocks the CGI section so that websites can configure their own CGI settings.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The path to the section to unlock. For a list of sections, run
        #
        # C:\Windows\System32\inetsrv\appcmd.exe unlock config /section:?
        [Parameter(Mandatory)]
        [String[]] $SectionPath
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    foreach( $sectionPathItem in $SectionPath )
    {
        $section = Get-CIisConfigurationSection -SectionPath $sectionPathItem
        $section.OverrideMode = 'Allow'
        Save-CIisConfiguration -Target $sectionPathItem -Action 'Unlocking IIS Configuration Section'
    }
}





function Use-CallerPreference
{
    <#
    .SYNOPSIS
    Sets the PowerShell preference variables in a module's function based on the callers preferences.
 
    .DESCRIPTION
    Script module functions do not automatically inherit their caller's variables, including preferences set by common
    parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't
    get passed into any function that belongs to a module.
 
    When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the
    function's caller:
 
     * ErrorAction
     * Debug
     * Confirm
     * InformationAction
     * Verbose
     * WarningAction
     * WhatIf
     
    This function should be used in a module's function to grab the caller's preference variables so the caller doesn't
    have to explicitly pass common parameters to the module function.
 
    This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d).
 
    There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that
    causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add
    explicit `-ErrorAction $ErrorActionPreference` to every `Write-Error` call. Please vote up this issue so it can get
    fixed.
 
    .LINK
    about_Preference_Variables
 
    .LINK
    about_CommonParameters
 
    .LINK
    https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
 
    .LINK
    http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/
 
    .EXAMPLE
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
    Demonstrates how to set the caller's common parameter preference variables in a module function.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        #[Management.Automation.PSScriptCmdlet]
        # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]`
        # attribute.
        $Cmdlet,

        [Parameter(Mandatory)]
        # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the
        # `[CmdletBinding()]` attribute.
        #
        # Used to set variables in its callers' scope, even if that caller is in a different script module.
        [Management.Automation.SessionState]$SessionState
    )

    Set-StrictMode -Version 'Latest'

    # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken
    # from about_CommonParameters).
    $commonPreferences = @{
                              'ErrorActionPreference' = 'ErrorAction';
                              'DebugPreference' = 'Debug';
                              'ConfirmPreference' = 'Confirm';
                              'InformationPreference' = 'InformationAction';
                              'VerbosePreference' = 'Verbose';
                              'WarningPreference' = 'WarningAction';
                              'WhatIfPreference' = 'WhatIf';
                          }

    foreach( $prefName in $commonPreferences.Keys )
    {
        $parameterName = $commonPreferences[$prefName]

        # Don't do anything if the parameter was passed in.
        if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) )
        {
            continue
        }

        $variable = $Cmdlet.SessionState.PSVariable.Get($prefName)
        # Don't do anything if caller didn't use a common parameter.
        if( -not $variable )
        {
            continue
        }

        if( $SessionState -eq $ExecutionContext.SessionState )
        {
            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
        }
        else
        {
            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
        }
    }
}


function Wait-CIisAppPoolWorkerProcess
{
    <#
    .SYNOPSIS
    Waits for an IIS application pool to have running worker processes.
 
    .DESCRIPTION
    The `Wait-CIisAppPoolWorkerProcess` function waits for an IIS application pool to have running worker processes.
    Pass the name of the application pool to the `AppPoolName` parameter. By default, the function waits 30 seconds for
    there to be at least one running worker process. You can change the timeout by passing a `[TimeSpan]` object to the
    `Timeout` parameter.
 
    Some IIS application pools don't auto-start: IIS waits to create a worker process until a website under the
    application pool has received a request.
 
    In order to get an accurate record of the application pool's worker processes, this function creates a new
    internal server manager object for every check. If you have pending changes made by other Carbon.IIS functions,
    call `Save-CIisConfiguration` before calling `Wait-CIisAppPoolWorkerProcess`.
 
    .EXAMPLE
    Wait-CIisAppPoolWorkerProcess -AppPoolName 'www'
 
    Demonstrates how to wait for an application pool to have a running worker process by passing the application pool
    name to the `AppPoolName` parameter. In this example, the function will wait for the "www" application pool.
 
    .EXAMPLE
    Wait-CIisAppPoolWorkerProcess -AppPoolName 'www' -Timeout (New-TimeSpan -Seconds 300)
 
    Demonstrates how control how long to wait for an application pool to have a running worker process by passing a
    custom `[TimeSpan]` to the `TimeSpan` parameter. In this example, the function will wait 300 seconds (i.e. five
    minutes).
    #>

    [CmdletBinding()]
    param(
        # The name of the application pool
        [Parameter(Mandatory)]
        [String] $AppPoolName,

        # The total amount of time to wait for the application pool to have running worker processes. The default
        # timeout is 30 seconds.
        [TimeSpan] $Timeout = (New-TimeSpan -Seconds 30)
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $appPool = Get-CIisAppPool -Name $AppPoolName
    if (-not $appPool)
    {
        return
    }

    $timer = [Diagnostics.Stopwatch]::StartNew()

    while ($timer.Elapsed -lt $Timeout)
    {
        $mgr = Get-CIisServerManager -Reset
        $appPool = $mgr.ApplicationPools | Where-Object 'Name' -EQ $appPool.Name
        [Object[]] $wps = $appPool.WorkerProcesses
        [Object[]] $pss = $wps | ForEach-Object { Get-Process -Id $_.ProcessId -ErrorAction Ignore }
        if ($wps.Length -eq $pss.Length)
        {
            return
        }

        Start-Sleep -Milliseconds 100
    }

    $msg = "The ""$($appPool.Name)"" IIS application pool's worker processes haven't started after waiting " +
           "$($Timeout)."
    Write-Error -Message $msg -ErrorAction $ErrorActionPreference
}


function Write-IisVerbose
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,Position=0)]
        [string]
        # The name of the site.
        $SiteName,

        [string]
        $VirtualPath = '',

        [Parameter(Position=1)]
        [string]
        # The name of the setting.
        $Name,

        [Parameter(Position=2)]
        [string]
        $OldValue = '',

        [Parameter(Position=3)]
        [string]
        $NewValue = ''
    )

    Set-StrictMode -Version 'Latest'

    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if( $VirtualPath )
    {
        $SiteName = Join-CIisPath -Path $SiteName, $VirtualPath
    }

    Write-Verbose -Message ('[IIS Website] [{0}] {1,-34} {2} -> {3}' -f $SiteName,$Name,$OldValue,$NewValue)
}




function Write-CIisWarningOnce
{
    [CmdletBinding(DefaultParameterSetName='Message')]
    param(
        [Parameter(ValueFromPipeline, ParameterSetName='Message')]
        [String] $Message,

        [Parameter(Mandatory, ParameterSetName='ObsoleteSiteNameAndVirtualPath')]
        [switch] $ForObsoleteSiteNameAndVirtualPathParameter
    )

    process
    {
        Set-StrictMode -Version 'Latest'
        Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

        if ($PSCmdlet.ParameterSetName -eq 'ObsoleteSiteNameAndVirtualPath')
        {
            $functionName = $PSCmdlet.MyInvocation.MyCommand.Name
            $caller = Get-PSCallStack | Select-Object -Skip 1 | Select-Object -First 1
            if ($caller.FunctionName -like '*-CIis*')
            {
                $functionName = $caller.FunctionName
            }

            $Message = "The $($functionName) function''s ""SiteName"" and ""VirtualPath"" parameters are obsolete " +
                       'and have been replaced with a single "LocationPath" parameter, which should be the combined ' +
                       'path of the location/object to configure, e.g. ' +
                       "``$($functionName) -LocationPath 'SiteName/Virtual/Path'``. You can also use the " +
                       '`Join-CIisPath` function to combine site names and virtual paths into a single location path ' +
                       "e.g. ``$($functionName) -LocationPath ('SiteName', 'Virtual/Path' | Join-CIisPath)``."
        }

        if ($script:warningMessages.ContainsKey($Message))
        {
            return
        }

        Write-Warning -Message $Message

        $script:warningMessages[$Message] = $true
    }
}



# Get-Command -ModuleName doesn't work inside a module while its being imported.
$carbonIisCmds = Get-ChildItem -Path 'function:' | Where-Object 'ModuleName' -EQ 'Carbon.IIS'
$alwaysExclude = @{
    'Split-CIisLocationPath' = $true;
    'Write-CIisVerbose' = $true;
    'Write-IisVerbose' = $true;
}

function Format-Argument
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [String] $InputObject
    )

    process
    {
        # If it contains any quote characters, enclose in single quotes and escape just the single quotes. This will
        # handle any double quotes, backticks, and spaces.
        if ($_.Contains("'") -or $_.Contains('"'))
        {
            return "'$($_ -replace "'", "''")'"
        }

        # No quotes, but contains spaces, so enclose in single quotes, which will handle the spaces and any backtick
        # characters.
        if ($_.Contains(' '))
        {
            return "'$($_)'"
        }

        # Sweet. Nothing fancy. Return the original string.
        return $_
    }
}

function Register-CIisArgumentCompleter
{
    [CmdletBinding()]
    param(
        [String] $Filter = '*',

        [String[]] $Exclude,

        [Parameter(Mandatory)]
        [String] $ParameterName,

        [String[]] $ExcludeParameterName,

        [Parameter(Mandatory)]
        [String] $Description,

        [Parameter(Mandatory)]
        [scriptblock] $ScriptBlock
    )

    $cmdNames =
        $carbonIisCmds |
        Where-Object 'Name' -Like $Filter |
        Where-Object { -not $alwaysExclude.ContainsKey($_.Name) } |
        Where-Object {
            $cmd = $_

            if (-not $Exclude)
            {
                return $true
            }

            $excludedMatches = $Exclude | Where-Object { $cmd.Name -like $_ }
            if ($excludedMatches)
            {
                return $false
            }

            return $true
        } |
        Where-Object { $_.Parameters.ContainsKey($ParameterName) } |
        Where-Object {
            $cmd = $_
            if (-not $ExcludeParameterName)
            {
                return $true
            }

            foreach ($excludeFilter in $ExcludeParameterName)
            {
                foreach ($paramName in $cmd.Parameters.Keys)
                {
                    if ($paramName -like $excludeFilter)
                    {
                        return $false
                    }
                }
            }

            return $true
        } |
        Select-Object -ExpandProperty 'Name'

    if (-not $cmdNames)
    {
        $msg =  "Found no $($Description) commands matching filter ""$($Filter)"" with a parameter named " +
                "$($ParameterName)."
        Write-Debug $msg
        return
    }

    Write-Debug "Registering $($Description) auto-completer on parameter ""$($ParameterName)"" for functions"
    $cmdNames | ForEach-Object { " * $($_)" } | Write-Debug

    Register-ArgumentCompleter -CommandName $cmdNames -ParameterName $ParameterName -ScriptBlock $ScriptBlock
}

$appPoolNameCompleter = {
    param(
        [String] $CommandName,
        [String] $ParameterName,
        [String] $WordToComplete,
        $CommandAst,
        [hashtable] $FakeBoundParameters
    )

    Write-Debug "$($WordToComplete)"

    $completions = @()

    Get-CIisAppPool -Name "$($WordToComplete)*" -ErrorAction Ignore |
        Select-Object -ExpandProperty 'Name' |
        Tee-Object -Variable 'completions' |
        Format-Argument |
        Write-Output

    $completions | ForEach-Object { Write-Debug "> $($_)" }
}

Register-CIisArgumentCompleter -Filter '*-CIisAppPool' `
                               -Exclude 'Install-CIisAppPool' `
                               -ParameterName 'Name' `
                               -Description 'application pool name' `
                               -ScriptBlock $appPoolNameCompleter

Register-CIisArgumentCompleter -Filter '*' `
                               -ParameterName 'AppPoolName' `
                               -Description 'application pool name' `
                               -ScriptBlock $appPoolNameCompleter

$websiteNameCompleter = {
    param(
        [String] $CommandName,
        [String] $ParameterName,
        [String] $WordToComplete,
        $CommandAst,
        [hashtable] $FakeBoundParameters
    )

    Write-Debug "$($WordToComplete)"

    $completions = @()

    Get-CIisWebsite -Name "$($WordToComplete)*" -ErrorAction Ignore |
        Select-Object -ExpandProperty 'Name' |
        Tee-Object -Variable 'completions' |
        Format-Argument |
        Write-Output

    $completions | ForEach-Object { Write-Debug "> $($_)" }
}

Register-CIisArgumentCompleter -Filter '*-CIisWebsite' `
                               -Exclude 'Install-CIisWebsite' `
                               -ParameterName 'Name' `
                               -Description 'website name' `
                               -ScriptBlock $websiteNameCompleter

Register-CIisArgumentCompleter -ParameterName 'SiteName' `
                               -ExcludeParameterName 'LocationPath' `
                               -Description 'website name' `
                               -ScriptBlock $websiteNameCompleter

$appCompleter = {
    param(
        [String] $CommandName,
        [String] $ParameterName,
        [String] $WordToComplete,
        $CommandAst,
        [hashtable] $FakeBoundParameters
    )

    if (-not $FakeBoundParameters.ContainsKey('SiteName'))
    {
        return
    }

    if ($WordToComplete -and $WordToComplete.Length -gt 0 -and $WordToComplete[0] -ne '/')
    {
        $WordToComplete = "/$($WordToComplete)"
    }

    $completions = @()

    Get-CIisApplication -LocationPath (Join-CIisPath $FakeBoundParameters['SiteName'], "$($WordToComplete)*") |
        Select-Object -ExpandProperty 'Path' |
        Tee-Object -Variable 'completions' |
        Format-Argument |
        Write-Output

    $completions | ForEach-Object { Write-Debug "> $($_)" }
}

Register-CIisArgumentCompleter -ParameterName 'VirtualPath' `
                               -ExcludeParameterName 'LocationPath' `
                               -Exclude 'Install-*' `
                               -ScriptBlock $appCompleter `
                               -Description 'application virtual path'

$appCompleter = {
    param(
        [String] $CommandName,
        [String] $ParameterName,
        [String] $WordToComplete,
        $CommandAst,
        [hashtable] $FakeBoundParameters
    )

    if (-not $FakeBoundParameters.ContainsKey('SiteName'))
    {
        Write-Debug 'No SiteName'
        return
    }

    if ($WordToComplete -and $WordToComplete.Length -gt 0 -and $WordToComplete[0] -ne '/')
    {
        $WordToComplete = "/$($WordToComplete)"
    }

    $completions = @()

    Get-CIisApplication -SiteName $FakeBoundParameters['SiteName'] |
        Select-Object -ExpandProperty 'Path'
        Tee-Object -Variable 'completions' |
        Format-Argument |
        Write-Output

    $completions | ForEach-Object { Write-Debug "> $($_)" }
}

Register-CIisArgumentCompleter -Description 'virtual directory' `
                               -ParameterName 'VirtualPath' `
                               -ExcludeParameterName 'LocationPath' `
                               -Exclude 'Install-*' `
                               -ScriptBlock $appCompleter


$locationCompleter = {
    param(
        [String] $CommandName,
        [String] $ParameterName,
        [String] $WordToComplete,
        $CommandAst,
        [hashtable] $FakeBoundParameters
    )

    $ErrorActionPreference = 'Continue'

    # Turn off other debug messages in the locater so if we need to we can debug just what's going on in this script
    # block.
    $PSDefaultParameterValues = @{
        'ConvertTo-CIisVirtualPath:Debug' = $false;
        'Join-CIisPath:Debug' = $false;
        'Get-CIisWebsite:Debug' = $false;
    }

    [String] $siteName = ''
    $locationFilter = '*'
    if ($WordToComplete)
    {
        $locationFilter = "$($WordToComplete)*" | ConvertTo-CIisVirtualPath -NoLeadingSlash
        $siteName, $null = $WordToComplete.Split('/', 2)
    }

    Write-Debug ''
    Write-Debug "$($WordToComplete) -> $($locationFilter)"

    $physicalPathsByVirtualPath = @{}

    [String[]] $completions = @()
    & {
            if (-not $siteName -or -not (Test-CIIsWebsite -Name ([wildcardpattern]::Escape($siteName))))
            {
                Write-Debug "Getting website names."
                Get-CIisWebsite -Name "$($siteName)*" |
                    Select-Object -ExpandProperty 'Name' |
                    ConvertTo-CIisVirtualPath -NoLeadingSlash |
                    Format-Argument |
                    Write-Output
                return
            }

            $site = Get-CIisWebsite -Name $siteName
            $siteLocationPath = $site.Name

            foreach ($app in $site.Applications)
            {
                $appLocationPath = $siteLocationPath
                if ($app.Path -ne '/')
                {
                    $appLocationPath = Join-CIisPath -Path $appLocationPath, $app.Path
                }

                foreach ($vdir in $app.VirtualDirectories)
                {
                    $vdirLocationPath = $appLocationPath
                    if ($vdir.Path -ne '/')
                    {
                        $vdirLocationPath = Join-CIisPath -Path $vdirLocationPath, $vdir.Path
                    }

                    $physicalPathsByVirtualPath[$vdirLocationPath] = $vdir.PhysicalPath

                    if ($vdirLocationPath -like $locationFilter)
                    {
                        Write-Debug " ~ $($vdirLocationPath)"
                        $vdirLocationPath | Write-Output
                    }
                    else
                    {
                        Write-Debug " ! ~ $($vdirLocationPath)"
                    }
                }
            }

            # In order to discover any physical paths for auto-completion, we need to break the user's input into two
            # parts on every slash, check if the first part is a virtual path, then check if the second part is a
            # physical directory under that virtual path. For example, if we have wwwroot/VDir/Dir, we need to check
            # if `VDir/Dir` exists under the `wwwroot` virtual path's physical path, then check if `Dir` exists under
            # the `wwwroot/VDir` virtual directory.
            $locationPath, $needle = $WordToComplete.Split('/', 2)
            do
            {
                if ($physicalPathsByVirtualPath.ContainsKey($locationPath))
                {
                    $physicalPath = $physicalPathsByVirtualPath[$locationPath]
                    if ($needle)
                    {
                        $physicalPath = Join-Path -Path $physicalPath -ChildPath $needle
                    }
                    if (Test-Path -Path $physicalPath)
                    {
                        foreach ($dir in (Get-ChildItem -Path $physicalPath -Directory))
                        {
                            Join-CIisPath -Path $locationPath, $needle, $dir.Name | Write-Output
                        }
                    }
                }

                if (-not $needle)
                {
                    break
                }

                $rootSegment, $needle = $needle.Split('/', 2)
                $locationPath = Join-CIisPath $locationPath, $rootSegment
            }
            while ($true)
        } |
        Tee-Object -Variable 'completions' |
        Format-Argument |
        Write-Output

    $completions | ForEach-Object { Write-Debug "> $($_)" }
}

Register-CIisArgumentCompleter -Description 'location path' `
                               -ParameterName 'LocationPath' `
                               -ScriptBlock $locationCompleter