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.Collections
using namespace System.Collections.Generic
using namespace System.Management.Automation
using namespace System.ServiceProcess
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'
# These are all the files that could cause the current server manager object to become stale.
$script:iisConfigs = & {
    Join-Path -Path ([Environment]::SystemDirectory) -ChildPath 'inetsrv\config\*.config'
    Join-Path -Path ([Environment]::GetFolderPath('Windows')) -ChildPath 'Microsoft.NET\Framework*\v*\config\*.config'
}
$script:skipCommit = $false

# Seriously. It has an `IsNumeric` method that isn't part of .NET.
Add-Type -AssemblyName 'Microsoft.VisualBasic'

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

Import-Module -Name (Join-Path -Path $psModulesRoot -ChildPath 'Carbon.Windows.HttpServer' -Resolve) `
              -Function @('Set-CHttpsCertificateBinding')

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 Add-CIisHttpHeader
{
    <#
    .SYNOPSIS
    Adds an HTTP header to IIS.
 
    .DESCRIPTION
    The `Add-CIisHttpHeader` function adds a header to IIS. Pass the header's name to the `Name` parameter and the
    header's value to the `Value` parameter. By default, the header is added to all HTTP responses (i.e. IIS's global
    settings are updated). To add the header only to responses from a specific website, application, virtual directory,
    or directory, pass the location's path to the `LocationPath` parameter.
 
    The function adds the HTTP header to the `system.webServer/httpProtocol/customHeaders` configuration collection.
 
    .EXAMPLE
    Add-CIisHttpHeader -Name 'foo' -Value 'bar'
 
    Demonstrates how to add a new HTTP header globally. After the above command runs, this will be in the
    applicationHost.
 
        <system.webServer>
            <httpProtocol>
                <customHeaders>
                    <add name="foo" value="bar" />
                </customHeaders>
            </httpProtocol>
        </system.webServer>
 
    .EXAMPLE
    Add-CIisHttpHeader -LocationPath 'SITE_NAME' -Name 'X-AddHeader' -Value 'usingCarbon'
 
    Demonstrates how to add a new HTTP header to the site `SITE_NAME`. After the above command runs, this will be in the
    applicationHost.config:
 
        <location path="SITE_NAME">
            <system.webServer>
                <httpProtocol>
                    <customHeaders>
                        <add name="X-AddHeader" value="usingCarbon" />
                    </customHeaders>
                </httpProtocol>
            </system.webServer>
        <location path="SITE_NAME" />
    #>

    [CmdletBinding(DefaultParameterSetName='Global')]
    param(
        # The HTTP header name to add.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [String] $Name,

        # The HTTP header value to add.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [String] $Value,

        # The location path to the site, directory, appliction, or virtual directory to configure. By default, headers
        # are added to global configuration.
        [Parameter(ParameterSetName='Local')]
        [String] $LocationPath
    )

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

        $setArgs = @{}

        if ($LocationPath)
        {
            $setArgs['LocationPath'] = $LocationPath
        }

        $headers = [List[hashtable]]::New()
    }

    process
    {
        $headers.Add(@{ 'name' = $Name ; 'value' = $Value })
    }

    end
    {
        $headers | Set-CIisCollectionItem -SectionPath 'system.webServer/httpProtocol' `
                                          -CollectionName 'customHeaders' `
                                          @setArgs
    }
}



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)"
        }

        # [IO.Path]::GetFullPath fails if a path contains certain characters, so we need to escape them, use
        # GetFullPath, then unescape them.
        $prevPath = $Path
        $charsToEscape = @('%', '"', '*', '<', '>', '?', '|')
        foreach ($invalidChar in $charsToEscape)
        {
            $escapeSequence = "%$([byte][char]$invalidChar)"
            $Path = $Path -replace [regex]::Escape($invalidChar),$escapeSequence
        }
        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)
        }
        foreach ($invalidChar in $charsToEscape)
        {
            $escapeSequence = "%$([byte][char]$invalidChar)"
            $Path = $Path -replace $escapeSequence, $invalidChar
        }
        $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-CIisCollectionInheritance
{
    <#
    .SYNOPSIS
    Updates IIS configuration collections so they no longer inherit items.
 
    .DESCRIPTION
    The `Disable-CIisCollectionInheritance` function turns off inheritance of items in an IIS configuration collection,
    i.e. it adds a `<clear />` element to the collection. Pass the path to the configuration section collection to the
    `SectionPath` parameter. If the collection is actually a sub-element of the configuration section, pass the name of
    the collection to the `Name` parameter. Inheritance is disabled only if the collection doesn't already have a
    `<clear />` element. The function reads the applicationHost.config in order to make this determination, since there
    are no APIs that make this information available.
 
    To disable inheritance for a site, directory, application, or virtual directory, pass its location path to the
    `LocationPath` parameter.
 
    To disable inheritance for a collection that is a or is a child of a
    `[Microsoft.Web.Administration.ConfigurationElement]` (i.e. configuration under a site, application pool, etc.),
    pass the object to the `ConfigurationElement` parameter. If the object isn't a collection, pass the name of the
    child element collection to the `Name` parameter. Becuase the Microsoft.Web.Administration API doesn't expose a way
    to know if a collection has been cleared, the `Disable-CIisCollectionInheritance` function has to inspect the
    application host config file directly, so you'll also need to pass the XPath expression to the collection element
    to the `CollectionElementXPath` parameter.
 
    When making changes directly to ConfigurationElement objects, test that those changes are saved correctly to the IIS
    application host configuration. Some configuration has to be saved at the same time as its parent configuration
    elements are created (i.e. sites, application pools, etc.). Use the `Suspend-CIisAutoCommit` and
    `Resume-CIisAutoCommit` functions to ensure configuration gets committed simultaneously.
 
    .EXAMPLE
    Disable-CIisCollectionInheritance -SectionPath 'system.webServer/httpProtocol' -Name 'customHeaders'
 
    Demonstrates how to disable inheritance on a global collection by passing its configuration section path to the
    `SectionPath` parameter and the collection name to the `Name` parameter.
 
    .EXAMPLE
    Disable-CIisCollectionInheritance -SectionPath 'system.webServer/httpProtocol' -Name 'customHeaders' -LocationPath 'mysite'
 
    Demonstrates how to disable inheritance on a collection under a site, directory, application, or virtual directory
    by passing the location path to the site, directory, application, or vitual directory to the `LocationPath`
    parameter.
    #>

    [CmdletBinding(DefaultParameterSetName='BySectionPath')]
    param(
        # The configuration element to configure. If this is the parent element of the collection to configure, pass the
        # name of the collection child element to the `Name` parameter.
        [Parameter(Mandatory, ParameterSetName='ByConfigurationElement')]
        [ConfigurationElement] $ConfigurationElement,

        # The XPath to the configuration element. The Microsoft.Web.Administration API doesn't expose a way to check
        # if a collection's inheritance is disabled or not, so we have to look directly in the application host config
        # file.
        [Parameter(Mandatory, ParameterSetName='ByConfigurationElement')]
        [String] $CollectionElementXPath,

        # The configuration section's path who's inheritance to disable. Can be the path to the collection itself, or
        # the collection's parent element. If passing the parent element, pass the name of the collection to the `Name`
        # parameter.
        [Parameter(Mandatory, ParameterSetName='BySectionPath')]
        [String] $SectionPath,

        # Location path to the site, directory, application, or virtual directory that should be changed. The default is
        # to modify global configuration.
        [Parameter(ParameterSetName='BySectionPath')]
        [String] $LocationPath,

        # The name of the collection.
        [String] $Name
    )

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

    $PSBoundParameters.Remove('CollectionElementXPath') | Out-Null

    $collection = Get-CIisCollection @PSBoundParameters
    if (-not $collection)
    {
        return
    }

    $testArgs = @{}
    $displayPath = $collection.ElementTagName
    if ($PSCmdlet.ParameterSetName -eq 'BySectionPath')
    {
        $displayPath = Get-CIisDisplayPath -SectionPath $SectionPath -LocationPath $LocationPath

        $CollectionElementXPath = $SectionPath.Trim('/')
        if ($Name)
        {
            $CollectionElementXPath = "${CollectionElementXPath}/$($Name.Trim('/'))"
        }

        if ($LocationPath)
        {
            $testArgs['LocationPath'] = $LocationPath
        }
    }

    if (-not $collection.AllowsClear)
    {
        $msg = "Failed to clear collection ${displayPath} because it does not allow clearing."
        Write-Message $msg -ErrorAction $ErrorActionPreference
        return
    }

    # The Microsoft.Web.Administration API does not expose any way of determining if a collection has a `clear`
    # element, so we have to crack open the applicationHost.config file to look for it. :(
    if (Test-CIisApplicationHostElement -XPath "${CollectionElementXPath}/clear" @testArgs)
    {
        Write-Verbose "IIS configuration collection ${displayPath} inheritance already disabled."
        return
    }

    Write-Information "Disabling IIS collection inheritance for ${displayPath}."
    $collection.Clear()

    Save-CIisConfiguration
}


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-CIisHttps
{
    <#
    .SYNOPSIS
    Turns on and configures HTTPS for a website or part of a website.
 
    .DESCRIPTION
    This function enables HTTPS and optionally the site/directory to:
 
     * Require HTTPS (the `RequireHttps` switch)
     * Ignore/accept/require client certificates (the `AcceptClientCertificates` and `RequireClientCertificates` switches).
     * Requiring 128-bit HTTPS (the `Require128BitHttps` switch).
 
    By default, this function will enable HTTPS, make HTTPS connections optional, ignores client certificates, and not
    require 128-bit HTTPS.
 
    Changing any HTTPS settings will do you no good if the website doesn't have an HTTPS binding or doesn't have an
    HTTPS certificate. The configuration will most likely succeed, but won't work in a browser. So sad.
 
    Beginning with IIS 7.5, the `Require128BitHttps` 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-CIisWebsiteHttpsCertificate
 
    .EXAMPLE
    Enable-CIisHttps -LocationPath 'Peanuts'
 
    Enables HTTPS on the `Peanuts` website's, making makes HTTPS connections optional, ignoring client certificates, and
    making 128-bit HTTPS optional.
 
    .EXAMPLE
    Enable-CIisHttps -LocationPath 'Peanuts/Snoopy/DogHouse' -RequireHttps
 
    Configures the `/Snoopy/DogHouse` directory in the `Peanuts` site to require HTTPS. It also turns off any client
    certificate settings and makes 128-bit HTTPS optional.
 
    .EXAMPLE
    Enable-CIisHttps -LocationPath 'Peanuts' -AcceptClientCertificates
 
    Enables HTTPS on the `Peanuts` website and configures it to accept client certificates, makes HTTPS optional, and
    makes 128-bit HTTPS optional.
 
    .EXAMPLE
    Enable-CIisHttps -LocationPath 'Peanuts' -RequireHttps -RequireClientCertificates
 
    Enables HTTPS on the `Peanuts` website and configures it to require HTTPS and client certificates. You can't require
    client certificates without also requiring HTTPS.
 
    .EXAMPLE
    Enable-CIisHttps -LocationPath 'Peanuts' -Require128BitHttps
 
    Enables HTTPS on the `Peanuts` website and require 128-bit HTTPS. Also, makes HTTPS connections optional and
    ignores client certificates.
    #>

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

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

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

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

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

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

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

    $httpsFlags = [CIisHttpsFlags]::None
    if( $RequireHttps -or $RequireClientCertificates )
    {
        $httpsFlags = $httpsFlags -bor [CIisHttpsFlags]::Ssl
    }

    if( $AcceptClientCertificates -or $RequireClientCertificates )
    {
        $httpsFlags = $httpsFlags -bor [CIisHttpsFlags]::SslNegotiateCert
    }

    if( $RequireClientCertificates )
    {
        $httpsFlags = $httpsFlags -bor [CIisHttpsFlags]::SslRequireCert
    }

    if( $Require128BitHttps )
    {
        $httpsFlags = $httpsFlags -bor [CIisHttpsFlags]::Ssl128
    }

    Set-CIisConfigurationAttribute -LocationPath (Join-CIisPath $LocationPath,$VirtualPath) `
                                   -SectionPath 'system.webServer/security/access' `
                                   -Name 'sslFlags' `
                                   -Value $httpsFlags
}




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 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-CIisCollection
{
    <#
    .SYNOPSIS
    Gets an IIS configuration collection.
 
    .DESCRIPTION
    The `Get-CIisCollection` function gets an IIS configuration element as a collection. Pass the collection's IIS
    configuration section path to the `SectionPath` parameter. If the collection is actually a child element of the
    configuration section, pass the name of the child element collection to the `Name` parameter. To get the section for
    a specific site, directory, application, or virtual directory, pass its location path to the `LocationPath`
    parameter.
 
    You can pass an instance of a `[Microsoft.Web.Administration.ConfigurationElement]` to the `ConfigurationElement`
    parameter to return that element as a collection, or, with the `Name` parameter, get a named collection under that
    configuration element.
 
    This function returns a configuration element collection object. To get the items from the collection, use
    `Get-CIisCollectionItem`.
 
    .EXAMPLE
    $collection = Get-CIisCollection -LocationPath 'SITE_NAME' -SectionPath 'system.webServer/httpProtocol' -Name 'customHeaders'
 
    Demonstrates how to get the collection 'customHeaders' inside the section 'system.webServer/httpProtocol' for the
    site 'SITE_NAME'.
    #>

    [CmdletBinding(DefaultParameterSetName='BySectionPath')]
    param(
        # The `[Microsoft.Web.Administration.ConfigurationElement]` object to get as a collection or the parent element
        # of the collection element to get. If this is the parent element, pass the name of the child element collection
        # to the `Name` parameter.
        [Parameter(Mandatory, ParameterSetName='ByConfigurationElement')]
        [ConfigurationElement] $ConfigurationElement,

        # The configuration section path of the collection, or, if the configuration section is a parent of the
        # collection, the configuration section path to the parent configuration section. If the configuration section
        # is the parent of the collection, pass the collection name to the `Name` parameter.
        [Parameter(Mandatory, ParameterSetName='BySectionPath')]
        [String] $SectionPath,

        # The location path to the site, directory, application, or virtual directory to configure.
        [Parameter(ParameterSetName='BySectionPath')]
        [String] $LocationPath,

        # The collection's name.
        [String] $Name
    )

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

    $displayPath = ''
    if ($ConfigurationElement)
    {
        $displayPath = $ConfigurationElement.ElementTagName
    }
    else
    {
        $getArgs = @{}
        if ($LocationPath)
        {
            $getArgs['LocationPath'] = $LocationPath
        }

        $ConfigurationElement = Get-CIisConfigurationSection @getArgs -SectionPath $SectionPath
        if (-not $ConfigurationElement)
        {
            return
        }

        $displayPath = Get-CIisDisplayPath -SectionPath $SectionPath -LocationPath $LocationPath -SubSectionPath $Name
    }

    if ($Name)
    {
        $collection = $ConfigurationElement.GetCollection($Name)
    }
    elseif ($ConfigurationElement -is [ICollection])
    {
        $collection = $ConfigurationElement
    }
    else
    {
        $collection = $ConfigurationElement.GetCollection()
    }

    if (-not $collection)
    {
        $msg = "Failed to get IIS configuration collection ${displayPath} because it does not exist or is not a " +
               'collection'
        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        return
    }

    # Return the collection itself *not* the collection items. Get-CIisCollectionItem will return individual items.
    return ,$collection
}

