Private/Kinds/WebFolder.ps1

# The WebFolder Kind: what a drive on a web folder (WebDAV) needs, and whether it has it.
#
# A drive mapped to SharePoint or to another web folder is slow, or refuses a file, for a
# handful of known reasons, and Windows keeps no event log of its WebDAV client to read
# them from: no provider for it is registered (docs/research/network-protocol-events.md).
# So this Kind reads the state instead, and only where the User has such a drive:
#
# - the WebClient service, which is the WebDAV client. It starts when first needed, so
# "stopped" is its ordinary state; switched off, no web drive can connect.
# - the largest file it will take: 50 000 000 bytes unless somebody raised it, and a
# larger file fails with an error that does not name the limit.
# - "automatically detect settings" of the User's proxy configuration. With it on, the
# client looks for a proxy before each first access to a server, which is the wait a
# User describes as "the drive hangs when I open it".
#
# Of a drive only its Server is kept: the path under it names what somebody works on
# (ADR-0006).
#
# What the Gatherer returns:
# Drives { Letter, Server }, the User's drives that are web folders
# ServiceStatus Running, Stopped ... or '' where there is no such service
# ServiceStartType Manual, Disabled ...
# FileSizeLimit bytes, or $null where the service's default applies
# AutoDetect $true, $false, or $null where that could not be read

# What the WebClient service takes when nothing says otherwise.
$script:WebFolderDefaultFileSizeLimit = 50000000

function ConvertTo-WebFolderServer {
    <#
    .SYNOPSIS
        The Server of a web folder, out of the path Windows keeps for the drive. Empty
        for a path that is no web folder. Pure.
    .DESCRIPTION
        A web folder is written as an address, "https://sp.contoso.example/sites/x", or
        as a UNC path with what a file share never has behind the host: "@SSL", a port,
        or the folder "DavWWWRoot".
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()][AllowEmptyString()][string]$Path)

    $text = "$Path".Trim()
    if ($text -match '^(?i)https?://([^/:@\s]+)') { return $Matches[1].ToLowerInvariant() }
    if ($text -match '^\\\\([^\\@\s]+)((?:@[A-Za-z0-9]+)+)(\\|$)') { return $Matches[1].ToLowerInvariant() }
    if ($text -match '^\\\\([^\\@\s]+)\\DavWWWRoot(\\|$)') { return $Matches[1].ToLowerInvariant() }
    ''
}

function Test-WebFolderAutoDetect {
    <#
    .SYNOPSIS
        Whether "automatically detect settings" is on, out of the User's connection
        settings as Windows stores them. $null for what cannot be read. Pure.
    .DESCRIPTION
        The ninth byte of DefaultConnectionSettings holds the switches of the dialog:
        2 a proxy server, 4 a configuration script, 8 automatic detection.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param([AllowNull()][byte[]]$Setting)

    if ($null -eq $Setting -or $Setting.Length -lt 9) { return $null }
    [bool]($Setting[8] -band 0x08)
}

# What Get-WebFolderData hands its Judge. See Private/DataShape.ps1.
$script:WebFolderDataShape = @{
    Drives           = 'List'
    ServiceStatus    = 'Value'
    ServiceStartType = 'Value'
    FileSizeLimit    = 'Value'
    AutoDetect       = 'Value'
}

