Public/Add-MSIXCapabilities.ps1


function Add-MSIXCapabilities {
<#
.SYNOPSIS
    Adds capabilities to an expanded MSIX package manifest.

.DESCRIPTION
    Modifies AppxManifest.xml in an expanded MSIX package folder to include the
    specified capabilities. Already-present capabilities are skipped silently.
    Creates the <Capabilities> element if it does not yet exist.

.PARAMETER MSIXFolder
    Path to the expanded MSIX package folder (must contain AppxManifest.xml).

.PARAMETER Capabilities
    One or more capability names to add. Each name is validated against the known
    set of MSIX capabilities and mapped to the correct XML namespace automatically.

.EXAMPLE
    Add-MSIXCapabilities -MSIXFolder "C:\MSIXTemp\MyApp" -Capabilities "internetClient"

.EXAMPLE
    Add-MSIXCapabilities -MSIXFolder "C:\MSIXTemp\MyApp" -Capabilities "internetClient", "privateNetworkClientServer"

.NOTES
    Author: Andreas Nick
    https://www.nick-it.de
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [System.IO.DirectoryInfo] $MSIXFolder,

        [Parameter(Mandatory = $true)]
        [ValidateSet(
            "runFullTrust", "allowElevation",
            "internetClient", "internetClientServer", "privateNetworkClientServer",
            "allJoyn", "codeGeneration",
            "enterpriseAuthentication", "videosLibrary", "musicLibrary", "contacts",
            "appointments", "blockedChatMessages", "picturesLibrary",
            "userAccountInformation", "removableStorage", "phoneCall", "voipCall",
            "chat", "sharedUserCertificates", "objects3D", "phoneCallHistoryPublic",
            "spatialPerception", "remoteSystem", "backgroundMediaPlayback",
            "userNotificationListener", "offlineMapsManagement", "userDataTasks",
            "graphicsCapture", "globalMediaControl", "gazeInput", "recordedCallsFolder",
            "systemManagement", "lowLevelDevices", "accessoryManager",
            "documentsLibrary", "location", "microphone", "webcam", "radios",
            "unvirtualizedResources",
            IgnoreCase = $false)]
        [string[]] $Capabilities
    )

    begin {
        # Maps capability name to its XML element tag (prefix:LocalName or LocalName only)
        $capabilityTag = @{
            "internetClient"             = "Capability"
            "internetClientServer"       = "Capability"
            "privateNetworkClientServer" = "Capability"
            "allJoyn"                    = "Capability"
            "codeGeneration"             = "Capability"
            "enterpriseAuthentication"   = "uap:Capability"
            "videosLibrary"              = "uap:Capability"
            "musicLibrary"               = "uap:Capability"
            "contacts"                   = "uap:Capability"
            "appointments"               = "uap:Capability"
            "blockedChatMessages"        = "uap:Capability"
            "picturesLibrary"            = "uap:Capability"
            "userAccountInformation"     = "uap:Capability"
            "removableStorage"           = "uap:Capability"
            "phoneCall"                  = "uap:Capability"
            "voipCall"                   = "uap:Capability"
            "chat"                       = "uap:Capability"
            "sharedUserCertificates"     = "uap:Capability"
            "objects3D"                  = "uap:Capability"
            "phoneCallHistoryPublic"     = "uap2:Capability"
            "spatialPerception"          = "uap2:Capability"
            "remoteSystem"               = "uap3:Capability"
            "backgroundMediaPlayback"    = "uap3:Capability"
            "userNotificationListener"   = "uap3:Capability"
            "offlineMapsManagement"      = "uap4:Capability"
            "userDataTasks"              = "uap4:Capability"
            "graphicsCapture"            = "uap6:Capability"
            "globalMediaControl"         = "uap7:Capability"
            "gazeInput"                  = "uap7:Capability"
            "recordedCallsFolder"        = "mobile:Capability"
            "systemManagement"           = "iot:Capability"
            "lowLevelDevices"            = "iot:Capability"
            "accessoryManager"           = "rescap:Capability"
            "documentsLibrary"           = "rescap:Capability"
            "runFullTrust"               = "rescap:Capability"
            "allowElevation"             = "rescap:Capability"
            "location"                   = "DeviceCapability"
            "microphone"                 = "DeviceCapability"
            "webcam"                     = "DeviceCapability"
            "radios"                     = "DeviceCapability"
            "unvirtualizedResources"     = "rescap:Capability"
        }
    }

    process {
        $manifestPath = Join-Path $MSIXFolder 'AppxManifest.xml'
        if (-not (Test-Path $manifestPath)) {
            Write-Error "AppxManifest.xml not found in: $($MSIXFolder.FullName)"
            return
        }

        $manifest = New-Object xml
        $manifest.Load($manifestPath)

        $nsmgr = New-Object System.Xml.XmlNamespaceManager($manifest.NameTable)
        $AppXNamespaces.GetEnumerator() | ForEach-Object { $nsmgr.AddNamespace($_.Key, $_.Value) }

        # Ensure <Capabilities> exists
        $capsNode = $manifest.SelectSingleNode('//ns:Capabilities', $nsmgr)
        if ($null -eq $capsNode) {
            $capsNode = $manifest.CreateElement('Capabilities', $AppXNamespaces['ns'])
            $null = $manifest.DocumentElement.AppendChild($capsNode)
            Write-Verbose "Created <Capabilities> element."
        }

        # Collect all prefixes needed and ensure they are declared on the root element
        $prefixesNeeded = $Capabilities | ForEach-Object {
            $t = $capabilityTag[$_]
            if ($t -match '^(?<prefix>[^:]+):') { $Matches.prefix }
        } | Select-Object -Unique
        if ($prefixesNeeded) {
            Add-MSIXManifestNamespace -Manifest $manifest -Prefixes $prefixesNeeded
        }

        $changed = $false
        foreach ($cap in $Capabilities) {
            $tag = $capabilityTag[$cap]

            # Build namespace-aware XPath: plain "Capability" / "DeviceCapability" live in ns namespace
            $xpathTag = if ($tag -eq 'Capability' -or $tag -eq 'DeviceCapability') { "ns:$tag" } else { $tag }
            $existing = $manifest.SelectSingleNode("//ns:Capabilities/$xpathTag[@Name='$cap']", $nsmgr)
            if ($null -ne $existing) {
                Write-Verbose "Capability '$cap' already present - skipped."
                continue
            }

            # Resolve element namespace URI
            $elemNamespace = $AppXNamespaces['ns']
            if ($tag -match '^(?<prefix>[^:]+):') {
                $elemNamespace = $AppXNamespaces[$Matches.prefix]
            }

            $elem = $manifest.CreateElement($tag, $elemNamespace)
            $null = $elem.SetAttribute('Name', $cap)
            $null = $capsNode.AppendChild($elem)
            Write-Verbose "Added capability: $cap ($tag)"
            $changed = $true
        }

        if ($changed) {
            $manifest.PreserveWhitespace = $false
            $manifest.Save($manifestPath)
        }
    }
}