function Get-CIisCollectionItem
{
    <#
    .SYNOPSIS
    Gets the items from an IIS configuration collection.
 
    .DESCRIPTION
    The `Get-CIisCollectionItem` function gets the items from an IIS configuration element collection. Pass the
    collection's IIS configuration section path to the `SectionPath` parameter. If the configuration section is actually
    the parent element of the the collection, pass the name of the child element collection to the `CollectionName`
    parameter. To get the section for a specific site, directory, application, or virtual directory, pass its location
    path to the `LocationPath` parameter.
 
    You can pass an instance of a `[Microsoft.Web.Administration.ConfigurationElement]` to the `ConfigurationElement`
    parameter to return that collection element's items, or, with the `CollectionName` parameter, get a named collection
    under that configuration element.
 
    This function returns configuration element collection items. To get the collection object itself, use
    `Get-CIisCollection`.
 
 
    .EXAMPLE
    $items = Get-CIisCollectionItem -SectionPath 'system.webServer/httpProtocol' -CollectionName 'customHeaders'
 
    Demonstrates how to get the custom HTTP headers from the 'customHeaders' collection, which is a child of the
    "system.webServer/httpProtocol" configuration section.
    #>

    [CmdletBinding(DefaultParameterSetName='BySectionPath')]
    param(
        # The `[Microsoft.Web.Administration.ConfigurationElement]` object whose collection items to get, or the parent
        # element of the collection whose items to get. If this is the parent element, pass the name of the child
        # element collection to the `CollectionName` parameter.
        [Parameter(Mandatory, ParameterSetName='ByConfigurationElement')]
        [ConfigurationElement] $ConfigurationElement,

        # The configuration section path of the collection, or, if the configuration section is a parent of the
        # collection, the configuration section path to the parent configuration section. If the configuration section
        # is the parent of the collection, pass the collection name to the `CollectionName` parameter.
        [Parameter(Mandatory, ParameterSetName='BySectionPath')]
        [String] $SectionPath,

        # The location path to the site, directory, application, or virtual directory to configure.
        [Parameter(ParameterSetName='BySectionPath')]
        [String] $LocationPath,

        # The collection's name.
        [Alias('Name')]
        [String] $CollectionName
    )

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

    if ($PSBoundParameters.ContainsKey('CollectionName'))
    {
        $PSBoundParameters['Name'] = $CollectionName
        $PSBoundParameters.Remove('CollectionName') | Out-Null
    }

    Get-CIisCollection @PSBoundParameters | Write-Output
}

function Get-CIisCollectionKeyName
{
    <#
    .SYNOPSIS
    Returns the unique key for a configuration collection.
 
    .DESCRIPTION
    The `Get-CIisCollectionKeyName` locates the mandatory attribute for an IIS configuration collection. This attribute
    name must be included for all entries inside of an IIS collection.
 
    .EXAMPLE
    Get-CIisCollectionKeyName -Collection (Get-CIisCollection -SectionPath 'system.webServer/httpProtocol' -Name 'customHeaders')
 
    Demonstrates how to get the collection key name for the 'system.webServer/httpProtocol/customHeaders' collection.
    This will return 'name' as the key name.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ConfigurationElement] $Collection
    )

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

    return $Collection.CreateElement().Attributes |
                Where-Object { $_.Schema.IsUniqueKey } |
                Select-Object -ExpandProperty 'name'
}


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
    {
        $displayPath = Get-CIisDisplayPath -SectionPath $SectionPath -LocationPath $LocationPath
        $msg = "IIS configuration section ${displayPath} does not exist."
        Write-Error $msg -ErrorAction $ErrorActionPreference
        return
    }
}




