Private/BrowserProfile.ps1

# The browser profiles of the User of the Run: one list, for every Kind that reads a browser.
#
# The BrowserExtension Kind found the profiles of Edge, Chrome and Firefox itself, and the
# Browser Kind needs the same ones. Two Kinds that each look for profiles in their own way
# end up disagreeing about which there are - a profile with the extension in it that the
# Browser Check never mentions - so both ask here, and nowhere else.
#
# Only the folders of the account the Run runs as are read: its own LocalApplicationData
# and ApplicationData. Of another User of the machine nothing is listed (ADR-0006).
#
# The listing is a Gatherer helper: it lists what is there and decides nothing. Beside it
# stand what every Kind that reads a browser reads the same way - an extension's entry and whether it is on, in
# Chromium and in Firefox, and where a browser's program is - so that no two of them call
# one extension on and off, or one browser installed and not.

function Get-BrowserProfileRoot {
    <#
    .SYNOPSIS
        Where each browser keeps the profiles of the User of the Run, whether or not the
        folder is there.
    .DESCRIPTION
        Chrome and Edge are Chromium: one "User Data" folder holds every profile, each a
        folder of its own. Firefox keeps its profiles in a "Profiles" folder of its own.
        In this order, which is the order a Report names them in.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param()

    $local   = [Environment]::GetFolderPath('LocalApplicationData')
    $roaming = [Environment]::GetFolderPath('ApplicationData')
    [pscustomobject]@{ Browser = 'Chrome';  Engine = 'Chromium'; Root = (Join-Path $local 'Google\Chrome\User Data') }
    [pscustomobject]@{ Browser = 'Edge';    Engine = 'Chromium'; Root = (Join-Path $local 'Microsoft\Edge\User Data') }
    [pscustomobject]@{ Browser = 'Firefox'; Engine = 'Firefox';  Root = (Join-Path $roaming 'Mozilla\Firefox\Profiles') }
}

function Get-BrowserProfile {
    <#
    .SYNOPSIS
        Every profile of the User of the Run: the browser it belongs to, the name the User
        knows it by, and the folder it lives in.
    .DESCRIPTION
        A Chromium browser keeps more than profiles in its "User Data" folder: caches,
        components, "System Profile" and "Guest Profile", which no User works in. A profile
        is "Default" or "Profile <n>". A Firefox profile is every folder of "Profiles",
        named "abcd1234.name"; the name is the part after the dot.
    .PARAMETER Browser
        Only the profiles of this browser: Chrome, Edge or Firefox. All three where none is
        named.
    .PARAMETER Root
        Where to look instead of the User's own folder. Given with a Browser.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [AllowEmptyString()][string]$Browser = '',
        [AllowEmptyString()][string]$Root = ''
    )

    foreach ($place in @(Get-BrowserProfileRoot | Where-Object { -not $Browser -or $_.Browser -eq $Browser })) {
        $folder = $place.Root
        if ($Root) { $folder = $Root }
        if (-not (Test-Path -LiteralPath $folder -PathType Container)) { continue }
        $found = @(Get-ChildItem -LiteralPath $folder -Directory -ErrorAction SilentlyContinue)
        if ($place.Engine -eq 'Chromium') { $found = @($found | Where-Object { $_.Name -match '^(Default|Profile \d+)$' }) }
        foreach ($one in $found) {
            $name = $one.Name
            if ($place.Engine -eq 'Firefox') { $name = ($one.Name -split '\.', 2)[-1] }
            [pscustomobject]@{ Browser = $place.Browser; Engine = $place.Engine; Profile = $name; Folder = $one.FullName }
        }
    }
}

function Test-BrowserExtensionOn {
    <#
    .SYNOPSIS
        Whether a Chromium extension's entry in the preferences says it is switched on. Pure.
    .DESCRIPTION
        Older browsers write "state" (1 is on); newer ones leave it out and write the
        reasons it is off, as a number or a list - none means on.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param([AllowNull()]$Entry)

    $reasons = @(@(Get-DataProperty $Entry 'disable_reasons') | Where-Object { $null -ne $_ -and "$_" -ne '0' })
    if ($reasons.Count) { return $false }
    $state = Get-DataProperty $Entry 'state'
    if ($null -ne $state -and "$state" -eq '0') { return $false }
    $true
}

function Get-BrowserPreferenceSetting {
    # The extension entries of a Chromium preferences document, by id, each as a table of
    # its fields; an empty table where the document names none. Pure.
    [CmdletBinding()]
    [OutputType([hashtable])]
    param([AllowNull()]$Document)

    $found = @{}
    if ($null -eq $Document -or $Document -is [string]) { return $found }
    $settings = Get-DataProperty (Get-DataProperty $Document 'extensions') 'settings'
    if ($null -eq $settings -or $settings -is [string] -or $settings -is [ValueType]) { return $found }
    foreach ($one in $settings.PSObject.Properties) {
        $fields = @{}
        if ($one.Value -and $one.Value -isnot [string] -and $one.Value -isnot [ValueType]) {
            foreach ($field in $one.Value.PSObject.Properties) { $fields[$field.Name] = $field.Value }
        }
        $found[$one.Name] = $fields
    }
    $found
}

function Test-BrowserFirefoxExtensionOn {
    # Whether a Firefox add-on, as extensions.json lists it, is switched on: active, and
    # not switched off by the User. Pure.
    [CmdletBinding()]
    [OutputType([bool])]
    param([AllowNull()]$Addon)

    [bool](Get-DataProperty $Addon 'active') -and -not [bool](Get-DataProperty $Addon 'userDisabled')
}

# The program of each browser, as Windows registers it and as the event log names it in a
# crash.
$script:BrowserProgram = @{ Chrome = 'chrome.exe'; Edge = 'msedge.exe'; Firefox = 'firefox.exe' }

function Get-BrowserExecutable {
    <#
    .SYNOPSIS
        Where a browser's program is, as Windows registers it for the machine or for this
        User (App Paths); '' where it registers none.
    .DESCRIPTION
        Every installer of the three registers its program there, so that "Run" finds it.
        The path is as registered: whether the file is still there is the caller's to ask.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([Parameter(Mandatory)][ValidateSet('Chrome', 'Edge', 'Firefox')][string]$Browser)

    foreach ($hive in 'HKLM:', 'HKCU:') {
        $key = '{0}\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\{1}' -f $hive, $script:BrowserProgram[$Browser]
        $path = "$((Get-ItemProperty -LiteralPath $key -ErrorAction SilentlyContinue).'(default)')".Trim('"', ' ')
        if ($path) { return $path }
    }
    ''
}