IntuneManagement 4.0.0-beta1

This commit is contained in:
Mikael Karlsson
2026-09-23 19:13:09 +10:00
commit 7869619510
892 changed files with 577109 additions and 0 deletions
@@ -0,0 +1,19 @@
# Avalonia UI extension for the Atlassian documentation output provider.
#
# Attaches file-browse metadata onto the registered [DocumentationRegistry]
# entry so the bulk-doc form's browse-button-wiring loop can render a Save
# picker for the Atlassian output file. The provider file itself
# (Internal/Documentation/OutputProviders/DocumentationOutputAtlassian.ps1)
# stays free of XAML control names and toolkit-specific dialog options —
# only the active UI backend's ClassExtensions load per session, so no
# WPF/Avalonia conflict is possible.
#
# Unlike HTML there is no CSS row: Confluence storage format carries no
# stylesheet, so the panel has a single browse control.
$provider = [DocumentationRegistry]::FindOutput('atlassian')
if ($provider) {
Add-Member -InputObject $provider -MemberType NoteProperty -Force -Name FileBrowseControls -Value @(
@{ Button='browseAtlassianDocumentName'; TextBox='txtAtlassianDocumentName'; Save=$true; Title='Save Atlassian documentation'; Extension='html'; FilterName='Confluence storage format'; FilterPattern='*.html' }
)
}
@@ -0,0 +1,17 @@
# Avalonia UI extension for the HTML documentation output provider.
#
# Attaches file-browse metadata onto the registered [DocumentationRegistry]
# entry so the bulk-doc form's browse-button-wiring loop can render Save
# and Open pickers for the HTML output file and its CSS reference. The
# provider file itself (Internal/Documentation/OutputProviders/DocumentationOutputHTML.ps1)
# stays free of XAML control names and toolkit-specific dialog options —
# only the active UI backend's ClassExtensions load per session, so no
# WPF/Avalonia conflict is possible.
$provider = [DocumentationRegistry]::FindOutput('html')
if ($provider) {
Add-Member -InputObject $provider -MemberType NoteProperty -Force -Name FileBrowseControls -Value @(
@{ Button='browseHTMLDocumentName'; TextBox='txtHTMLDocumentName'; Save=$true; Title='Save HTML documentation'; Extension='html'; FilterName='HTML files'; FilterPattern='*.html' }
@{ Button='browseHTMLCSSFile'; TextBox='txtHTMLCSSFile'; Save=$false; Title='Select HTML CSS file'; FilterName='CSS files'; FilterPattern='*.css' }
)
}
@@ -0,0 +1,10 @@
# Avalonia UI extension for the Markdown documentation output provider.
# See DocumentationOutputHTMLUIExtension.ps1 for the rationale + pattern.
$provider = [DocumentationRegistry]::FindOutput('md')
if ($provider) {
Add-Member -InputObject $provider -MemberType NoteProperty -Force -Name FileBrowseControls -Value @(
@{ Button='browseMDDocumentName'; TextBox='txtMDDocumentName'; Save=$true; Title='Save Markdown documentation'; Extension='md'; FilterName='Markdown files'; FilterPattern='*.md' }
@{ Button='browseMDCSSFile'; TextBox='txtMDCSSFile'; Save=$false; Title='Select Markdown CSS file'; FilterName='CSS files'; FilterPattern='*.css' }
)
}
@@ -0,0 +1,10 @@
# Avalonia UI extension for the Word documentation output provider.
# See DocumentationOutputHTMLUIExtension.ps1 for the rationale + pattern.
$provider = [DocumentationRegistry]::FindOutput('word')
if ($provider) {
Add-Member -InputObject $provider -MemberType NoteProperty -Force -Name FileBrowseControls -Value @(
@{ Button='browseWordDocumentName'; TextBox='txtWordDocumentName'; Save=$true; Title='Save Word documentation'; Extension='docx'; FilterName='Word files'; FilterPattern='*.docx' }
@{ Button='browseWordDocumentTemplate'; TextBox='txtWordDocumentTemplate'; Save=$false; Title='Select Word template'; FilterName='Word templates'; FilterPattern='*.dotx;*.dotm' }
)
}
@@ -0,0 +1,211 @@
# Avalonia parallel of UI/WPF/ClassExtensions/IntuneApplicationClassUIExtensions.ps1.
# Attaches AddUIExportExtensions / AddUIDetailsExtension to ApplicationType
# only. Same scope-loss + R13 constraints documented in
# IntuneScriptClassUIExtension.ps1 — helpers captured up front, OpenFileDialog
# / SaveFileDialog routed through Host::OpenFilePicker / Host::SaveFilePicker.
#
# Two non-trivial differences from the script flow:
#
# 1. Export adds TWO checkboxes — "Export script" (shared with the script
# path; chains through Add-AvaloniaExportPropertyCheckbox under the
# `chkExportScript` name) and an Application-specific
# "Export application file" (`chkExportAppContent`). The latter's
# initial state comes from a persisted setting, not a hard-coded `$true`.
#
# 2. Download is a multi-step pipeline: SaveFilePicker for the encrypted
# content, Find-AppEncryptionFile to locate the matching .json,
# OpenFilePicker fallback when the json is missing, Start-DecryptFile to
# produce the unencrypted .zip, then delete the .encrypted intermediate.
##############################################
# Helpers
##############################################
function Invoke-UploadAppFileAvalonia
{
param($Row)
if (-not $Row -or -not $Row.Source) { return }
$appItem = $Row.Source
$obj = $appItem.Object
if ($obj.publishingState -ne "notPublished") {
$confirm = $script:UIProvider.ShowMessageBox("Are you sure you want to upload a new file for the app?`n`nApplication:`n$($appItem.Name)", "Update app file?", "YesNo", "Warning")
if ($confirm -ne "Yes") { return }
}
$pkgPath = Get-SettingValue "IntuneAppPackagesFolder"
$picked = (Get-AvaloniaHost)::OpenFilePicker(
$script:Window,
'Select Intune Win32 file',
$pkgPath,
'Intune Win32',
'*.intunewin')
if (-not $picked) { return }
Write-Status "Import $($obj.displayName) file"
Start-ApplicationImportFile $appItem $picked
Write-Status ""
}
function Invoke-DownloadAppFileAvalonia
{
param($Row)
if (-not $Row -or -not $Row.Source) { return }
$appItem = $Row.Source
$obj = $appItem.Object
# Prefer the package fileName; fall back to the app display name so the save
# dialog never defaults to a bare ".encrypted" (win32 list rows may not carry
# a fileName).
$baseName = if ($obj.FileName) { [string]$obj.FileName } elseif ($obj.displayName) { [string]$obj.displayName } else { 'app' }
Write-Status "Download file"
try {
$pkgPath = Get-SettingValue "IntuneAppDownloadFolder" (Get-SettingValue "IntuneAppPackagesFolder")
$picked = (Get-AvaloniaHost)::SaveFilePicker(
$script:Window,
'Save encrypted intunewin',
($baseName + ".encrypted"),
'encrypted',
'Encrypted intunewin',
'*.encrypted')
if (-not $picked) { return }
$contentFileObj = Start-DownloadAppContent $appItem $picked
if (-not [IO.File]::Exists($picked)) { return }
$fullPath = Find-AppEncryptionFile $obj $contentFileObj $pkgPath
if (-not [IO.File]::Exists($fullPath)) {
$browse = $script:UIProvider.ShowMessageBox("Could not find decryption file for $($obj.displayName)`nApp Id: $($obj.id)`nContent version $($obj.committedContentVersion)`n`nDo you want to browse for the file?", "Encryption file not found", "YesNo", "Warning")
if ($browse -eq "Yes") {
$alt = (Get-AvaloniaHost)::OpenFilePicker(
$script:Window,
'Select decryption json',
$pkgPath,
'JSON files',
'*.json')
if ($alt) { $fullPath = $alt }
}
}
if ([IO.File]::Exists($fullPath)) {
Write-Status "Decrypting file"
$encryptionInfo = ConvertFrom-Json ([IO.File]::ReadAllText($fullPath))
if ($encryptionInfo.fileEncryptionInfo) { $encryptionInfo = $encryptionInfo.fileEncryptionInfo }
$zipName = ($baseName -replace 'intunewin$', 'zip')
if ($zipName -eq $baseName) { $zipName = "$baseName.zip" }
$destination = [IO.Path]::Combine($pkgPath, $zipName)
Start-DecryptFile $picked $destination $encryptionInfo.encryptionKey $encryptionInfo.initializationVector
try { [IO.File]::Delete($picked) }
catch { Write-LogError "Failed to delete exported encrypted file" $_.Exception }
} else {
Write-Log "Decryption file for $($obj.displayName) not found. Skipping decryption" 2
}
}
finally {
# Always clear the status overlay - an error mid-download previously left
# it stuck on screen and looked like a hang.
Write-Status ""
}
}
##############################################
# Captures
##############################################
$sbAddCheckbox = ${function:Add-AvaloniaExportPropertyCheckbox}
$sbAddDetailsBtn = ${function:Add-AvaloniaDetailsButton}
$sbSetCache = ${function:Set-CacheObject}
$sbWriteLogError = ${function:Write-LogError}
$sbGetSetting = ${function:Get-SettingStoreValue}
$sbInvokeUploadApp = ${function:Invoke-UploadAppFileAvalonia}
$sbInvokeDownloadApp = ${function:Invoke-DownloadAppFileAvalonia}
##############################################
# ScriptBlocks
##############################################
$SBAddApplicationUIExportExtension = {
param($Form)
if (-not $Form) { return }
# "Export script" checkbox (shared with script-typed exports).
$chkScript = & $sbAddCheckbox -Form $Form `
-CheckboxName 'chkExportScript' `
-LabelText 'Export script' `
-ToolTip 'Export the script associated with selected profiles' `
-InitialChecked $true
if ($chkScript) {
& $sbSetCache "ExportScripts" $chkScript.IsChecked
$chkScript.add_IsCheckedChanged({
param($s, $e)
& $sbSetCache "ExportScripts" $s.IsChecked
}.GetNewClosure())
}
# "Export application file" checkbox — initial state from setting.
$initial = ((& $sbGetSetting "Intune" "ExportAppFile" "false") -eq "true")
$chkApp = & $sbAddCheckbox -Form $Form `
-CheckboxName 'chkExportAppContent' `
-LabelText 'Export application file' `
-ToolTip 'Export the application file. Note: Application file will only be exported if encryption file is found.' `
-InitialChecked $initial
if ($chkApp) {
& $sbSetCache "ExportAppContent" $chkApp.IsChecked
$chkApp.add_IsCheckedChanged({
param($s, $e)
& $sbSetCache "ExportAppContent" $s.IsChecked
}.GetNewClosure())
}
}.GetNewClosure()
$SBAddApplicationUIDetailsExtension = {
param($Form)
if (-not $Form) { return }
$onUpload = {
param($s, $e)
$row = $script:dgIntuneManagerObjects.SelectedItem
if (-not $row) { return }
try { & $sbInvokeUploadApp $row }
catch { & $sbWriteLogError "Upload application file failed" $_.Exception }
}.GetNewClosure()
$onDownload = {
param($s, $e)
$row = $script:dgIntuneManagerObjects.SelectedItem
if (-not $row) { return }
try { & $sbInvokeDownloadApp $row }
catch { & $sbWriteLogError "Download application file failed" $_.Exception }
}.GetNewClosure()
# Insert order matches the WPF original: Upload at 0 first, then Download
# at 0 — net layout left-to-right is Download, Upload, [existing buttons].
& $sbAddDetailsBtn -Form $Form -Name 'btnUploadAppfile' -Content 'Upload' `
-InsertIndex 0 -OnClick $onUpload | Out-Null
& $sbAddDetailsBtn -Form $Form -Name 'btnDownloadAppfile' -Content 'Download' `
-InsertIndex 0 -OnClick $onDownload | Out-Null
}.GetNewClosure()
##############################################
# Init
##############################################
function Invoke-InitializeApplicationUIExtensions
{
$typeClasses = @()
try { $typeClasses += Get-SingletonObject "ApplicationType" } catch {}
foreach ($typeClass in $typeClasses) {
if (-not $typeClass) { continue }
Add-ObjectMethod $typeClass "AddUIExportExtensions" $SBAddApplicationUIExportExtension
Add-ObjectMethod $typeClass "AddUIDetailsExtension" $SBAddApplicationUIDetailsExtension
}
}
Invoke-InitializeApplicationUIExtensions
@@ -0,0 +1,68 @@
# Avalonia counterpart of UI/WPF/ClassExtensions/IntuneCommonUIExtensions.ps1.
# Attaches LoadIconImage / GetImage to every IntunePolicyType + IntunePolicyGroup
# singleton so left-nav menu items (and downstream consumers via $item.IconImage)
# get a real Avalonia visual instead of $null.
#
# Two divergences from the WPF original:
# 1. File extension is .axaml (the converter under XAML/Icons/_Convert-Icons.ps1
# writes Avalonia-flavoured copies of the WPF .xaml shape sources).
# 2. Get-XamlObject here is the Avalonia loader (Extensions/CoreUI.ps1), so
# the Image returned is an Avalonia Viewbox/Canvas tree that the
# lstMenuItems DataTemplate's <ContentControl Content="{Binding IconImage}"/>
# can host directly — no WPF→Avalonia conversion at runtime.
$SBAddCommonUILoadImage = [scriptblock] {
try {
$this.IconImage = $this.GetImage()
}
catch {}
}
# Capture the module-bound Get-XamlObject + UI root path at attachment time.
# Per [[scriptmethod-loses-module-scope]], Add-Member ScriptMethod scriptblocks
# can't reliably resolve module-scoped vars/functions by name. Routing through
# $script:UIProvider here also fails because the methods are attached at module
# load, *before* the loader instantiates the provider.
#
# Only the methods are attached here. The icons themselves are parsed later, by
# Initialize-MenuItemIcons (Extensions/UIMenuIcons*.ps1) when a menu is built -
# after the splash is up, not on the Import-Module critical path.
$_getXamlObjectFn = ${function:Get-XamlObject}
$_uiRootFolder = $script:AppUIRootFolder
$SBAddCommonUIGetImage = [scriptblock] {
if (-not $this.Icon) { return $null }
$iconPath = Join-Path $_uiRootFolder ("XAML/Icons/" + $this.Icon + ".axaml")
if (-not [IO.File]::Exists($iconPath)) { return $null }
return (& $_getXamlObjectFn $iconPath)
}.GetNewClosure()
function Invoke-InitializeCommonUIExtensions {
$typeClasses = @()
$groupClasses = @()
Get-SubClasses "IntunePolicyTypeBase" | ForEach-Object {
if (Test-ClassIsAbstract $_) { return }
$policyTypeName = $_.Name
try {
$tmpClass = Get-SingletonObject $_.Name
if ($null -ne $tmpClass.PolicyGroup) {
$typeClasses += $tmpClass
}
}
catch {
Write-LogError "Failed to initialize class $policyTypeName." $_.Exception
}
}
Get-SubClasses "IntunePolicyGroupBase" | ForEach-Object {
$groupClasses += Get-SingletonObject $_.Name
}
foreach ($class in ($typeClasses + $groupClasses)) {
Add-ObjectMethod $class "LoadIconImage" $SBAddCommonUILoadImage
Add-ObjectMethod $class "GetImage" $SBAddCommonUIGetImage
}
}
Invoke-InitializeCommonUIExtensions
@@ -0,0 +1,182 @@
# Avalonia parallel of UI/WPF/ClassExtensions/IntunePolicyFileClassUIExtension.ps1.
# Same shape as IntuneScriptClassUIExtension.ps1; differences:
#
# * The "JSON property to edit" is dynamic — `_PolicyFileAttributes[0]` on
# the type class points at the property holding base64 file bytes (e.g.
# "fileContents" for custom attribute scripts). The TODO note about
# "Support multiple attributes" carries over from the WPF original.
# * Download writes raw bytes (no UTF-8 BOM stripping) to preserve binary
# payloads. Filename defaults to JsonObject.FileName, falling back to
# "<attribute>.json".
#
# Same scope-loss + R13 constraints as IntuneScriptClassUIExtension.ps1 —
# helpers captured up front and OnClick / OnSave SBs use .GetNewClosure().
##############################################
# Helpers
##############################################
function Invoke-DownloadPolicyFileAvalonia
{
param($Policy)
if (-not $Policy) { return }
if ($Policy.IsFullObject -eq $false) { [void]$Policy.Get() }
$attribute = $Policy.PolicyType._PolicyFileAttributes[0]
if (-not $Policy.JsonObject.$attribute) { return }
$fileName = ?? $Policy.JsonObject.FileName "$($attribute).json"
Write-Log "Download PowerShell file '$fileName' from $($Policy.Name)"
$picked = (Get-AvaloniaHost)::SaveFilePicker(
$script:Window,
'Save file',
[string]$fileName,
$null,
'All files',
'*.*')
if (-not $picked) { return }
[IO.File]::WriteAllBytes($picked, ([System.Convert]::FromBase64String($Policy.JsonObject.$attribute)))
}
function Invoke-EditPolicyFileAvalonia
{
param($Policy)
if (-not $Policy) { return }
if ($Policy.IsFullObject -eq $false) { [void]$Policy.Get() }
$attribute = $Policy.PolicyType._PolicyFileAttributes[0] # TODO: Support multiple attributes
if (-not $Policy.JsonObject.$attribute) { return }
$policyFileText = [System.Text.Encoding]::UTF8.GetString(
[System.Convert]::FromBase64String($Policy.JsonObject.$attribute))
Show-AvaloniaScriptEditor `
-Title "Edit: $($Policy.JsonObject.displayName)" `
-InitialText $policyFileText `
-OnSave {
param([string]$NewText)
$pre = [System.Text.Encoding]::UTF8.GetPreamble()
$utfBom = [System.Text.Encoding]::UTF8.GetString($pre)
if ($NewText.StartsWith($utfBom)) { $NewText = $NewText.Remove(0, $utfBom.Length) }
$bytes = [System.Text.Encoding]::UTF8.GetBytes($NewText)
$encodedText = [Convert]::ToBase64String($bytes)
$attr = $Policy.PolicyType._PolicyFileAttributes[0]
if ($Policy.JsonObject.$attr -eq $encodedText) { return }
$confirm = $script:UIProvider.ShowMessageBox("Are you sure you want to update the file?`n`nObject:`n$($Policy.Name)", "Update file?", "YesNo", "Warning")
if ($confirm -ne "Yes") { return }
Write-Status "Update $($Policy.Name)"
$cloned = $Policy.Clone()
$cloned.JsonObject.$attr = $encodedText
Remove-GraphPropertiesForImport $cloned $cloned.JsonObject
foreach ($prop in $Policy.PolicyType.PropertiesToRemoveForUpdate) {
Remove-Property $cloned.JsonObject $prop
}
Remove-Property $cloned.JsonObject "Assignments"
Remove-Property $cloned.JsonObject "isAssigned"
if ($Policy.Set($null, $cloned.JsonString, "PATCH", $null)) {
Write-Log "File saved successfully"
} else {
Write-Log "Failed to save script" 3
}
Write-Status ""
}.GetNewClosure()
}
##############################################
# Captures
##############################################
$sbAddCheckbox = ${function:Add-AvaloniaExportPropertyCheckbox}
$sbAddDetailsBtn = ${function:Add-AvaloniaDetailsButton}
$sbSetCache = ${function:Set-CacheObject}
$sbWriteLogError = ${function:Write-LogError}
$sbInvokeDownloadPolicyFile = ${function:Invoke-DownloadPolicyFileAvalonia}
$sbInvokeEditPolicyFile = ${function:Invoke-EditPolicyFileAvalonia}
##############################################
# ScriptBlocks
##############################################
$SBAddPolicyFileUIExportExtension = {
param($Form)
if (-not $Form) { return }
$chk = & $sbAddCheckbox -Form $Form `
-CheckboxName 'chkExportPolicyFiles' `
-LabelText 'Export file' `
-ToolTip 'Export the files associated with selected profiles' `
-InitialChecked $true
if (-not $chk) { return }
& $sbSetCache "ExportPolicyFiles" $chk.IsChecked
$chk.add_IsCheckedChanged({
param($s, $e)
& $sbSetCache "ExportPolicyFiles" $s.IsChecked
}.GetNewClosure())
}.GetNewClosure()
$SBAddPolicyFileUIDetailsExtension = {
param($Form)
if (-not $Form) { return }
$onDownload = {
param($s, $e)
$row = $script:dgIntuneManagerObjects.SelectedItem
$sel = if ($row) { $row.Source } else { $null }
if (-not $sel) { return }
try { & $sbInvokeDownloadPolicyFile $sel }
catch { & $sbWriteLogError "Download policy file failed" $_.Exception }
}.GetNewClosure()
$onEdit = {
param($s, $e)
$row = $script:dgIntuneManagerObjects.SelectedItem
$sel = if ($row) { $row.Source } else { $null }
if (-not $sel) { return }
try { & $sbInvokeEditPolicyFile $sel }
catch { & $sbWriteLogError "Edit policy file failed" $_.Exception }
}.GetNewClosure()
& $sbAddDetailsBtn -Form $Form -Name 'btnDownload' -Content 'Download' `
-InsertIndex 0 -OnClick $onDownload | Out-Null
& $sbAddDetailsBtn -Form $Form -Name 'btnEdit' -Content 'Edit' `
-InsertIndex 1 -OnClick $onEdit | Out-Null
}.GetNewClosure()
##############################################
# Init
##############################################
function Invoke-InitializePolicyFileUIExtensions
{
$typeClasses = @()
Get-SubClasses "IntunePolicyTypeBase" | ForEach-Object {
if (Test-ClassIsAbstract $_) { return }
try {
$tmpClass = Get-SingletonObject $_.Name
if ($null -ne $tmpClass.PolicyGroup -and $tmpClass._PolicyFileAttributes.Count -gt 0) {
$typeClasses += $tmpClass
}
} catch {}
}
foreach ($typeClass in $typeClasses) {
Add-ObjectMethod $typeClass "AddUIExportExtensions" $SBAddPolicyFileUIExportExtension
Add-ObjectMethod $typeClass "AddUIDetailsExtension" $SBAddPolicyFileUIDetailsExtension
}
}
Invoke-InitializePolicyFileUIExtensions
@@ -0,0 +1,184 @@
# Avalonia parallel of UI/WPF/ClassExtensions/IntuneScriptClassUIExtension.ps1.
# Same ScriptMethod names (AddUIExportExtensions / AddUIDetailsExtension) so
# the Add-IntuneManagerExportUIExtensions wrapper from
# Extensions/IntuneManagerExtensionHooks.ps1 invokes them transparently.
#
# Two cross-cutting concerns:
#
# * [[scriptmethod-loses-module-scope]] — Add-Member ScriptMethod scriptblocks
# execute outside the defining module's session state and can't see
# module-scoped functions by name. Every helper used inside the SBs is
# captured up front via `${function:X}` and invoked through `&`. The outer
# SB calls .GetNewClosure() so the captures travel with it; nested OnClick
# handlers do the same so their inner-scope copies survive too.
#
# * The WPF original used [System.Windows.Forms.SaveFileDialog] (R13 forbids
# WinForms in UI/Avalonia) and built [System.Windows.Controls.*] objects
# directly. Both are routed through the Avalonia helpers in
# IntuneManagerExtensionHooks.ps1 (Add-AvaloniaExportPropertyCheckbox,
# Add-AvaloniaDetailsButton, Show-AvaloniaScriptEditor) which use
# [Avalonia.Controls.*] + Host::SaveFilePicker.
##############################################
# Helpers (file-private functions invoked from inside the ScriptMethods).
# Defined first so the `${function:X}` captures below see live refs before
# the SBs build their closures.
##############################################
function Invoke-DownloadScriptAvalonia
{
param($ScriptPolicy)
if (-not $ScriptPolicy) { return }
if ($ScriptPolicy.IsFullObject -eq $false) { [void]$ScriptPolicy.Get() }
if (-not $ScriptPolicy.JsonObject.scriptContent) { return }
Write-Status ""
$picked = (Get-AvaloniaHost)::SaveFilePicker(
$script:Window,
'Save script',
[string]$ScriptPolicy.JsonObject.FileName,
$null,
'All files',
'*.*')
if (-not $picked) { return }
Save-IntuneScriptContent $ScriptPolicy $picked | Out-Null
}
function Invoke-EditScriptAvalonia
{
param($ScriptPolicy)
if (-not $ScriptPolicy) { return }
if ($ScriptPolicy.IsFullObject -eq $false) { [void]$ScriptPolicy.Get() }
if (-not $ScriptPolicy.JsonObject.scriptContent) { return }
$scriptText = [System.Text.Encoding]::UTF8.GetString(
[System.Convert]::FromBase64String($ScriptPolicy.JsonObject.scriptContent))
Show-AvaloniaScriptEditor `
-Title "Edit: $($ScriptPolicy.JsonObject.displayName)" `
-InitialText $scriptText `
-OnSave {
param([string]$NewText)
$pre = [System.Text.Encoding]::UTF8.GetPreamble()
$utfBom = [System.Text.Encoding]::UTF8.GetString($pre)
if ($NewText.StartsWith($utfBom)) { $NewText = $NewText.Remove(0, $utfBom.Length) }
$bytes = [System.Text.Encoding]::UTF8.GetBytes($NewText)
$encodedText = [Convert]::ToBase64String($bytes)
if ($ScriptPolicy.JsonObject.scriptContent -eq $encodedText) { return }
$confirm = $script:UIProvider.ShowMessageBox("Are you sure you want to update the script?`n`nObject:`n$($ScriptPolicy.Name)", "Update script?", "YesNo", "Warning")
if ($confirm -ne "Yes") { return }
Write-Status "Update $($ScriptPolicy.Name)"
$cloned = $ScriptPolicy.Clone()
$cloned.JsonObject.scriptContent = $encodedText
Remove-GraphPropertiesForImport $cloned $cloned.JsonObject
foreach ($prop in $ScriptPolicy.PolicyType.PropertiesToRemoveForUpdate) {
Remove-Property $cloned.JsonObject $prop
}
Remove-Property $cloned.JsonObject "Assignments"
Remove-Property $cloned.JsonObject "isAssigned"
if ($ScriptPolicy.Set($null, $cloned.JsonString, "PATCH", $null)) {
Write-Log "Script saved successfully"
} else {
Write-Log "Failed to save script" 3
}
Write-Status ""
}.GetNewClosure()
}
##############################################
# Captures (see [[scriptmethod-loses-module-scope]])
##############################################
$sbAddCheckbox = ${function:Add-AvaloniaExportPropertyCheckbox}
$sbAddDetailsBtn = ${function:Add-AvaloniaDetailsButton}
$sbSetCache = ${function:Set-CacheObject}
$sbWriteLogError = ${function:Write-LogError}
$sbInvokeDownloadScript = ${function:Invoke-DownloadScriptAvalonia}
$sbInvokeEditScript = ${function:Invoke-EditScriptAvalonia}
##############################################
# ScriptBlocks (built AFTER captures are in scope so GetNewClosure picks them up)
##############################################
$SBAddScriptUIExportExtension = {
param($Form)
if (-not $Form) { return }
$chk = & $sbAddCheckbox -Form $Form `
-CheckboxName 'chkExportScript' `
-LabelText 'Export script' `
-ToolTip 'Export the script associated with selected profiles' `
-InitialChecked $true
if (-not $chk) { return }
& $sbSetCache "ExportScripts" $chk.IsChecked
$chk.add_IsCheckedChanged({
param($s, $e)
& $sbSetCache "ExportScripts" $s.IsChecked
}.GetNewClosure())
}.GetNewClosure()
$SBAddScriptUIDetailsExtension = {
param($Form)
if (-not $Form) { return }
$onDownload = {
param($s, $e)
$row = $script:dgIntuneManagerObjects.SelectedItem
$sel = if ($row) { $row.Source } else { $null }
if (-not $sel) { return }
try { & $sbInvokeDownloadScript $sel }
catch { & $sbWriteLogError "Download script failed" $_.Exception }
}.GetNewClosure()
$onEdit = {
param($s, $e)
$row = $script:dgIntuneManagerObjects.SelectedItem
$sel = if ($row) { $row.Source } else { $null }
if (-not $sel) { return }
try { & $sbInvokeEditScript $sel }
catch { & $sbWriteLogError "Edit script failed" $_.Exception }
}.GetNewClosure()
& $sbAddDetailsBtn -Form $Form -Name 'btnDownload' -Content 'Download' `
-InsertIndex 0 -OnClick $onDownload | Out-Null
& $sbAddDetailsBtn -Form $Form -Name 'btnEdit' -Content 'Edit' `
-InsertIndex 1 -OnClick $onEdit | Out-Null
}.GetNewClosure()
##############################################
# Init
##############################################
function Invoke-InitializeScriptUIExtensions
{
$typeClasses = @()
Get-SubClasses "ScriptTypeBase" | ForEach-Object {
try {
$tmpClass = Get-SingletonObject $_.Name
if ($null -ne $tmpClass.PolicyGroup) {
$typeClasses += $tmpClass
}
} catch {}
}
foreach ($typeClass in $typeClasses) {
Add-ObjectMethod $typeClass "AddUIExportExtensions" $SBAddScriptUIExportExtension
Add-ObjectMethod $typeClass "AddUIDetailsExtension" $SBAddScriptUIDetailsExtension
}
}
Invoke-InitializeScriptUIExtensions