function Get-CIisDescription
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ParameterSetName='ByConfigurationPath')]
        [ConfigurationElement] $ConfigurationElement,

        [Parameter(Mandatory, ParameterSetName='BySectionPath')]
        [String] $SectionPath,

        [Parameter(ParameterSetName='BySectionPath')]
        [String] $LocationPath,

        [Parameter(ParameterSetName='BySectionPath')]
        [String] $SubSectionPath
    )

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

    function Get-LocationDescription
    {
        [CmdletBinding()]
        param(
            [String] $LocationPath
        )

        if (-not $LocationPath)
        {
            return ''
        }

        return " for location ""${LocationPath}"""
    }

    if ($ConfigurationElement)
    {
        $SectionPath = ''
        $LocationPath = ''
        $SubSectionPath = ''

        if ($ConfigurationElement | Get-Member -Name 'SectionPath')
        {
            $SectionPath = $ConfigurationElement.SectionPath
        }

        $locationDesc = ''
        if ($ConfigurationElement | Get-Member -Name 'LocationPath')
        {
            $LocationPath = $ConfigurationElement.LocationPath
        }

        if (-not $SectionPath)
        {
            $locationDesc = Get-LocationDescription -LocationPath $LocationPath

            $name = $ConfigurationElement.Attributes['name']
            if ($name)
            {
                $name = " ""$($name.Value)"""
            }
            else
            {
                $name = $ConfigurationElement.Attributes['path']
                if ($name)
                {
                    $name = " $($name.Value)"
                }
            }
            return "IIS configuration element $($ConfigurationElement.ElementTagName)${name}${locationDesc}"
        }
    }

    $sectionDesc = $SectionPath.Trim('/')
    if ($SubSectionPath)
    {
        $sectionDesc = "${sectionDesc}/$($SubSectionPath.Trim('/'))"
    }

    $locationDesc = Get-LocationDescription -LocationPath $LocationPath

    return "IIS configuration section ${sectionDesc}${locationDesc}"
}