function Get-WebFolderData {
    <#
    .SYNOPSIS
        The User's drives on web folders and the state of what they need. Decides nothing.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param([hashtable]$Parameters = @{})

    $drives = New-Object System.Collections.Generic.List[psobject]
    $seen   = @{}
    $add    = {
        param([string]$Letter, [string]$Path)
        $server = ConvertTo-WebFolderServer -Path $Path
        $name   = "$Letter".Trim().TrimEnd(':').ToUpperInvariant()
        if (-not $server -or -not $name -or $seen.ContainsKey($name)) { return }
        $seen[$name] = $true
        $drives.Add([pscustomobject]@{ Letter = $name + ':'; Server = $server })
    }

    # The drives that are reconnected at sign-in, and the ones connected now.
    try {
        foreach ($key in @(Get-ChildItem -Path 'HKCU:\Network' -ErrorAction Stop)) {
            $remote = "$((Get-ItemProperty -Path $key.PSPath -ErrorAction SilentlyContinue).RemotePath)"
            & $add $key.PSChildName $remote
        }
    }
    catch { }
    try {
        foreach ($drive in @(Get-PSDrive -PSProvider FileSystem -ErrorAction Stop)) { & $add $drive.Name "$($drive.DisplayRoot)" }
    }
    catch { }

    $status = ''
    $start  = ''
    try {
        $service = Get-Service -Name WebClient -ErrorAction Stop
        $status  = "$($service.Status)"
        $start   = "$($service.StartType)"
    }
    catch { }

    $limit = $null
    try {
        $limit = ConvertTo-Number (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\WebClient\Parameters' -ErrorAction Stop).FileSizeLimitInBytes
    }
    catch { }

    $auto = $null
    try {
        $setting = (Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections' -ErrorAction Stop).DefaultConnectionSettings
        $auto = Test-WebFolderAutoDetect -Setting ([byte[]]$setting)
    }
    catch { }

    [pscustomobject]@{
        PSTypeName       = 'Gutcheck.Data.WebFolder'
        Drives           = @($drives | Sort-Object Letter)
        ServiceStatus    = $status
        ServiceStartType = $start
        FileSizeLimit    = $limit
        AutoDetect       = $auto
    }
}

function ConvertTo-WebFolderFinding {
    [CmdletBinding()]
    [OutputType([psobject])]
    param([AllowNull()]$Data, [hashtable]$Parameters = @{})

    $check  = Get-Text 'Check.WebFolder.State'
    $drives = @((Get-DataCollection $Data 'Drives') | ForEach-Object { $_ })
    if (-not $drives.Count) {
        return New-Finding -Category Network -Check $check -Severity OK -Value (Get-Text 'Value.WebFolder.None')
    }

    $value   = @((Get-Text 'Value.WebFolder.Drives') -f (@($drives | ForEach-Object {
        (Get-Text 'Value.WebFolder.Drive') -f "$(Get-DataProperty $_ 'Letter')", "$(Get-DataProperty $_ 'Server')" }) -join '; '))
    $meaning = @()
    $hint    = @()
    $severity = 'OK'

    # Switched off, or not there at all: no web drive connects. Stopped is how it waits.
    $status = "$(Get-DataProperty $Data 'ServiceStatus')"
    $start  = "$(Get-DataProperty $Data 'ServiceStartType')"
    if (-not $status) {
        $value += Get-Text 'Value.WebFolder.Service.Missing'
        $meaning += Get-Text 'Meaning.WebFolder.Service.Off'
        $hint += Get-Text 'Hint.WebFolder.Service.Off'
        $severity = 'WARN'
    }
    elseif ($start -eq 'Disabled') {
        $value += Get-Text 'Value.WebFolder.Service.Disabled'
        $meaning += Get-Text 'Meaning.WebFolder.Service.Off'
        $hint += Get-Text 'Hint.WebFolder.Service.Off'
        $severity = 'WARN'
    }
    elseif ($status -eq 'Running') { $value += Get-Text 'Value.WebFolder.Service.Running' }
    else { $value += Get-Text 'Value.WebFolder.Service.Waiting' }

    $limit = ConvertTo-Number (Get-DataProperty $Data 'FileSizeLimit')
    if ($null -eq $limit) { $limit = $script:WebFolderDefaultFileSizeLimit }
    $megabytes = [math]::Floor([double]$limit / 1MB)
    $value += (Get-Text 'Value.WebFolder.Limit') -f $megabytes
    $noteAbove = [double](Get-Parameter $Parameters 'FileSizeLimitNoteBelowMB' 100)
    if ($megabytes -lt $noteAbove) {
        $meaning += (Get-Text 'Meaning.WebFolder.Limit') -f $megabytes
        $hint += Get-Text 'Hint.WebFolder.Limit'
        if ($severity -eq 'OK') { $severity = 'INFO' }
    }

    $auto = Get-DataProperty $Data 'AutoDetect'
    if ($null -ne $auto -and [bool]$auto) {
        $value += Get-Text 'Value.WebFolder.AutoDetect.On'
        $meaning += Get-Text 'Meaning.WebFolder.AutoDetect'
        $hint += Get-Text 'Hint.WebFolder.AutoDetect'
        if ($severity -eq 'OK') { $severity = 'INFO' }
    }
    elseif ($null -ne $auto) { $value += Get-Text 'Value.WebFolder.AutoDetect.Off' }

    if ($severity -eq 'OK') { return New-Finding -Category Network -Check $check -Severity OK -Value ($value -join ' | ') }
    New-Finding -Category Network -Check $check -Severity $severity -Value ($value -join ' | ') `
        -Meaning ($meaning -join ' | ') -Hint ($hint -join ' | ')
}