FilterBox/SRxFilterBox.psm1
|
$script:SRxPnpNs = 'http://schemas.dev.office.com/PnP/2022/09/ProvisioningSchema' $script:SRxChunkSize = 9000 Import-Module (Join-Path $PSScriptRoot 'SRxCustomizationJson.psm1') -DisableNameChecking -Force -ErrorAction SilentlyContinue function Get-SRxProfileFolderPath { [CmdletBinding()] param() $sgroupName = Remove-SRxStringSpecialCharacter -String $global:SRxEnv.Tenancy.TermGroupName return (Join-Path (Join-Path $global:SRxEnv.CachePath 'Profiles') $sgroupName) } function Get-SRxEffectiveFilter { [CmdletBinding()] param( [Parameter(Mandatory=$false)] [hashtable]$LayerFilters = @{}, [Parameter(Mandatory=$false)] [int]$ActiveLayer = -1, [Parameter(Mandatory=$false)] $Draft = $null ) if ($null -eq $LayerFilters) { $LayerFilters = @{} } $effLayers = @{} foreach ($i in 0..3) { $lf = $null if ($LayerFilters.ContainsKey($i) -and $LayerFilters[$i]) { $lf = $LayerFilters[$i] } if ($i -eq $ActiveLayer -and $null -ne $Draft) { $lf = [pscustomobject]@{ Attributes = (Merge-SRxHash (Get-SRxLayerAttrs $lf) (Get-SRxDraftAttrs $Draft)) Exclude = [bool](Get-SRxProp $Draft 'Exclude' (Get-SRxProp $lf 'Exclude' $false)) Include = [bool](Get-SRxProp $Draft 'Include' (Get-SRxProp $lf 'Include' $false)) } } if ($null -ne $lf) { $effLayers[$i] = $lf } } $include = $false; $includeLayer = 99 foreach ($i in 0..3) { if ($effLayers.ContainsKey($i) -and (Get-SRxProp $effLayers[$i] 'Include' $false)) { $include = $true; $includeLayer = $i; break } } $exclude = $false if (-not $include) { foreach ($i in 0..3) { if ($effLayers.ContainsKey($i) -and (Get-SRxProp $effLayers[$i] 'Exclude' $false)) { $exclude = $true; break } } } else { foreach ($i in 0..3) { if ($i -ge $includeLayer) { break } if ($effLayers.ContainsKey($i) -and (Get-SRxProp $effLayers[$i] 'Exclude' $false)) { $exclude = $true; break } } } $attrs = @{} foreach ($i in 0..3) { if (-not $effLayers.ContainsKey($i)) { continue } $la = Get-SRxLayerAttrs $effLayers[$i] foreach ($k in $la.Keys) { if (-not $attrs.ContainsKey($k)) { $attrs[$k] = [string]$la[$k] } } } return [pscustomobject]@{ Attributes = $attrs Exclude = [bool]$exclude Include = [bool]$include HasFilter = ([bool]$exclude -or [bool]$include -or ($attrs.Count -gt 0)) } } function Get-SRxProp { param($obj,[string]$name,$default) if ($null -eq $obj) { return $default } if ($obj -is [hashtable]) { if ($obj.ContainsKey($name)) { return $obj[$name] } else { return $default } } $p = $obj.PSObject.Properties[$name]; if ($p) { return $p.Value } else { return $default } } function Get-SRxLayerAttrs { param($lf) $a = Get-SRxProp $lf 'Attributes' @{} $h = @{}; if ($a -is [hashtable]) { foreach ($k in $a.Keys) { $h[$k]=[string]$a[$k] } } elseif ($null -ne $a) { foreach ($p in $a.PSObject.Properties) { $h[$p.Name]=[string]$p.Value } } return $h } function Get-SRxDraftAttrs { param($draft) return (Get-SRxLayerAttrs $draft) } function Merge-SRxHash { param([hashtable]$base,[hashtable]$over) $h = @{}; if ($base) { foreach ($k in $base.Keys) { $h[$k]=$base[$k] } } if ($over) { foreach ($k in $over.Keys) { $h[$k]=$over[$k] } } return $h } function Start-SRxFilterBranchOperation { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$Pack, [Parameter(Mandatory)] [string]$SourcePath, [Parameter(Mandatory=$false)] [switch]$Force, [Parameter(Mandatory=$false)] [switch]$HideWindow ) try { $worker = $("$PSScriptRoot\Write-SRxFilterBranch.debug.ps1") if (-not (Test-Path $worker)) { throw "Worker not found: $worker" } $tag = ("{0:yyyyMMddHHmmssfff}-{1}" -f (Get-Date), ([guid]::NewGuid().ToString('N').Substring(0,8))) $resultPath = Join-Path $env:TEMP ("srx-filter-$tag.json") $packPath = Join-Path $env:TEMP ("srx-filter-$tag.pack.json") $packJson = ($Pack | ConvertTo-Json -Depth 12 -Compress) if ([string]::IsNullOrWhiteSpace($packJson)) { $packJson = '{}' } Set-Content -LiteralPath $packPath -Value $packJson -Encoding UTF8 $shim = @" & '$worker' -RootPath '$SourcePath' -PackPath '$packPath' -ResultPath '$resultPath'$(if($Force){' -Force'}) "@ $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($shim)) $startArgs = @{ FilePath = 'powershell.exe' ArgumentList = @('-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-EncodedCommand', $encoded) PassThru = $true } if ($HideWindow) { $startArgs['WindowStyle'] = 'Hidden' } $proc = Start-Process @startArgs return [pscustomobject]@{ ProcessId = $proc.Id Process = $proc ResultPath = $resultPath PackPath = $packPath Pack = $Pack StartedUtc = (Get-Date).ToUniversalTime() Op = 'filter-branch' } } catch { return [pscustomobject]@{ ProcessId=0; Process=$null; ResultPath=$null; PackPath=$null; Pack=$Pack; StartError=$_.Exception.Message } } } function Get-SRxFilterBranchOperationState { [CmdletBinding()] param([Parameter(Mandatory)]$Operation) $state = [pscustomobject]@{ State='Running'; Success=$false; Conflict=$false; Data=$null; Error=$null; Pid=$Operation.ProcessId } if ($Operation.StartError) { $state.State='Failed'; $state.Error=$Operation.StartError; return $state } if ($Operation.ResultPath -and (Test-Path $Operation.ResultPath)) { try { $raw = Get-Content -LiteralPath $Operation.ResultPath -Raw -ErrorAction Stop if (-not [string]::IsNullOrWhiteSpace($raw)) { $obj = $raw | ConvertFrom-Json -ErrorAction Stop $state.Success = [bool]$obj.Success $state.Conflict = [bool]$obj.Conflict $state.Data = $obj.Data $state.Error = $obj.Error if ($obj.Success) { $state.State = 'Completed' } elseif ($obj.Conflict) { $state.State = 'Conflict' } else { $state.State = 'Failed' } return $state } } catch { } } $alive = $false try { $p = Get-Process -Id $Operation.ProcessId -ErrorAction SilentlyContinue; $alive = ($null -ne $p -and -not $p.HasExited) } catch { $alive = $false } if ($alive) { $state.State='Running'; return $state } $state.State='Failed'; $state.Error='Worker exited without a result (killed or crashed).'; return $state } function Stop-SRxFilterBranchOperation { [CmdletBinding()] param([Parameter(Mandatory)]$Operation) try { if ($Operation.ProcessId -gt 0) { $p = Get-Process -Id $Operation.ProcessId -ErrorAction SilentlyContinue if ($p -and -not $p.HasExited) { Stop-Process -Id $Operation.ProcessId -Force -ErrorAction Stop; return $true } } } catch { } return $false } function Remove-SRxFilterBranchTempFiles { [CmdletBinding()] param([Parameter(Mandatory)]$Operation) foreach ($p in @($Operation.ResultPath, $Operation.PackPath)) { try { if ($p -and (Test-Path $p)) { Remove-Item $p -Force -ErrorAction SilentlyContinue } } catch { } } } function Sync-SRxLocalFilterBranch { [CmdletBinding()] param([Parameter(Mandatory)] $Pack) $result = [pscustomobject]@{ Success=$false; Error=$null; OuterXml=$null; FilterNodes=@() } try { $scope = [int](Get-SRxProp $Pack 'Scope' 0) $operation = [string](Get-SRxProp $Pack 'Operation' 'add') $branch = Get-SRxProp $Pack 'Branch' @() $profileFolder = Get-SRxProfileFolderPath $schemaPath = Join-Path $profileFolder 'termSet.xml' if (-not (Test-Path $schemaPath)) { throw "termSet.xml not found at $schemaPath" } [xml]$xml = Get-Content -LiteralPath $schemaPath -Raw -Encoding UTF8 $ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) $ns.AddNamespace('pnp', $script:SRxPnpNs) $host1 = Find-SRxCustomizationsHost -Xml $xml -Ns $ns -Pack $Pack if ($null -eq $host1) { throw "Customizations host not found for scope $scope (Pack Environment/Design/TermID)." } $leafNode = Apply-SRxBranch -Xml $xml -Ns $ns -HostNode $host1 -Branch $branch -Operation $operation $xml.Save($schemaPath) $jsonPath = Join-Path $profileFolder 'ProvisioningDatabase.json' $minPath = Join-Path $profileFolder 'ProvisioningDatabase.min.json' [xml[]] (Get-Content -Raw -Encoding UTF8 -Path $schemaPath) ` | Convert-SRxFromXML | ConvertTo-Json -Depth 50 | Format-SRxJson -Indentation 1 ` | Set-Content -LiteralPath $jsonPath -Encoding UTF8 [xml[]] (Get-Content -Raw -Encoding UTF8 -Path $schemaPath) ` | Convert-SRxFromXML | ConvertTo-Json -Depth 50 | Format-SRxJson -Minify ` | Set-Content -LiteralPath $minPath -Encoding UTF8 $result.Success = $true $result.OuterXml = if ($leafNode) { $leafNode.OuterXml } else { $null } $result.FilterNodes = @( [pscustomobject]@{ xPath = [string](Get-SRxProp ($branch | Select-Object -Last 1) 'XPath' '') Scope = $scope optype = [int](Get-SRxProp $Pack 'Optype' -1) Filter = $operation } ) return $result } catch { $result.Error = $_.Exception.Message; return $result } } function Find-SRxCustomizationsHost { param([xml]$Xml, $Ns, $Pack) $termId = [string](Get-SRxProp $Pack 'TermID' '') if ($termId) { $hostTerm = $Xml.SelectSingleNode("//pnp:Term[@ID='$termId']", $Ns) if ($hostTerm) { $cust = $hostTerm.SelectSingleNode("pnp:Terms/pnp:Term[@Name='Customizations']", $Ns) if (-not $cust) { $termsEl = $hostTerm.SelectSingleNode('pnp:Terms', $Ns) if (-not $termsEl) { $termsEl = $Xml.CreateElement('pnp','Terms',$script:SRxPnpNs); [void]$hostTerm.AppendChild($termsEl) } $cust = $Xml.CreateElement('pnp','Term',$script:SRxPnpNs); $cust.SetAttribute('Name','Customizations'); [void]$termsEl.AppendChild($cust) } return $cust } } return $null } function Apply-SRxBranch { param([xml]$Xml, $Ns, $HostNode, $Branch, [string]$Operation) $current = $HostNode $leaf = $null $count = @($Branch).Count $i = 0 foreach ($d in @($Branch)) { $i++ $name = [string](Get-SRxProp $d 'Name' '') if ([string]::IsNullOrEmpty($name)) { continue } $termsEl = $current.SelectSingleNode('pnp:Terms', $Ns) if (-not $termsEl) { $termsEl = $Xml.CreateElement('pnp','Terms',$script:SRxPnpNs); [void]$current.AppendChild($termsEl) } $child = $termsEl.SelectSingleNode("pnp:Term[@Name=`"$name`"]", $Ns) $isLeaf = ($i -eq $count) if ($Operation -eq 'remove' -and $isLeaf) { if ($child) { [void]$termsEl.RemoveChild($child) } $leaf = $null break } if (-not $child) { $child = $Xml.CreateElement('pnp','Term',$script:SRxPnpNs) $child.SetAttribute('Name',$name) [void]$termsEl.AppendChild($child) } if ($isLeaf) { $cp = $child.SelectSingleNode('pnp:CustomProperties', $Ns) if ($cp) { [void]$child.RemoveChild($cp) } $attrs = Get-SRxProp $d 'Attributes' @{} $attrHash = @{}; if ($attrs -is [hashtable]) { foreach ($k in $attrs.Keys) { $attrHash[$k]=[string]$attrs[$k] } } elseif ($null -ne $attrs) { foreach ($p in $attrs.PSObject.Properties) { $attrHash[$p.Name]=[string]$p.Value } } if ((Get-SRxProp $d 'Deprecated' $false)) { $attrHash['ts_Deprecated'] = 'true' } if ((Get-SRxProp $d 'CustomizationTarget' $false)) { $attrHash['ts_CustomizationTarget'] = 'true' } if ($attrHash.Count -gt 0) { $cpEl = $Xml.CreateElement('pnp','CustomProperties',$script:SRxPnpNs) foreach ($k in ($attrHash.Keys | Sort-Object)) { $v = [string]$attrHash[$k] if ($v.Length -le $script:SRxChunkSize) { $pe = $Xml.CreateElement('pnp','Property',$script:SRxPnpNs); $pe.SetAttribute('Key',$k); $pe.SetAttribute('Value',$v); [void]$cpEl.AppendChild($pe) } else { $parts = @($v -split ("(.{$($script:SRxChunkSize)})") | Where-Object { $_ }) for ([int]$j=0; $j -lt $parts.Count; $j++) { $pe = $Xml.CreateElement('pnp','Property',$script:SRxPnpNs); $pe.SetAttribute('Key',"$($k)_$($j)"); $pe.SetAttribute('Value',$parts[$j]); [void]$cpEl.AppendChild($pe) } } } [void]$child.AppendChild($cpEl) } $leaf = $child } $current = $child } return $leaf } function Sync-SRxLocalJsonCustomization { [CmdletBinding()] param( [Parameter(Mandatory)] $Pack, [Parameter(Mandatory)] [string]$Operation ) $result = [pscustomobject]@{ Success=$false; Error=$null; OuterXml=$null; FilterNodes=@() } try { $scope = [int](Get-SRxProp $Pack 'Scope' 0) $profileFolder = Get-SRxProfileFolderPath $schemaPath = Join-Path $profileFolder 'termSet.xml' if (-not (Test-Path $schemaPath)) { throw "termSet.xml not found at $schemaPath" } [xml]$xml = Get-Content -LiteralPath $schemaPath -Raw -Encoding UTF8 $ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) $ns.AddNamespace('pnp', $script:SRxPnpNs) $host1 = Find-SRxCustomizationsHost -Xml $xml -Ns $ns -Pack $Pack if ($null -eq $host1) { throw "Customizations host not found for scope $scope." } $container = New-SRxJsonContainerTerm -Pack $Pack -Operation $Operation $name = [string]$container.Name $termsEl = $host1.SelectSingleNode('pnp:Terms', $ns) if (-not $termsEl) { $termsEl = $xml.CreateElement('pnp','Terms',$script:SRxPnpNs); [void]$host1.AppendChild($termsEl) } $existing = $termsEl.SelectSingleNode("pnp:Term[@Name=`"$name`"]", $ns) if ($Operation -eq 'remove') { if ($existing) { [void]$termsEl.RemoveChild($existing) } $leaf = $null } else { if ($existing) { [void]$termsEl.RemoveChild($existing) } $term = $xml.CreateElement('pnp','Term',$script:SRxPnpNs) $term.SetAttribute('Name', $name) $cp = $xml.CreateElement('pnp','CustomProperties',$script:SRxPnpNs) $props = [hashtable]$container.Attributes $flat = @{} foreach ($k in $props.Keys) { $v = [string]$props[$k] if ($k -eq 'ts_CustomizationJson') { $chunked = Split-SRxJsonProperty -BaseKey 'ts_CustomizationJson' -Value $v foreach ($ck in $chunked.Keys) { $flat[$ck] = [string]$chunked[$ck] } } else { $flat[$k] = $v } } foreach ($k in ($flat.Keys | Sort-Object)) { $pe = $xml.CreateElement('pnp','Property',$script:SRxPnpNs) $pe.SetAttribute('Key', [string]$k) $pe.SetAttribute('Value', [string]$flat[$k]) [void]$cp.AppendChild($pe) } [void]$term.AppendChild($cp) [void]$termsEl.AppendChild($term) $leaf = $term } $xml.Save($schemaPath) $jsonPath = Join-Path $profileFolder 'ProvisioningDatabase.json' $minPath = Join-Path $profileFolder 'ProvisioningDatabase.min.json' [xml[]] (Get-Content -Raw -Encoding UTF8 -Path $schemaPath) ` | Convert-SRxFromXML | ConvertTo-Json -Depth 50 | Format-SRxJson -Indentation 1 ` | Set-Content -LiteralPath $jsonPath -Encoding UTF8 [xml[]] (Get-Content -Raw -Encoding UTF8 -Path $schemaPath) ` | Convert-SRxFromXML | ConvertTo-Json -Depth 50 | Format-SRxJson -Minify ` | Set-Content -LiteralPath $minPath -Encoding UTF8 $result.Success = $true $result.OuterXml = if ($leaf) { $leaf.OuterXml } else { $null } $result.FilterNodes = @( [pscustomobject]@{ xPath = [string]$container.xPath Scope = $scope optype = [int]$container.optype Filter = $Operation } ) return $result } catch { $result.Error = $_.Exception.Message; return $result } } Export-ModuleMember -Function ` Get-SRxProfileFolderPath, ` Get-SRxEffectiveFilter, ` Start-SRxFilterBranchOperation, ` Get-SRxFilterBranchOperationState, ` Stop-SRxFilterBranchOperation, ` Remove-SRxFilterBranchTempFiles, ` Sync-SRxLocalJsonCustomization, ` Sync-SRxLocalFilterBranch |