function Get-CIisDisplayPath
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [String] $SectionPath,

        [String] $LocationPath,

        [String] $SubSectionPath
    )

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

    $path = $SectionPath.Trim('/')
    if ($LocationPath)
    {
        $path = "${LocationPath}:${path}"
    }

    if ($SubSectionPath)
    {
        $path = "${path}/$($SubSectionPath.Trim('/'))"
    }

    return $path
}


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())"
    }

    foreach ($config in ($script:iisConfigs | Get-Item))
    {
        if ($script:serverMgrCreatedAt -lt $config.LastWriteTimeUtc)
        {
            $Reset = $true
            "$(New-MessagePrefix)Stale $($script:serverMgrCreatedAt.ToString('O')) < " +
                "$($config.LastWriteTimeUtc.ToSTring('O')) $($config.FullName)" | Write-Debug
            break
        }
    }

    if ($Commit -and -not $script:skipCommit)
    {
        try
        {
            $appHostLastWriteTimeUtc =
                Get-Item -Path $script:applicationHostPath | Select-Object -ExpandProperty 'LastWriteTimeUtc'

            Write-Debug "$(New-MessagePrefix)CommitChanges()"
            Write-Verbose "Committing IIS configuration changes."
            $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 $appHostLastWriteTimeUtc -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 "$(New-MessagePrefix)Dispose()"
        $script:serverMgr.Dispose()
    }

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

    Write-Debug "$(New-MessagePrefix)"
    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.VirtualDirectory])]
    param(
        # The site where the application is running.
        [Parameter(Mandatory, Position=0, ParameterSetName='ByLocationPath')]
        [String] $LocationPath,

        # The virtual directory's site's name.
        [Parameter(Mandatory, ParameterSetName='ByName')]
        [String] $SiteName,

        # The virtual directory's virtual path. Wildcards supported.
        [Parameter(ParameterSetName='ByName')]
        [String] $VirtualPath,

        # The virtual directory's application's virtual path. The default is to get the root site's virtual directories.
        [Parameter(ParameterSetName='ByName')]
        [String] $ApplicationPath
    )

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

    if ($PSCmdlet.ParameterSetName -eq 'ByName')
    {
        $sites = Get-CIisWebsite -Name $SiteName
        if (-not $sites)
        {
            return
        }

        if (-not $ApplicationPath)
        {
            $ApplicationPath = '/'
        }

        $VirtualPath = $VirtualPath | ConvertTo-CIisVirtualPath

        foreach ($site in $sites)
        {
            $apps = Get-CIisApplication -SiteName $site.Name -VirtualPath $ApplicationPath
            if (-not $apps)
            {
                continue
            }

            foreach ($app in $apps)
            {
                $appDesc = ''
                if ($app.Path -ne '/')
                {
                    $appDesc = " under application ""$($app.Path)"""
                }

                $vdir =
                    $app.VirtualDirectories |
                    Where-Object {
                        if ($VirtualPath)
                        {
                            return $_.Path -like $VirtualPath
                        }

                        return $true
                    }
                if (-not $vdir)
                {
                    if ($VirtualPath -and -not [wildcardpattern]::ContainsWildcardCharacters($VirtualPath))
                    {
                        $msg = "Failed to get virtual directory ""${VirtualPath}""${appDesc} under site " +
                               """${SiteName}"" because the virtual directory does not exist."
                        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                    }
                    continue
                }

                $vdir | Write-Output
            }
        }

        return
    }

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

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

    $virtualPath = $virtualPath | ConvertTo-CIisVirtualPath

    foreach ($app in $site.Applications)
    {
        foreach ($vdir in $app.VirtualDirectories)
        {
            $fullVirtualPath = Join-CIisPath $app.Path, $vdir.Path -LeadingSlash

            if ($fullVirtualPath -like $virtualPath)
            {
                $vdir | Write-Output
            }
        }
    }
}




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
    }

    $appDesc = ''
    if ($destinationApp -and $destinationApp.Path -ne '/')
    {
        $appDesc = " under application ""$($destinationApp.Path)"""
    }

    $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 '/'
    }

    $desc = "IIS virtual directory ""${VirtualPath}""${appDesc} under site ""${SiteName}"""
    $created = $false
    if (-not $vdir)
    {
        [Microsoft.Web.Administration.ConfigurationElementCollection]$vdirs = $destinationApp.GetCollection()
        $vdir = $vdirs.CreateElement('virtualDirectory')
        Write-Information "Creating ${desc}."
        Write-Information " + physicalPath ${PhysicalPath}"
        $vdir['path'] = $VirtualPath
        [void]$vdirs.Add( $vdir )
        $created = $true
    }

    $modified = $false
    if ($vdir['physicalPath'] -ne $PhysicalPath)
    {
        $vdir['physicalPath'] = $PhysicalPath
        if (-not $created)
        {
            Write-Information $desc
            Write-Information " physicalPath $($vdir['physicalPath']) -> ${PhysicalPath}"
        }
        $modified = $true
    }

    if ($created -or $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 ) ) }

    $bindingsPreamble = "IIS Website ""$($site.Name)"" Bindings"
    $bindingsPrefix = ' '
    $shownBindingsPreamble = $false

    foreach( $bindingToRemove in $bindingsToRemove )
    {
        if (-not $shownBindingsPreamble)
        {
            Write-Information $bindingsPreamble
            $shownBindingsPreamble = $true
        }
        Write-Information "${bindingsPrefix}- $($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 )
    {
        if (-not $shownBindingsPreamble)
        {
            Write-Information $bindingsPreamble
            $shownBindingsPreamble = $true
        }
        Write-Information "${bindingsPrefix}+ $($bindingToAdd.Protocol)/$($bindingToAdd.BindingInformation)"
        $site.Bindings.Add( $bindingToAdd.BindingInformation, $bindingToAdd.Protocol ) | Out-Null
        $modified = $true
    }

    [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,

        [Parameter(Mandatory)]
        [ConfigurationElement] $Defaults,

        [switch] $AsDefaults
    )

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

    $invocation = $SourceCmdlet.MyInvocation
    $cmd = $invocation.MyCommand

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

    $cmdParameters = $invocation.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 `
                                   -Defaults $Defaults `
                                   -AsDefaults:$AsDefaults
}



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-CIisCollectionItem
{
    <#
    .SYNOPSIS
    Removes a IIS configuration element.
 
    .DESCRIPTION
    The `Remove-CIisCollectionItem` function removes an item from an IIS configuration collection. Pass the collection's
    IIS configuration section path to the `SectionPath` parameter and the value to remove from the collection to the
    `Value` parameter. This function removes that value from the collection if it exists. If the value does not exist,
    the function writes an error.
 
    If removing an item from the collection for a website, application, virtual directory, pass the path to that
    location to the `LocationPath` parameter'
 
    .EXAMPLE
    Remove-CIisCollectionItem -SectionPath 'system.webServer/httpProtocol' -CollectionName 'customHeaders' -Value 'X-CarbonRemoveItem'
 
    Demonstrates how to remove the 'X-CarbonRemoveItem' header if it has previously been added.
 
    .EXAMPLE
    Remove-CIisCollectionItem -LocationPath 'SITE_NAME' -SectionPath `system.webServer/httpProtocol' -CollectionName 'customHeaders' -Value 'X-CarbonRemoveItem'
 
    Demonstrates how to remove the 'X-CarbonRemoveItem' header from the 'SITE_NAME' location.
    #>

    [CmdletBinding(DefaultParameterSetName='BySectionPath')]
    param(
        # The `[Microsoft.Web.Administration.ConfigurationElement]` object to get as a collection or the parent element
        # of the collection element to get. If this is the parent element, pass the name of the child element collection
        # to the `CollectionName` parameter.
        [Parameter(Mandatory, ParameterSetName='ByConfigurationElement')]
        [ConfigurationElement] $ConfigurationElement,

        # The path to the collection's configuration section.
        [Parameter(Mandatory, ParameterSetName='BySectionPath')]
        [String] $SectionPath,

        # The location path of the site, directory, application, or virtual directory whose configuration to update.
        # Default is to update the global configuration.
        [Parameter(ParameterSetName='BySectionPath')]
        [String] $LocationPath,

        # The collection the item belongs to.
        [Alias('Name')]
        [String] $CollectionName,

        # The value to be removed.
        [Parameter(Mandatory, ValueFromPipeline)]
        [String[]] $Value,

        # The attribute name for the attribute that uniquely identifies each item in a collection. This is usually
        # automatically detected.
        [String] $UniqueKeyAttributeName,

        # ***INTERNAL***. Do not use.
        [switch] $SkipCommit
    )

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

        $getArgs = @{}
        if ($CollectionName)
        {
            $getArgs['Name'] = $CollectionName
        }

        $displayPath = ''
        if ($ConfigurationElement)
        {
            $getArgs['ConfigurationElement'] = $ConfigurationElement
            $displayPath = $ConfigurationElement.ElementTagName
        }
        else
        {
            $getArgs['SectionPath'] = $SectionPath
            if ($LocationPath)
            {
                $getArgs['LocationPath'] = $LocationPath
            }
            $displayPath =
                Get-CIisDisplayPath -SectionPath $SectionPath -LocationPath $LocationPath -SubSectionPath $CollectionName
        }

        $stopProcessing = $false

        $collection = Get-CIisCollection @getArgs
        if (-not $collection)
        {
            $stopProcessing = $true
            return
        }

        if (-not $UniqueKeyAttributeName)
        {
            $UniqueKeyAttributeName = Get-CIisCollectionKeyName -Collection $collection

            if (-not $UniqueKeyAttributeName)
            {
                $stopProcessing = $true
                $msg = "Failed to remove items from IIS configuration collection ${displayPath} because that " +
                       'collection doesn''t have a unique key attribute. Use the "UniqueKeyAttributeName" parameter ' +
                       'to specify the attribute name.'
                Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                return
            }
        }

        $firstLine = "IIS configuration collection ${displayPath}"
        $firstLineWritten = $false

        $itemsRemoved = $false
    }

    process
    {
        if ($stopProcessing)
        {
            return
        }

        foreach ($valueItem in $Value)
        {
            $itemToRemove = $collection | Where-Object { $_.GetAttributeValue($UniqueKeyAttributeName) -eq $valueItem }

            if (-not $itemToRemove)
            {
                $msg = "Failed to remove item ""${valueItem}"" from IIS configuration collection ${displayPath} " +
                       'because it doesn''t exist in the collection.'
                Write-Error $msg -ErrorAction $ErrorActionPreference
                return
            }

            if (-not $firstLineWritten)
            {
                Write-Information $firstLine
                $firstLineWritten = $true
            }

            Write-Information " - $($valueItem)"
            $collection.Remove($itemToRemove)
            $itemsRemoved = $true
        }
    }

    end
    {
        if ($stopProcessing -or -not $itemsRemoved)
        {
            return
        }

        if ($SkipCommit)
        {
            return $true
        }

        Save-CIisConfiguration
    }
}


function Remove-CIisConfigurationAttribute
{
    <#
    .SYNOPSIS
    Removes an attribute from a configuration section.
 
    .DESCRIPTION
    The `Remove-CIisConfigurationAttribute` function removes an attribute from a configuration section in the IIS
    application host configuration file. Pass the configuration section path to the `SectionPath` parameter, and the
    names of the attributes to remove to the `Name` parameter (or pipe the names to
    `Remove-CIisConfigurationAttribute`). The function deletes that attribute. If the attribute doesn't exist, nothing
    happens.
 
    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.
 
    To remove an attribute from an arbitrary configuration element, pass the configuration element to the
    `ConfigurationElement` parameter. You must also pass the xpath to that element to the `ElementXpath` parameter
    because the Microsoft.Web.Administration API doesn't expose a way to determine if an attribute no longer exists in
    the applicationHost.config file, so `Remove-CIisConfigurationAttribute` has to check.
 
    .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.
 
    .EXAMPLE
    Remove-CIisConfigurationAttribute -ConfigurationElement $vdir -ElementXpath "system.applicationHost/sites/site[@name = 'site']/application[@path = '/']/virtualDirectory[@path = '/']" -Name 'logonMethod'
 
    Demonstrates how to remove an attribute from an arbitrary configuration element. In this example, the `logonMethod`
    attribute will be removed from the `site` site's default virtual directory.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The configuration element whose attribute to remove.
        [Parameter(Mandatory, ParameterSetName='ByConfigurationElement')]
        [ConfigurationElement] $ConfigurationElement,

        # The xpath expression to the configuration element in the applicationHost.config file, without the
        # `/configuration` root path. The Microsoft.Web.Administration API doesn't expose a way to check if an attribute
        # is defined or is missing and has its default value. This xpath expression is used to check if an attribute
        # exists or not.
        [Parameter(Mandatory, ParameterSetName='ByConfigurationElement')]
        [String] $ElementXpath,

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

        # OBSOLETE. Use the `LocationPath` parameter instead.
        [Parameter(ParameterSetName='BySectionPath')]
        [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, ParameterSetName='BySectionPath')]
        [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

        $desc = ''
        if ($ConfigurationElement)
        {
            $desc = Get-CIisDescription -ConfigurationElement $ConfigurationElement
        }
        else
        {
            $ConfigurationElement = Get-CIisConfigurationSection -SectionPath $SectionPath `
                                                                 -LocationPath $LocationPath `
                                                                 -VirtualPath $VirtualPath
            if( -not $ConfigurationElement )
            {
                return
            }

            $desc = Get-CIisDescription -SectionPath $SectionPath -LocationPath $LocationPath
        }

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

        $save = $false
    }

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

        $shownDescription = $false
        Write-Debug $desc

        foreach( $nameItem in $Name )
        {
            $attr = $ConfigurationElement.Attributes[$nameItem]
            if( -not $attr )
            {
                $msg = "${desc} 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))"

            $msg = " $($nameFormat -f $nameItem)] $($attr.IsInheritedFromDefaultValue) $($attr.Value) " +
                   "$($attr.Schema.DefaultValue)"
            Write-Debug $msg

            $exists = $false
            if ($PSCmdlet.ParameterSetName -eq 'ByConfigurationElement')
            {
                $exists = Test-CIisApplicationHostElement -Xpath "${ElementXpath}/@${nameItem}"
            }
            else
            {
                $exists =
                    Test-CIisApplicationHostElement -Xpath "${SectionPath}/@${nameItem}" -LocationPath $LocationPath
            }

            if (-not $exists)
            {
                Write-Verbose "Attribute ${nameItem} on ${desc} does not exist."
                continue
            }

            $target = "$($nameItem) on ${desc}"
            $action = 'Remove Attribute'
            if( $PSCmdlet.ShouldProcess($target, $action) )
            {
                if (-not $shownDescription)
                {
                    Write-Information $desc
                    $shownDescription = $true
                }
                Write-Information " - ${nameItem}"
                $attr.Delete()
                $save = $true
            }
        }
    }

    end
    {
        if (-not $ConfigurationElement)
        {
            return
        }

        if (-not $save)
        {
            return
        }

        Save-CiisConfiguration
    }
}


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,

        [String] $SectionPath
    )

    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 = "IIS configuration location ""${LocationPath}"" does not exist."
        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        return
    }

    if (-not $SectionPath)
    {
        (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
        return
    }

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

    $desc = Get-CIisDescription -ConfigurationElement $section
    if (-not (Test-CIisApplicationHostElement -XPath $section.SectionPath -LocationPath $section.LocationPath))
    {
        $msg = "Failed to delete ${desc} because that configuration section does not exist for that location."
        Write-Error -Message $msg -ErrorAction $ErrorActionPreference
        return
    }

    $msg = "Removing ${desc}."
    Write-Information $msg
    $section.Delete()
    Save-CIisConfiguration
}


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 to remove 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 Resume-CIisAutoCommit
{
    <#
    .SYNOPSIS
    Starts Carbon.IIS functions committing changes to IIS.
 
    .DESCRIPTION
    The `Resume-CIisAutoCommit` functions starts Carbon.IIS functions committing changes to IIS. Some IIS configuration
    is only committed correctly when an item is first saved/created. To ensure that all the changes made by Carbon.IIS
    are committed at the same time, call `Suspend-CIisAutoCommit`, make your changes, then call `Resume-CIisAutoCommit
    -Save` to start auto-committing again *and* to commit all uncomitted changes.
 
    .EXAMPLE
    Resume-CIisAutoCommit -Save
 
    Demonstrates how to call this function to both start auto-committing changes again *and* to save any uncommitted
    changes.
 
    .EXAMPLE
    Resume-CIisAutoCommit
 
    Demonstrates how to call this function to start auto-committing changes but not to save any uncommitted changes and
    leave them pending in memory.
    #>

    [CmdletBinding()]
    param(
        # When set, will save any uncommitted changes.
        [switch] $Save
    )

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

    $script:skipCommit = $false

    if (-not $Save)
    {
        return
    }

    Save-CIisConfiguration
}



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 `
                                     -Exclude @('applicationPoolSid', 'state') `
                                     -Reset:$Reset `
                                     -Defaults (Get-CIIsAppPool -Defaults) `
                                     -AsDefaults:$AsDefaults
}



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 `
                                     -Defaults (Get-CIIsAppPool -Defaults).Cpu `
                                     -AsDefaults:$AsDefaults
}


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
        $AppPoolName = 'default'
    }

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

    $targetMsg = """$($AppPoolName)"" IIS application pool periodic restart"

    Invoke-SetConfigurationAttribute -ConfigurationElement $appPool.Recycling.PeriodicRestart `
                                     -PSCmdlet $PSCmdlet `
                                     -Target $targetMsg `
                                     -Reset:$Reset `
                                     -Defaults (Get-CIIsAppPool -Defaults).Recycling.PeriodicRestart `
                                     -AsDefaults:$AsDefaults

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

    $currentSchedule = $appPool.Recycling.PeriodicRestart.Schedule
    $currentTimes = $currentSchedule | Select-Object -ExpandProperty 'Time' | Sort-Object
    $currentTimesMsg = $currentTimes -join ', '
    $Schedule = $Schedule | Sort-Object
    $scheduleMsg = $Schedule -join ', '
    $scheduleChanged = $false
    Write-Debug "$($AppPoolName) application pool recycling/periodicRestart/schedule"
    Write-Debug " current $($currentTimesMsg)"
    Write-Debug " new $($scheduleMsg)"
    if( $currentTimesMsg -ne $scheduleMsg )
    {
        $clearedPrefix = $false

        $bothSchedules = & {
                $currentTimes | Write-Output
                $Schedule | Write-Output
            } |
            Where-Object { $_ } |
            Select-Object -Unique

        Write-Information "IIS ""$($AppPoolName)"" application pool periodic restart schedule "
        foreach ($time in $bothSchedules)
        {
            $flag = ' '
            $action = ''
            if( $Schedule -notcontains $time )
            {
                $flag = '-'
                $action = 'Remove'
            }
            elseif( $currentTimes -notcontains $time )
            {
                $flag = '+'
                $action = 'Add'
            }

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

            $action = "$($action) Time"
            $target = "$($time) for '$($AppPoolName)' IIS application pool's periodic restart schedule"
            if( $PSCmdlet.ShouldProcess($target, $action) )
            {

                Write-Information " $($flag) $($time)"
                $scheduleChanged = $true
            }
            if( -not $clearedPrefix )
            {
                $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
                }

                if (-not $WhatIfPreference)
                {
                    [void]$currentSchedule.Add($add)
                }
            }

            if (-not $WhatIfPreference)
            {
                Save-CIisConfiguration
            }
        }
    }
}



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 `
                                     -Defaults (Get-CIIsAppPool -Defaults).ProcessModel `
                                     -AsDefaults:$AsDefaults
}



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 `
                                     -Defaults (Get-CIIsAppPool -Defaults).Recycling `
                                     -AsDefaults:$AsDefaults
}


function Set-CIisCollection
{
    <#
    .SYNOPSIS
    Sets the exact contents of an IIS configuration section collection.
 
    .DESCRIPTION
    The `Set-CIisCollection` function sets IIS configuration collection to a specific set of items. Pipe the collection
    items to the function (or pass them to the `InputObject` parameter). To set just the item's default value/attribute,
    pass a raw value. To set more than one of the item's attributes, pass them as a hashtable. By default, extra
    attributets on the item are ignored and left as-is. To remove any item attributes that aren't passed to the
    function, use the `Strict` switch.
 
    To make changes to a global configuration section, pass its path to the `SectionPath` parameter. To make changes to
    a site, directory, application, or virtual directory, pass its pass location path to the `LocationPath`. To make
    changes to a specific `[Microsoft.Web.Administration.ConfigurationElement]` object, pass it to the
    `ConfigurationElement` parameter.
 
    When making changes directly to ConfigurationElement objects, test that those changes are saved correctly to the IIS
    application host configuration. Some configuration has to be saved at the same time as its parent configuration
    elements are created (i.e. sites, application pools, etc.). Use the `Suspend-CIisAutoCommit` and
    `Resume-CIisAutoCommit` functions to ensure configuration gets committed simultaneously.
 
    .EXAMPLE
    @{ name = 'HttpLoggingModule' ; image = '%windir%\System32\inetsrv\loghttp.dll' } | Set-CIisCollection -SectionPath 'system.webServer/globalModules'
 
    Demonstrates how to set a collection to a specific list where each item in the collection has more than a single
    default attribute. The above command would result in the applicationHost.config file to be updated to be:
 
        <system.webServer>
            <globalModules>
                <add name="HttpLoggingModule" image="%windir%\System32\inetsrv\loghttp.dll" />
            </globalModules>
        </system.webServer>
 
    .EXAMPLE
    'default.aspx', 'index.html' | Set-CIisCollection -LocationPath 'SITE_NAME' -SectionPath 'system.webServer/defaultDocument' -Name 'files'
 
    Demonstrates how to set an IIS collection to have only a specific list of values by piping them to the function.
    After the above command runs, this will be in the applicationHost.config:
 
        <location path="SITE_NAME">
            <system.webServer>
                <defaultDocument>
                    <files>
                        <add value="default.aspx" />
                        <add value="index.html" />
                    </files>
                </defaultDocument>
            </system.webServer>
        </location>
 
    #>

    [CmdletBinding(DefaultParameterSetName='BySectionPath')]
    param(
        # The configuration element on which to operate. If not a collection, pass the name of the collection under this
        # element to the `Name` parameter.
        [Parameter(Mandatory, ParameterSetName='ByConfigurationElement')]
        [ConfigurationElement] $ConfigurationElement,

        # The path to the collection.
        [Parameter(Mandatory, ParameterSetName='BySectionPath')]
        [String] $SectionPath,

        # The site the collection belongs to.
        [Parameter(ParameterSetName='BySectionPath')]
        [String] $LocationPath,

        # The name of the collection to change.
        [String] $Name,

        # Pass a hashtable for each item that has more than one attribute value to set. Otherwise, pass the attribute
        # value for the default attribute.
        [Parameter(ValueFromPipeline)]
        [Object[]] $InputObject,

        # The attribute name for the attribute that uniquely identifies each item in a collection. This is usually
        # automatically detected.
        [String] $UniqueKeyAttributeName,

        # By default, extra attributes on collection items are ignored. If this switch is set, any attributes not passed
        # to `Set-CIisCollection` are removed from collection items.
        [switch] $Strict
    )

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

        $stopProcessing = $false

        $getSetArgs = @{}
        if ($Name)
        {
            $getSetArgs['Name'] = $Name
        }

        $displayPath = ''
        if ($ConfigurationElement)
        {
            $getSetArgs['ConfigurationElement'] = $ConfigurationElement
            $displayPath = $ConfigurationElement.ElementTagName
        }
        else
        {
            $getSetArgs['SectionPath'] = $SectionPath

            if ($LocationPath)
            {
                $getSetArgs['LocationPath'] = $LocationPath
            }

            $displayPath =
                Get-CIisDisplayPath -SectionPath $SectionPath -LocationPath $LocationPath -SubSectionPath $Name
        }

        $collection = Get-CIisCollection @getSetArgs

        if (-not $collection)
        {
            $stopProcessing = $true
            return
        }

        $items = [List[hashtable]]::New()
        $keyValues = @{}

        if (-not $UniqueKeyAttributeName)
        {
            $UniqueKeyAttributeName = Get-CIisCollectionKeyName -Collection $collection

            if (-not $UniqueKeyAttributeName)
            {
                $msg = "Failed to set IIS configuration collection ${displayPath} because it does not have a unique " +
                       'key attribute. Use the "UniqueKeyAttributeName" parameter to specify the attribute name.'
                Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                $stopProcessing = $true
                return
            }
        }

        $removeSetArgs = @{}
        if ($UniqueKeyAttributeName)
        {
            $removeSetArgs['UniqueKeyAttributeName'] = $UniqueKeyAttributeName
        }
    }

    process
    {
        if ($stopProcessing)
        {
            return
        }

        foreach ($item in $InputObject)
        {
            if ($item -isnot [IDictionary])
            {
                $item = @{ $UniqueKeyAttributeName = $item }
            }

            $items.Add($item)
            $keyValues[$item[$UniqueKeyAttributeName]] = $true
        }
    }

    end
    {
        if ($stopProcessing)
        {
            return
        }

        $itemsToRemove =
            $collection |
            ForEach-Object { $_.GetAttributeValue($UniqueKeyAttributeName) } |
            Where-Object { -not $keyValues.ContainsKey($_) }

        $itemsRemoved = $itemsToRemove | Remove-CIisCollectionItem @getSetArgs @removeSetArgs -SkipCommit

        $itemsModified = $items | Set-CIisCollectionItem @getSetArgs @removeSetArgs -SkipCommit -Strict:$Strict

        if ($itemsRemoved -or $itemsModified)
        {
            Save-CIisConfiguration
        }
    }
}


function Set-CIisCollectionItem
{
    <#
    .SYNOPSIS
    Adds a new item to an IIS configuration collection.
 
    .DESCRIPTION
    The `Set-CIisCollectionItem` function adds a new item or updates the configuration of an existing IIS configuration
    collection item. Pipe the item value to the function, or pass it to the `InputObject` parameter. If the collection
    items you're configuring have only one attribute/value, pass just the value. Otherwise, pass a hashtable of
    attribute names/values. By default, only attributes passed in are added/set in the collection item. To delete any
    attributes not passed, use the `Strict` switch.
 
    To configure a collection that is part of a global configuration section, pass the configuration section's path to
    the `SectionPath` parameter. If the configuration section itself isn't a collection, pass the name of the collection
    to the `CollectionName` parameter. To configure a configuration section for a specific site, directory, application,
    or virtual directory, pass its location path to the `LocationPath` parameter. To configure a specific
    `[Microsoft.Web.Administration.ConfigurationElement]` item (i.e. a site, application pool, etc.), pass that object
    to the `ConfigurationElement` parameter. If the configuration element itself isn't a collection, pass the name of
    the object' collection property to the `CollectionName` parameter.
 
    When making changes directly to ConfigurationElement objects, test that those changes are saved correctly to the IIS
    application host configuration. Some configuration has to be saved at the same time as its parent configuration
    elements are created (i.e. sites, application pools, etc.). Use the `Suspend-CIisAutoCommit` and
    `Resume-CIisAutoCommit` functions to ensure configuration gets committed simultaneously.
 
    .EXAMPLE
    Set-CIisCollectionItem -SectionPath 'system.webServer/defaultDocument' -CollectionName 'files' -Value 'welcome.html'
 
    Demonstrates how to add an item to a configuration collection under a configuration section. This example will add
    "welcome.html" to the list of default documents.
 
    .EXAMPLE
    Set-CIisCollectionItem -LocationPath 'example.com' -SectionPath 'system.webServer/defaultDocument' -CollectionName 'files' -Value 'welcome.html'
 
    Demonstrates how to add an item to a site, directory, application, or virtual directory by using the `LocationPath`
    parameter. In this example, the "example.com" website will be configured to include "welcome.html" as a default
    document.
 
    .EXAMPLE
    @{ 'name' = 'X-Example' ; value = 'example' } | Set-CIisCollectionItem -SectionPath 'system.webServer/httpProtocol/customHeaders' -CollectionName 'files'
 
    Demonstrates how to add items that have multiple attributes by piping a hashtable of attribute names/values to
    the function. In this example, `Set-CIIsCollectionItem` will add `X-Example` HTTP header with a value of `example`
    to global configuration.
    #>

    [CmdletBinding(DefaultParameterSetName='BySectionPath')]
    param(
        # The `[Microsoft.Web.Administration.ConfigurationElement]` object to get as a collection or the parent element
        # of the collection element to get. If this is the parent element, pass the name of the child element collection
        # to the `CollectionName` parameter.
        [Parameter(Mandatory, ParameterSetName='ByConfigurationElement')]
        [ConfigurationElement] $ConfigurationElement,

        # The path to the collection's configuration section.
        [Parameter(Mandatory, ParameterSetName='BySectionPath')]
        [String] $SectionPath,

        # The location path to the site, directory, application, or virtual directory to configure. By default, the
        # global configuration will be updated.
        [Parameter(ParameterSetName='BySectionPath')]
        [String] $LocationPath,

        # The value for the IIS collection's identifying key.
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object] $InputObject,

        # The name of the IIS collection to modify. If not provided, will use the SectionPath as the collection.
        [Alias('Name')]
        [String] $CollectionName,

        # The attribute name for the attribute that uniquely identifies each item in a collection. This is usually
        # automatically detected.
        [String] $UniqueKeyAttributeName,

        # If set, remove any attributes that aren't passed in.
        [switch] $Strict,

        # ***INTERNAL***. Do not use.
        [switch] $SkipCommit
    )

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

        function Write-Message
        {
            [CmdletBinding()]
            param(
                [Parameter(Mandatory)]
                [String] $Message
            )

            if (-not $firstLineWritten)
            {
                Write-Information $firstLine
                Set-Variable -Name 'firstLineWritten' -Value $true -Scope 1
            }

            if (-not $keyValueWritten)
            {
                Write-Information " ${keyValue}"
                Set-Variable -Name 'keyValueWritten' -Value $true -Scope 1
            }

            Write-Information $Message
        }

        $firstLine = 'IIS configuration collection '
        $firstLineWritten = $false
        $keyValueWritten = $false

        $save = $false
        $collectionArgs = @{}

        $elementPath = ''
        if ($ConfigurationElement)
        {
            $firstLine = "${firstLine}$($ConfigurationElement.ElementTagName)"
            $collectionArgs['ConfigurationElement'] = $ConfigurationElement
            $elementPath = $ConfigurationElement.ElementTagName
            if (Get-Member -Name 'SectionPath' -InputObject $ConfigurationElement)
            {
                $elementPath = $ConfigurationElement.SectionPath
            }
        }
        else
        {
            $displayPath = Get-CIisDisplayPath -SectionPath $SectionPath `
                                               -LocationPath $locationPath `
                                               -SubSectionPath $CollectionName
            $firstLine = "${firstLine}${displayPath}"
            $elementPath = $SectionPath
            $collectionArgs['SectionPath'] = $SectionPath
            if ($LocationPath)
            {
                $collectionArgs['LocationPath'] = $LocationPath
            }
        }

        if ($CollectionName)
        {
            $elementPath = "${elementPath}/$($CollectionName)"
            $collectionArgs['Name'] = $CollectionName
        }

        $collection = Get-CIisCollection @collectionArgs

        if (-not $collection)
        {
            return
        }

        if (-not $UniqueKeyAttributeName)
        {
            $UniqueKeyAttributeName = Get-CIisCollectionKeyName -Collection $collection

            if (-not $UniqueKeyAttributeName)
            {
                $msg = "Failed to set IIS configuration collection ${displayPath} because it does not have a unique " +
                       'key attribute. Use the "UniqueKeyAttributeName" parameter to specify the attribute name.'
                Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                return
            }
        }
    }

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

        if ($InputObject -isnot [IDictionary])
        {
            $InputObject = @{ $UniqueKeyAttributeName = $InputObject }
        }

        if (-not $InputObject.Contains($UniqueKeyAttributeName))
        {
            $msg = "Failed to add item to collection ""$($collection.Path)"" because the attributes of the item are " +
                   "missing a value for the key attribute ""${UniqueKeyAttributeName}""."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $keyValue = $InputObject[$UniqueKeyAttributeName]

        $item = $collection | Where-Object { $_.GetAttributeValue($UniqueKeyAttributeName) -eq $keyValue }

        if (-not $item)
        {
            $keyValueWritten = $true
            Write-Message " + ${keyValue}"

            $addElementName = $collection.Schema.AddElementNames
            $item = $collection.CreateElement($addElementName)
            foreach ($attrName in $InputObject.Keys)
            {
                if ($attrName -ne $UniqueKeyAttributeName)
                {
                    Write-Message " + ${attrName} $($InputObject[$attrName])"
                }
                $item.SetAttributeValue($attrName, $InputObject[$attrName])
            }
            [void]$collection.Add($item)
            $save = $true
        }
        else
        {
            $attrNameFieldLength =
                $InputObject.Keys |
                Select-Object -ExpandProperty 'Length' |
                Measure-Object -Maximum |
                Select-Object -ExpandProperty 'Maximum'
            $attrNameFormat = "{0,-${attrNameFieldLength}}"
            foreach ($attrName in $InputObject.Keys)
            {
                $expectedValue = $InputObject[$attrName]
                $actualValue = $item.GetAttributeValue($attrName)

                if ($expectedValue -eq $actualValue)
                {
                    continue
                }

                $flag = ' '
                $changeMsg = "${actualValue} -> ${expectedValue}"
                $isAddingAttr = $null -eq $actualValue -or '' -eq $actualValue
                if ($isAddingAttr)
                {
                    $flag = '+'
                    $changeMsg = $expectedValue
                }

                $attrDisplayName = $attrNameFormat -f $attrName
                Write-Message " ${flag} ${attrDisplayName} ${changeMsg}"
                $item.SetAttributeValue($attrName, $expectedValue)
                $save = $true
            }
        }

        if ($Strict)
        {
            foreach ($attr in $item.Attributes)
            {
                if ($InputObject.Contains($attr.Name))
                {
                    continue
                }

                Write-Message " - $($attr.Name)"
                [void]$attr.Delete()
                $save = $true
            }
        }
    }

    end
    {
        if (-not $save)
        {
            return
        }

        if ($SkipCommit)
        {
            return $true
        }

        Save-CIisConfiguration
    }
}



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,

        [ConfigurationElement] $Defaults,

        [switch] $AsDefaults
    )

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

    function Get-TypeName
    {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory, ValueFromPipeline)]
            [AllowNull()]
            [AllowEmptyString()]
            [Object] $InputObject
        )

        process
        {
            if ($null -eq $InputObject)
            {
                return ''
            }

            return "[$($InputObject.GetType().FullName -replace 'System\.', '')]"
        }
    }

    function Get-DisplayValue
    {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory, ValueFromPipeline)]
            [AllowNull()]
            [AllowEmptyString()]
            [Object] $InputObject
        )

        process
        {
            if ($null -eq $InputObject)
            {
                return " (null)"
            }

            if ($InputObject -is [String] -and [String]::IsNullOrEmpty($InputObject))
            {
                return " (empty)"
            }

            if ($Sensitive -or $InputObject -is [securestring])
            {
                return "********"
            }

            if ($InputObject -is [Enum])
            {
                $valueAsEnum = [Enum]::Parse($InputObject.GetType().Name, $InputObject, $true)
                return "$($InputObject.ToString()) ($($valueAsEnum.ToString('D')))"
            }

            if ($Value -is [switch])
            {
                return "$($Value.IsPresent)"
            }

            return "$($InputObject.ToString())"
        }
    }

    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)
        {
            $desc = Get-CIisDescription -ConfigurationElement $Element
            $msg = "Unable to set attribute ""$($Name)"" on ${desc} because that attribute doesn't exist. Valid " +
                   "attributes are: $(($Element.Attributes | Select-Object -ExpandProperty 'Name') -join ', ')."
            Write-Error -Message $msg -ErrorAction $ErrorActionPreference
            return
        }

        $currentValue = $currentAttr.Value
        $noCurrentValue = $null -eq $currentValue
        $hasCurrentValue = -not $noCurrentValue
        $currentValueMsg = $currentValue | Get-DisplayValue

        $defaultValue = $defaultValueSchema = $currentAttr.Schema.DefaultValue
        if (-not $AsDefaults -and $Defaults)
        {
            $defaultAttrSchema = $Defaults.GetAttribute($Name)
            if ($null -ne $defaultAttrSchema)
            {
                $defaultValue = $defaultAttrSchema.Value
            }
        }
        $defaultValueMsg = $defaultValue | Get-DisplayValue
        $defaultValueSchemaMsg = $defaultValueSchema | Get-DisplayValue
        $currentValueIsDefault = $currentValue -eq $defaultValue
        if ($currentValue -is [TimeSpan] -and $defaultValue -isnot [TimeSpan])
        {
            $currentValueIsDefault = $currentValue.Ticks -eq $defaultValue
        }
        else
        {
            $currentValueIsNull = $null -eq $currentValue
            $defaultValueIsNull = $null -eq $defaultValue

            if (-not $currentValueIsNull -and -not $defaultValueIsNull)
            {
                $currentValueIsNumeric = [Microsoft.VisualBasic.Information]::IsNumeric($currentValue)
                $currentValueType = $currentValue.GetType()
                $defaultValueIsNumeric = [Microsoft.VisualBasic.Information]::IsNumeric($defaultValue)
                $defaultValueType = $defaultValue.GetType()

                if (-not $currentValueIsNumeric -and `
                    -not $defaultValueIsNumeric -and `
                    $currentValueType -ne $defaultValueType)
                {
                    "Unable to safely determine the state of attribute ""$($Name)"" on IIS configuration element " +
                        """$($Target)"": the current value's type " +
                        "([$($currentValueType.FullName)] $($currentValueMsg)) is different than the default " +
                        "value's type ([$($currentValueType.FullName)] $($currentValueMsg))." | Write-Warning
                }
            }
        }

        $valueMsg = $Value | Get-DisplayValue

        if ($Value -is [Enum] -and $null -ne $currentValue -and $currentValue -isnot [Enum])
        {
            try
            {
                $currentValue = [Enum]::Parse($Value.GetType(), $currentValue.ToString(), $true)
                $currentValueMsg = $currentValue | Get-DisplayValue
            }
            catch
            {
                $Global:Error.RemoveAt(0)
            }
        }
        if ($currentValue -is [Enum] -and $null -ne $Value -and $Value -isnot [Enum])
        {
            try
            {
                $Value = [Enum]::Parse($currentValue.GetType(), $Value.ToString(), $true)
                $valueMsg = $Value | Get-DisplayValue
            }
            catch
            {
                $Global:Error.RemoveAt(0)
            }
        }

        $newValue = $Value
        if ($newValue -is [securestring])
        {
            $newValue = [pscredential]::New('i', $newValue).GetNetworkCredential().Password
            $currentValueMsg = '********'
        }

        if ($Sensitive -or $currentAttr.Name -eq 'password')
        {
            $valueMsg = '********'
            $currentValueMsg = '********'
        }

        $msgPrefix = " $($nameFormat -f $currentAttr.Name) "
        $emptyPrefixMsg = ' ' * $msgPrefix.Length

        Write-Debug "$($msgPrefix )current $($currentValue | Get-TypeName) $($currentValueMsg)"
        if (-not $AsDefaults)
        {
            "$($emptyPrefixMsg)schema default $($defaultValueSchema | Get-TypeName) $($defaultValueSchemaMsg)" |
                Write-Debug
        }
        Write-Debug "$($emptyPrefixMsg)default $($defaultValue | Get-TypeName) $($defaultValueMsg)"
        Write-Debug "$($emptyPrefixMsg)new $($newValue | Get-TypeName ) $($valueMsg)"
        Write-Debug "$($emptyPrefixMsg)current -eq new $($currentValue -eq $newValue)"
        Write-Debug "$($emptyPrefixMsg)current -eq default $($currentValue -eq $defaultValue)"
        if ($null -ne $currentValue)
        {
            Write-Debug "$($emptyPrefixMsg)current.Equals(new) $($currentValue.Equals($newValue))"
            Write-Debug "$($emptyPrefixMsg)current.Equals(default) $($currentValue.Equals($defaultValue))"
        }

        $whatIfTarget = "$($currentAttr.Name) for $($Target -replace '"', '''')"

        if (-not $PSBoundParameters.ContainsKey('Value') -and $hasCurrentValue)
        {
            if ($currentValueIsDefault)
            {
                return
            }

            $deletedMsg = "$($msgPrefix)- $($currentValueMsg)"
            $infoMessages.Add($deletedMsg)
            $action = "Remove Attribute"
            if ($PSCmdlet.ShouldProcess($whatIfTarget, $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]$updatedNames.Add($currentAttr.Name)
            }
            return
        }

        if ($currentValue -eq $newValue)
        {
            return
        }

        $changedMsg =  "$($msgPrefix)$($currentValueMsg) -> $($valueMsg)"
        if ($noCurrentValue)
        {
            $changedMsg = "$($msgPrefix)+ $($valueMsg)"
        }
        [void]$infoMessages.Add($changedMsg)
        if ($PSCmdlet.ShouldProcess($whatIfTarget, 'Set Attribute'))
        {
            try
            {
                $ConfigurationElement.SetAttributeValue($currentAttr.Name, $newValue)
            }
            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
        }
    }

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

    $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()

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

    if (-not $Target)
    {
        $Target = Get-CIisDescription -ConfigurationElement $ConfigurationElement
    }

    if ($Name)
    {
        Set-AttributeValue -Element $ConfigurationElement -Name $Name -Value $Value
    }
    else
    {
        $attrNames = $Attribute.Keys | Sort-Object
        foreach ($attrName in $attrNames)
        {
            Set-AttributeValue -Element $ConfigurationElement -Name $attrName -Value $Attribute[$attrName]
        }

        if ($Reset)
        {
            $attrNamesToDelete =
                $ConfigurationElement.Attributes |
                Where-Object 'Name' -NotIn $attrNames |
                Select-Object -ExpandProperty 'Name' |
                Sort-Object

            foreach ($attrName in ($attrNamesToDelete))
            {
                Set-AttributeValue -Element $ConfigurationElement -Name $attrName
            }
        }
    }

    if ($updatedNames)
    {
        Write-Information $Target
        $infoMessages | ForEach-Object { Write-Information $_ }

        if (-not $WhatIfPreference)
        {
            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 `
                                     -Defaults (Get-CIIsWebsite -Defaults) `
                                     -AsDefaults:$AsDefaults
}



function Set-CIisWebsiteBinding
{
    <#
    .SYNOPSIS
    Sets configuration on a website's bindings.
 
    .DESCRIPTION
    The `Set-CIisWebsiteBinding` function configures bindings on a website. Pass the website name to the `SiteName`
    parameter. Pass the SSL flags to set on all the website's HTTPS bindings to the `SslFlag` parameter. The function
    will update each HTTPS binding's SSL flags to match what is passed in. If they already match, the function does
    nothing. When setting the SslFlags setting, the function automatically skips non-HTTPS bindings.
 
    To only update a specific binding, pass the binding information to the `BindingInformation` parameter. You can also
    pipe binding information and/or actual binding objects.
 
    .EXAMPLE
    Set-CIisWebsiteBinding -SiteName 'example.com' -SslFlag ([Microsoft.Web.Administration.SslFlags]:Sni)
 
    Demonstrates how to require SNI (i.e. server name indication) on all of the example.com website's HTTPS bindings.
 
    .EXAMPLE
    Set-CIisWebsiteBinding -SiteName 'example.com' -BindingInformation '*:443:example.com' -SslFlag ([Microsoft.Web.Administration.SslFlags]:Sni)
 
    Demonstrates how to only update a specific binding by passing its binding information string to the
    `BindingInformation` parameter.
 
    .EXAMPLE
    Get-CIisWebsite -Name 'example.com' | Select-Object -ExpandProperty 'Bindings' | Where-Object 'Protocol' -EQ 'https' | Where-Object 'Host' -NE '' | Set-CIisWebsiteBinding -SiteName 'example.com' -SslFlag Sni
 
    Demonstrates that you can pipe binding objects into `Set-CIisWebsiteBinding`. In this example, only HTTPS and
    hostname bindings will get updated to have the `Sni` SSL flag.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of the website whose bindings to update.
        [Parameter(Mandatory)]
        [String] $SiteName,

        # The specific binding to set. Binding information must be in the IP-ADDRESS:PORT:HOSTNAME format. Can also be
        # piped in as strings or binding objects.
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [String] $BindingInformation,

        # The SSL flags for each of the website's HTTPS bindings. If a value for this parameter is omitted, the function
        # does nothing (i.e. existing SSL flags are not changed).
        [SslFlags] $SslFlag
    )

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

        $bindings = Get-CIisWebsite -Name $SiteName | Select-Object -ExpandProperty 'Bindings'
        $updated = $false
    }

    process
    {
        foreach ($binding in $bindings)
        {
            if ($BindingInformation -and $binding.BindingInformation -ne $BindingInformation)
            {
                continue
            }

            if ($binding.Protocol -ne 'https')
            {
                continue
            }

            if (-not $PSBoundParameters.ContainsKey('SslFlag'))
            {
                continue
            }

            if ($binding.SslFlags -eq $SslFlag)
            {
                continue
            }

            if ($SslFlag.HasFlag([SslFlags]::Sni))
            {
                if (-not $binding.Host)
                {
                    $msg = "Unable to set SSL flags for binding ""$($binding.BindingInformation)"" because the " +
                           """Sni"" flag is set but the binding doesn't have a hostname."
                    Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                    continue
                }
            }

            $msg = "${SiteName} $($binding.Protocol)/$($binding.BindingInformation) SslFlags $($binding.SslFlags) -> " +
                "${SslFlag}"
            Write-Information $msg
            $binding.SslFlags = $SslFlag
            $updated = $true
        }
    }

    end
    {
        if ($updated)
        {
            Save-CIisConfiguration
        }
    }
}



function Set-CIisWebsiteHttpsCertificate
{
    <#
    .SYNOPSIS
    Sets a website's HTTPS certificate.
 
    .DESCRIPTION
    The `Set-CIisWebsiteHttpsCertificate` sets the HTTPS certificate for all of a website's HTTPS bindings. Pass the
    website name to the SiteName parameter, the certificate thumbprint to the `Thumbprint` parameter (the certificate
    should be in the LocalMachine's My store), and the website's application ID (a GUID that uniquely identifies the
    website) to the `ApplicationID` parameter. The function gets all the unique IP address/port HTTPS bindings and
    creates a binding for that address/port to the given certificate. Any HTTPS bindings on that address/port that
    don't use this thumbprint and application ID are removed.
 
    Make sure you call this method *after* you create a website's bindings.
 
    .EXAMPLE
    Set-CIisWebsiteHttpsCertificate -SiteName Peanuts -Thumbprint 'a909502dd82ae41433e6f83886b00d4277a32a7b' -ApplicationID $PeanutsAppID
 
    Binds the certificate whose thumbprint is `a909502dd82ae41433e6f83886b00d4277a32a7b` to the `Peanuts` website.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of the website whose HTTPS certificate is being set.
        [Parameter(Mandatory)]
        [string] $SiteName,

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

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

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

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

    foreach ($binding in ($site.Bindings | Where-Object 'Protocol' -EQ 'https'))
    {
        $endpoint = $binding.Endpoint

        $portArg = @{
            Port = $endpoint.Port;
        }
        if ($endpoint.Port -eq '*')
        {
            $portArg['Port'] = 443
        }

        Set-CHttpsCertificateBinding -IPAddress $binding.Endpoint.Address `
                                    @portArg `
                                    -Thumbprint $Thumbprint `
                                    -ApplicationID $ApplicationID
    }
}



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 `
                                     -Defaults (Get-CIIsWebsite -Defaults).Limits `
                                     -AsDefaults:$AsDefaults
}



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 `
                                     -Defaults (Get-CIIsWebsite -Defaults).LogFile `
                                     -AsDefaults:$AsDefaults
}



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.
 
    The Windows Process Activation Service (WAS) must be running in order for an application pool to start. The
    `Start-CIisAppPool` function will attempt to start the WAS if it exists and isn't running.
 
    .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
        }

        $waSvc = Get-Service -Name 'WAS' -ErrorAction Ignore
        if ($waSvc -and $waSvc.Status -ne [ServiceControllerStatus]::Running)
        {
            Write-Information "Starting ""$($waSvc.DisplayName)"" ($($waSvc.Name))."
            Start-Service -Name 'WAS'
        }

        $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
                {
                    # We're delaying saving configuration changes, so the app pool might not exist. Don't try to start
                    # a non-existent app pool.
                    if ($script:skipCommit)
                    {
                        return
                    }

                    $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.
 
    The World Wide Web Publishing Service (W3SVC) must be running for a website to start. The `Start-CIisWebsite` will
    attempt to start the W3SVC if it exists and isn't running.
 
    .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
        }

        $w3Svc = Get-Service 'W3SVC' -ErrorAction Ignore
        if ($w3Svc -and $w3Svc.Status -ne [ServiceControllerStatus]::Running)
        {
            Write-Information "Starting ""$($w3Svc.DisplayName)"" ($($w3Svc.Name))."
            Start-Service -Name 'W3SVC'
        }

        $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
                {
                    if ($script:skipCommit)
                    {
                        return
                    }

                    $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
                {
                    if ($script:skipCommit)
                    {
                        return
                    }

                    $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
                {
                    if ($script:skipCommit)
                    {
                        return
                    }
                    $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 Suspend-CIisAutoCommit
{
    <#
    .SYNOPSIS
    Stops Carbon.IIS functions from committing changes to IIS.
 
    .DESCRIPTION
    The `Suspend-CIisAutoCommit` functions stops Carbon.IIS functions from committing changes to IIS. Some IIS
    configuration is only committed correctly when an item is first saved/created. To ensure that all the changes made
    by Carbon.IIS are committed at the same time, call `Suspend-CIisAutoCommit`, make your changes, then call
    `Resume-CIisAutoCommit -Save` to start auto-committing again *and* to commit all uncomitted changes.
 
    .EXAMPLE
    Suspend-CIisAutoCommit
 
    Demonstrates how to call this function.
    #>

    [CmdletBinding()]
    param(
    )

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

    $script:skipCommit = $true
}


function Test-CIisApplicationHostElement
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [String] $XPath,

        [String] $LocationPath
    )

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

    $xml = [xml](Get-Content -Path $script:applicationHostPath -Raw)
    $element = $xml.DocumentElement
    if ($LocationPath)
    {
        $element = $element.SelectSingleNode("location[@path = ""$($LocationPath.TrimStart('/'))""]")
        if (-not $element)
        {
            return $false
        }
    }

    $element = $element.SelectSingleNode($XPath)
    if ($element)
    {
        return $true
    }

    return $false
}


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-CIisApplication
{
    <#
    .SYNOPSIS
    Delete an IIS application.
 
    .DESCRIPTION
    The `Uninstall-CIisApplication` function deletes an application. Pass the application's site name to the `SiteName`
    parameter. Pass the application's virtual path to the `VirtualPath` parameter. If the application exists, it is
    deleted. If it doesn't exist, nothing happens.
 
    The function will not delete a site's default, root application at virtual path `/` and will instead write an error.
 
    .EXAMPLE
    Uninstall-CIisApplication -SiteName 'site' -VirtualPath '/some/app'
 
    Demonstrates how to use this function to delete an IIS application. In this example, the `/some/app` application
    under the `site` site will be removed, if it exists.
    #>

    [CmdletBinding()]
    [OutputType([Microsoft.Web.Administration.Application])]
    param(
        # The applicatoin's site.
        [Parameter(Mandatory)]
        [String] $SiteName,

        # The application's virtual path.
        [Parameter(Mandatory)]
        [Alias('Name')]
        [String] $VirtualPath
    )

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

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

    $save = $false
    foreach ($site in $sites)
    {
        $apps = Get-CIisApplication -SiteName $site.Name -VirtualPath $VirtualPath
        if (-not $apps)
        {
            Write-Verbose "IIS application ""${VirtualPath}"" under site ""$($site.Name)"" does not exist."
            continue
        }

        foreach ($app in $apps)
        {
            if ($app.Path -eq '/')
            {
                $msg = "Failed to delete IIS application ""$($app.Path)}"" under site ""$($site.Name)"" because it " +
                       'is the root, default application. Use the "Uninstall-CIisWebsite" function to uninstall IIS ' +
                       'sites.'
                Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                continue
            }

            Write-Information "Deleting IIS application ""$($app.Path)"" under site ""$($site.Name)""."
            $apps = Get-CIisCollection -ConfigurationElement $site
            $appToRemove = $apps | Where-Object { $_.GetAttributeValue('path') -eq $app.Path }
            $apps.Remove($appToRemove)
            $save = $true
        }
    }

    if ($save)
    {
        Save-CIisConfiguration
    }
}


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-CIisVirtualDirectory
{
    <#
    .SYNOPSIS
    Delete an IIS virtual directory.
 
    .DESCRIPTION
    The `Uninstall-CIisVirtualDirectory` function deletes a virtual directory. Pass the virtual directory's site name to
    the `SiteName` parameter. Pass the virtual directory's virtual path to the `VirtualPath` parameter. If the virtual
    directory exists, it is deleted. If it doesn't exist, nothing happens. If the virtual directory is under an
    application, pass the application's path to the `ApplicationPath` parameter.
 
    The function will not delete a site's default, root application at virtual path `/` and will instead write an error.
 
    .EXAMPLE
    Uninstall-CIisVirtualDirectory -SiteName 'site' -VirtualPath '/some/vdir'
 
    Demonstrates how to use this function to delete an IIS virtual directory. In this example, the `/some/vdir` virtual
    directory under the `site` site will be removed, if it exists.
 
    .EXAMPLE
    Uninstall-CIisVirtualDirectory -SiteName 'site' -ApplicationPath 'app' -VirtualPath '/some/vdir'
 
    Demonstrates how to use this function to delete an IIS virtual directory that exists under an application. In this
    example, the `/some/vdir` virtual directory under the `site` site's '/app' app will be removed, if it exists.
    #>

    [CmdletBinding()]
    [OutputType([Microsoft.Web.Administration.Application])]
    param(
        # The virtual directory's site.
        [Parameter(Mandatory)]
        [String] $SiteName,

        # The virtual directory's virtual path.
        [Parameter(Mandatory)]
        [Alias('Name')]
        [String] $VirtualPath,

        # The path of the virtual directory's application. The default is to look for the the virtual directory under
        # the site.
        [String] $ApplicationPath
    )

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

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

    if (-not $ApplicationPath)
    {
        $ApplicationPath = '/'
    }

    $ApplicationPath = $ApplicationPath | ConvertTo-CIisVirtualPath

    $save = $false
    foreach ($site in $sites)
    {
        $app = $null
        $appDesc = ''

        $apps = Get-CIisApplication -SiteName $site.Name -VirtualPath $ApplicationPath
        if (-not $apps)
        {
            continue
        }

        foreach ($app in $apps)
        {
            $appDesc = ''
            $suggestedCmd = 'Uninstall-CIisWebsite'
            if ($app.Path -ne '/')
            {
                $appDesc = " under application ""$($app.Path)"""
                $suggestedCmd = 'Uninstall-CIisApplication'
            }

            $desc = "IIS virtual directory ""${VirtualPath}""${appDesc} under site ""$($site.Name)"""

            $vdirs = Get-CIisVirtualDirectory -SiteName $site.Name `
                                              -VirtualPath $VirtualPath `
                                              -ApplicationPath $ApplicationPath `
                                              -ErrorAction Ignore
            if (-not $vdirs)
            {
                Write-Verbose "${desc} does not exist."
                continue
            }

            foreach ($vdir in $vdirs)
            {
                $desc = "IIS virtual directory ""$($vdir.Path)""${appDesc} under site ""$($site.Name)"""
                if ($vdir.Path -eq '/')
                {
                    $msg = "Failed to delete ${desc} because it is the root, default virtual directory. Use the " +
                        """${suggestedCmd}"" function instead."
                    Write-Error -Message $msg -ErrorAction $ErrorActionPreference
                    continue
                }

                $vdirToDelete = $app.VirtualDirectories | Where-Object 'Path' -EQ $vdir.Path
                if (-not $vdirToDelete)
                {
                    Write-Verbose "${desc} does not exist."
                    continue
                }

                Write-Information "Deleting ${desc}."
                $app.VirtualDirectories.Remove($vdirToDelete)
                $save = $true
            }
        }
    }

    if ($save)
    {
        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;
    'Get-CIisCollectionKeyName' = $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