4180 lines
198 KiB
PowerShell
4180 lines
198 KiB
PowerShell
Add-Type -AssemblyName System.Windows.Forms
|
|
Add-Type -AssemblyName System.Drawing
|
|
|
|
#region ===== Shared Helpers =====
|
|
|
|
function Write-Log {
|
|
param([string]$Message, [string]$Color = "LightGreen")
|
|
if ($script:LogBox -and !$script:LogBox.IsDisposed) {
|
|
$script:LogBox.SelectionStart = $script:LogBox.TextLength
|
|
$script:LogBox.SelectionLength = 0
|
|
$script:LogBox.SelectionColor = [System.Drawing.Color]::$Color
|
|
$script:LogBox.AppendText("$(Get-Date -Format 'HH:mm:ss') - $Message`n")
|
|
$script:LogBox.ScrollToCaret()
|
|
[System.Windows.Forms.Application]::DoEvents()
|
|
}
|
|
Write-Host $Message
|
|
}
|
|
|
|
function EscHtml([string]$s) {
|
|
return $s -replace '&','&' -replace '<','<' -replace '>','>' -replace '"','"'
|
|
}
|
|
|
|
function Format-Bytes([long]$b) {
|
|
if ($b -ge 1GB) { return "$([math]::Round($b/1GB,2)) GB" }
|
|
if ($b -ge 1MB) { return "$([math]::Round($b/1MB,1)) MB" }
|
|
if ($b -ge 1KB) { return "$([math]::Round($b/1KB,0)) KB" }
|
|
return "$b B"
|
|
}
|
|
|
|
function Validate-Inputs {
|
|
if ([string]::IsNullOrWhiteSpace($script:txtClientId.Text)) {
|
|
[System.Windows.Forms.MessageBox]::Show("Please enter a Client ID.", "Missing Field", "OK", "Warning")
|
|
return $false
|
|
}
|
|
$hasSites = ($script:SelectedSites -and $script:SelectedSites.Count -gt 0)
|
|
if (-not $hasSites -and [string]::IsNullOrWhiteSpace($script:txtSiteURL.Text)) {
|
|
[System.Windows.Forms.MessageBox]::Show(
|
|
"Please enter a Site URL or select sites via 'Voir les sites'.",
|
|
"Missing Field", "OK", "Warning")
|
|
return $false
|
|
}
|
|
return $true
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ===== Profile Management =====
|
|
|
|
function Get-ProfilesFilePath {
|
|
$dir = if ($script:DataFolder) { $script:DataFolder }
|
|
elseif ($PSScriptRoot) { $PSScriptRoot }
|
|
else { $PWD.Path }
|
|
return Join-Path $dir "Sharepoint_Export_profiles.json"
|
|
}
|
|
|
|
function Load-Profiles {
|
|
$path = Get-ProfilesFilePath
|
|
if (Test-Path $path) {
|
|
try {
|
|
$data = Get-Content $path -Raw | ConvertFrom-Json
|
|
if ($data.profiles) { return @($data.profiles) }
|
|
} catch {}
|
|
}
|
|
return @()
|
|
}
|
|
|
|
function Save-Profiles {
|
|
param([array]$Profiles)
|
|
$path = Get-ProfilesFilePath
|
|
@{ profiles = @($Profiles) } | ConvertTo-Json -Depth 5 | Set-Content $path -Encoding UTF8
|
|
}
|
|
|
|
function Show-InputDialog {
|
|
param(
|
|
[string]$Prompt,
|
|
[string]$Title,
|
|
[string]$Default = "",
|
|
[System.Windows.Forms.Form]$Owner = $null
|
|
)
|
|
$dlg = New-Object System.Windows.Forms.Form
|
|
$dlg.Text = $Title
|
|
$dlg.Size = New-Object System.Drawing.Size(380, 140)
|
|
$dlg.StartPosition = "CenterParent"
|
|
$dlg.FormBorderStyle = "FixedDialog"
|
|
$dlg.MaximizeBox = $false
|
|
$dlg.MinimizeBox = $false
|
|
$dlg.BackColor = [System.Drawing.Color]::WhiteSmoke
|
|
$lbl = New-Object System.Windows.Forms.Label
|
|
$lbl.Text = $Prompt; $lbl.Location = New-Object System.Drawing.Point(10,10)
|
|
$lbl.Size = New-Object System.Drawing.Size(340,20)
|
|
$txt = New-Object System.Windows.Forms.TextBox
|
|
$txt.Text = $Default; $txt.Location = New-Object System.Drawing.Point(10,36)
|
|
$txt.Size = New-Object System.Drawing.Size(340,22)
|
|
$btnOK = New-Object System.Windows.Forms.Button
|
|
$btnOK.Text = "OK"; $btnOK.Location = New-Object System.Drawing.Point(152,70)
|
|
$btnOK.Size = New-Object System.Drawing.Size(80,26); $btnOK.DialogResult = "OK"
|
|
$dlg.AcceptButton = $btnOK
|
|
$btnCancel = New-Object System.Windows.Forms.Button
|
|
$btnCancel.Text = "Annuler"; $btnCancel.Location = New-Object System.Drawing.Point(246,70)
|
|
$btnCancel.Size = New-Object System.Drawing.Size(104,26); $btnCancel.DialogResult = "Cancel"
|
|
$dlg.CancelButton = $btnCancel
|
|
$dlg.Controls.AddRange(@($lbl, $txt, $btnOK, $btnCancel))
|
|
$result = if ($Owner) { $dlg.ShowDialog($Owner) } else { $dlg.ShowDialog() }
|
|
if ($result -eq "OK") { return $txt.Text.Trim() }
|
|
return $null
|
|
}
|
|
|
|
function Refresh-ProfileList {
|
|
$script:Profiles = Load-Profiles
|
|
$script:cboProfile.Items.Clear()
|
|
foreach ($p in $script:Profiles) { [void]$script:cboProfile.Items.Add($p.name) }
|
|
if ($script:cboProfile.Items.Count -gt 0) { $script:cboProfile.SelectedIndex = 0 }
|
|
}
|
|
|
|
function Apply-Profile {
|
|
param([int]$idx)
|
|
if ($idx -lt 0 -or $idx -ge $script:Profiles.Count) { return }
|
|
$p = $script:Profiles[$idx]
|
|
if ($p.clientId) { $script:txtClientId.Text = $p.clientId }
|
|
if ($p.tenantUrl) { $script:txtTenantUrl.Text = $p.tenantUrl }
|
|
# Reset site selection when switching profile
|
|
$script:SelectedSites = @()
|
|
$script:btnBrowseSites.Text = "Voir les sites"
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ===== Settings =====
|
|
|
|
function Get-SettingsFilePath {
|
|
$dir = if ($PSScriptRoot) { $PSScriptRoot } else { $PWD.Path }
|
|
return Join-Path $dir "Sharepoint_Settings.json"
|
|
}
|
|
|
|
function Load-Settings {
|
|
$path = Get-SettingsFilePath
|
|
if (Test-Path $path) {
|
|
try {
|
|
$data = Get-Content $path -Raw | ConvertFrom-Json
|
|
return $data
|
|
} catch {}
|
|
}
|
|
return [PSCustomObject]@{ dataFolder = ""; lang = "en" }
|
|
}
|
|
|
|
function Save-Settings {
|
|
param([string]$DataFolder, [string]$Lang = "en")
|
|
$path = Get-SettingsFilePath
|
|
[PSCustomObject]@{ dataFolder = $DataFolder; lang = $Lang } |
|
|
ConvertTo-Json | Set-Content $path -Encoding UTF8
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ===== Site Picker =====
|
|
|
|
# All state in $script:_pkl; accessible from any event handler (no closure tricks needed)
|
|
|
|
function _Pkl-FormatMB([long]$mb) {
|
|
if ($mb -ge 1024) { return "$([math]::Round($mb / 1024, 1)) GB" }
|
|
if ($mb -gt 0) { return "$mb MB" }
|
|
return "-"
|
|
}
|
|
|
|
function _Pkl-Sort {
|
|
$col = $script:_pkl.SortCol
|
|
$desc = -not $script:_pkl.SortAsc
|
|
$script:_pkl.AllSites = @(switch ($col) {
|
|
0 { $script:_pkl.AllSites | Sort-Object -Property Title -Descending:$desc }
|
|
1 { $script:_pkl.AllSites | Sort-Object -Property { [int][bool]$_.IsTeamsConnected } -Descending:$desc }
|
|
2 { $script:_pkl.AllSites | Sort-Object -Property { [long]$_.StorageUsageCurrent } -Descending:$desc }
|
|
default { $script:_pkl.AllSites | Sort-Object -Property Title -Descending:$desc }
|
|
})
|
|
$lv = $script:_pkl.Lv
|
|
$dir = if (-not $desc) { " ^" } else { " v" }
|
|
$script:_pkl.ColNames | ForEach-Object -Begin { $i = 0 } -Process {
|
|
$lv.Columns[$i].Text = $_ + $(if ($i -eq $col) { $dir } else { "" })
|
|
$i++
|
|
}
|
|
}
|
|
|
|
function _Pkl-Repopulate {
|
|
$filter = $script:_pkl.TxtFilter.Text.ToLower()
|
|
$lv = $script:_pkl.Lv
|
|
$script:_pkl.SuppressCheck = $true
|
|
$lv.BeginUpdate()
|
|
$lv.Items.Clear()
|
|
$visible = if ($filter) {
|
|
$script:_pkl.AllSites | Where-Object {
|
|
$_.Title.ToLower().Contains($filter) -or $_.Url.ToLower().Contains($filter)
|
|
}
|
|
} else { $script:_pkl.AllSites }
|
|
foreach ($s in $visible) {
|
|
$teams = if ($s.IsTeamsConnected) { "Oui" } else { "Non" }
|
|
$stor = _Pkl-FormatMB ([long]$s.StorageUsageCurrent)
|
|
$item = New-Object System.Windows.Forms.ListViewItem($s.Title)
|
|
$item.Tag = $s.Url
|
|
[void]$item.SubItems.Add($teams)
|
|
[void]$item.SubItems.Add($stor)
|
|
[void]$item.SubItems.Add($s.Url)
|
|
[void]$lv.Items.Add($item)
|
|
$item.Checked = $script:_pkl.CheckedUrls.Contains($s.Url)
|
|
}
|
|
$lv.EndUpdate()
|
|
$script:_pkl.SuppressCheck = $false
|
|
$script:_pkl.LblStatus.Text = "$($script:_pkl.CheckedUrls.Count) coche(s) -- " +
|
|
"$($lv.Items.Count) affiche(s) sur $($script:_pkl.AllSites.Count)"
|
|
$script:_pkl.LblStatus.ForeColor = [System.Drawing.Color]::Gray
|
|
}
|
|
|
|
function Show-SitePicker {
|
|
param(
|
|
[string]$TenantUrl,
|
|
[string]$ClientId,
|
|
[System.Windows.Forms.Form]$Owner = $null
|
|
)
|
|
|
|
$dlg = New-Object System.Windows.Forms.Form
|
|
$dlg.Text = "Sites SharePoint -- $TenantUrl"
|
|
$dlg.Size = New-Object System.Drawing.Size(900, 580)
|
|
$dlg.StartPosition = "CenterParent"
|
|
$dlg.FormBorderStyle = "Sizable"
|
|
$dlg.MinimumSize = New-Object System.Drawing.Size(600, 440)
|
|
$dlg.BackColor = [System.Drawing.Color]::WhiteSmoke
|
|
|
|
# -- Top bar --
|
|
$lblFilter = New-Object System.Windows.Forms.Label
|
|
$lblFilter.Text = "Filtrer :"
|
|
$lblFilter.Location = New-Object System.Drawing.Point(10, 12)
|
|
$lblFilter.Size = New-Object System.Drawing.Size(52, 22)
|
|
$lblFilter.TextAlign = "MiddleLeft"
|
|
|
|
$txtFilter = New-Object System.Windows.Forms.TextBox
|
|
$txtFilter.Location = New-Object System.Drawing.Point(66, 10)
|
|
$txtFilter.Size = New-Object System.Drawing.Size(570, 22)
|
|
$txtFilter.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
$txtFilter.Anchor = [System.Windows.Forms.AnchorStyles]"Top,Left,Right"
|
|
|
|
$btnLoad = New-Object System.Windows.Forms.Button
|
|
$btnLoad.Text = "Charger les sites"
|
|
$btnLoad.Location = New-Object System.Drawing.Point(648, 8)
|
|
$btnLoad.Size = New-Object System.Drawing.Size(148, 26)
|
|
$btnLoad.Anchor = [System.Windows.Forms.AnchorStyles]"Top,Right"
|
|
$btnLoad.BackColor = [System.Drawing.Color]::SteelBlue
|
|
$btnLoad.ForeColor = [System.Drawing.Color]::White
|
|
$btnLoad.FlatStyle = "Flat"
|
|
$btnLoad.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
|
|
|
|
# -- Site list (ListView with columns) --
|
|
$lv = New-Object System.Windows.Forms.ListView
|
|
$lv.Location = New-Object System.Drawing.Point(10, 44)
|
|
$lv.Size = New-Object System.Drawing.Size(864, 400)
|
|
$lv.Anchor = [System.Windows.Forms.AnchorStyles]"Top,Bottom,Left,Right"
|
|
$lv.View = [System.Windows.Forms.View]::Details
|
|
$lv.CheckBoxes = $true
|
|
$lv.FullRowSelect = $true
|
|
$lv.GridLines = $true
|
|
$lv.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
$lv.HeaderStyle = [System.Windows.Forms.ColumnHeaderStyle]::Clickable
|
|
|
|
[void]$lv.Columns.Add("Nom", 380)
|
|
[void]$lv.Columns.Add("Equipe Teams", 90)
|
|
[void]$lv.Columns.Add("Stockage", 100)
|
|
[void]$lv.Columns.Add("URL", 280)
|
|
|
|
# -- Status bar --
|
|
$lblStatus = New-Object System.Windows.Forms.Label
|
|
$lblStatus.Text = "Cliquez sur 'Charger les sites' pour recuperer la liste du tenant."
|
|
$lblStatus.Location = New-Object System.Drawing.Point(10, 456)
|
|
$lblStatus.Size = New-Object System.Drawing.Size(860, 18)
|
|
$lblStatus.Anchor = [System.Windows.Forms.AnchorStyles]"Bottom,Left,Right"
|
|
$lblStatus.ForeColor = [System.Drawing.Color]::Gray
|
|
$lblStatus.Font = New-Object System.Drawing.Font("Segoe UI", 8, [System.Drawing.FontStyle]::Italic)
|
|
|
|
# -- Bottom buttons --
|
|
$btnSelAll = New-Object System.Windows.Forms.Button
|
|
$btnSelAll.Text = "Tout selectionner"
|
|
$btnSelAll.Location = New-Object System.Drawing.Point(10, 484)
|
|
$btnSelAll.Size = New-Object System.Drawing.Size(130, 26)
|
|
$btnSelAll.Anchor = [System.Windows.Forms.AnchorStyles]"Bottom,Left"
|
|
|
|
$btnSelNone = New-Object System.Windows.Forms.Button
|
|
$btnSelNone.Text = "Tout decocher"
|
|
$btnSelNone.Location = New-Object System.Drawing.Point(148, 484)
|
|
$btnSelNone.Size = New-Object System.Drawing.Size(110, 26)
|
|
$btnSelNone.Anchor = [System.Windows.Forms.AnchorStyles]"Bottom,Left"
|
|
|
|
$btnOK = New-Object System.Windows.Forms.Button
|
|
$btnOK.Text = "OK"
|
|
$btnOK.Location = New-Object System.Drawing.Point(694, 484)
|
|
$btnOK.Size = New-Object System.Drawing.Size(90, 26)
|
|
$btnOK.Anchor = [System.Windows.Forms.AnchorStyles]"Bottom,Right"
|
|
$btnOK.DialogResult = "OK"
|
|
$btnOK.BackColor = [System.Drawing.Color]::SteelBlue
|
|
$btnOK.ForeColor = [System.Drawing.Color]::White
|
|
$btnOK.FlatStyle = "Flat"
|
|
$btnOK.Enabled = $false
|
|
|
|
$btnDlgCancel = New-Object System.Windows.Forms.Button
|
|
$btnDlgCancel.Text = "Annuler"
|
|
$btnDlgCancel.Location = New-Object System.Drawing.Point(794, 484)
|
|
$btnDlgCancel.Size = New-Object System.Drawing.Size(90, 26)
|
|
$btnDlgCancel.Anchor = [System.Windows.Forms.AnchorStyles]"Bottom,Right"
|
|
$btnDlgCancel.DialogResult = "Cancel"
|
|
|
|
$dlg.AcceptButton = $btnOK
|
|
$dlg.CancelButton = $btnDlgCancel
|
|
$dlg.Controls.AddRange(@($lblFilter, $txtFilter, $btnLoad, $lv, $lblStatus,
|
|
$btnSelAll, $btnSelNone, $btnOK, $btnDlgCancel))
|
|
|
|
# Init script-scope state (modal dialog - no concurrency issue)
|
|
$script:_pkl = @{
|
|
AllSites = @()
|
|
CheckedUrls = [System.Collections.Generic.HashSet[string]]::new(
|
|
[System.StringComparer]::OrdinalIgnoreCase)
|
|
Lv = $lv
|
|
TxtFilter = $txtFilter
|
|
LblStatus = $lblStatus
|
|
BtnOK = $btnOK
|
|
BtnLoad = $btnLoad
|
|
SuppressCheck = $false
|
|
SortCol = 0
|
|
SortAsc = $true
|
|
ColNames = @("Nom", "Equipe Teams", "Stockage", "URL")
|
|
Sync = $null
|
|
Timer = $null
|
|
RS = $null
|
|
PS = $null
|
|
Hnd = $null
|
|
AdminUrl = if ($TenantUrl -match '^(https?://[^.]+)(\.sharepoint\.com.*)') {
|
|
"$($Matches[1])-admin$($Matches[2])"
|
|
} else { $null }
|
|
ClientId = $ClientId
|
|
}
|
|
|
|
# ItemChecked fires AFTER the checked state changes (unlike ItemCheck)
|
|
$lv.Add_ItemChecked({
|
|
param($s, $e)
|
|
if ($script:_pkl.SuppressCheck) { return }
|
|
$url = $e.Item.Tag
|
|
if ($e.Item.Checked) { [void]$script:_pkl.CheckedUrls.Add($url) }
|
|
else { [void]$script:_pkl.CheckedUrls.Remove($url) }
|
|
$script:_pkl.LblStatus.Text = "$($script:_pkl.CheckedUrls.Count) coche(s) -- " +
|
|
"$($script:_pkl.Lv.Items.Count) affiche(s) sur $($script:_pkl.AllSites.Count)"
|
|
})
|
|
|
|
$lv.Add_ColumnClick({
|
|
param($s, $e)
|
|
if ($script:_pkl.SortCol -eq $e.Column) {
|
|
$script:_pkl.SortAsc = -not $script:_pkl.SortAsc
|
|
} else {
|
|
$script:_pkl.SortCol = $e.Column
|
|
$script:_pkl.SortAsc = $true
|
|
}
|
|
_Pkl-Sort
|
|
_Pkl-Repopulate
|
|
})
|
|
|
|
$txtFilter.Add_TextChanged({ _Pkl-Repopulate })
|
|
|
|
$btnSelAll.Add_Click({
|
|
$script:_pkl.CheckedUrls.Clear()
|
|
$script:_pkl.SuppressCheck = $true
|
|
foreach ($item in $script:_pkl.Lv.Items) {
|
|
$item.Checked = $true
|
|
[void]$script:_pkl.CheckedUrls.Add($item.Tag)
|
|
}
|
|
$script:_pkl.SuppressCheck = $false
|
|
$script:_pkl.LblStatus.Text = "$($script:_pkl.CheckedUrls.Count) coche(s) -- " +
|
|
"$($script:_pkl.Lv.Items.Count) affiche(s) sur $($script:_pkl.AllSites.Count)"
|
|
})
|
|
|
|
$btnSelNone.Add_Click({
|
|
$script:_pkl.CheckedUrls.Clear()
|
|
$script:_pkl.SuppressCheck = $true
|
|
foreach ($item in $script:_pkl.Lv.Items) { $item.Checked = $false }
|
|
$script:_pkl.SuppressCheck = $false
|
|
$script:_pkl.LblStatus.Text = "0 coche(s) -- " +
|
|
"$($script:_pkl.Lv.Items.Count) affiche(s) sur $($script:_pkl.AllSites.Count)"
|
|
})
|
|
|
|
$btnLoad.Add_Click({
|
|
if (-not $script:_pkl.AdminUrl) {
|
|
[System.Windows.Forms.MessageBox]::Show(
|
|
"URL Tenant invalide (attendu: https://xxx.sharepoint.com).",
|
|
"Erreur", "OK", "Error")
|
|
return
|
|
}
|
|
$script:_pkl.BtnLoad.Enabled = $false
|
|
$script:_pkl.BtnOK.Enabled = $false
|
|
$script:_pkl.Lv.Items.Clear()
|
|
$script:_pkl.AllSites = @()
|
|
$script:_pkl.LblStatus.Text = "Connexion a $($script:_pkl.AdminUrl) ..."
|
|
$script:_pkl.LblStatus.ForeColor = [System.Drawing.Color]::DarkOrange
|
|
|
|
$sync = [hashtable]::Synchronized(@{ Done = $false; Error = $null; Sites = $null })
|
|
$script:_pkl.Sync = $sync
|
|
|
|
$bgFetch = {
|
|
param($AdminUrl, $ClientId, $Sync)
|
|
try {
|
|
Import-Module PnP.PowerShell -ErrorAction Stop
|
|
Connect-PnPOnline -Url $AdminUrl -Interactive -ClientId $ClientId
|
|
$Sync.Sites = @(
|
|
Get-PnPTenantSite |
|
|
Select-Object Title, Url, IsTeamsConnected, StorageUsageCurrent |
|
|
Sort-Object Title
|
|
)
|
|
} catch { $Sync.Error = $_.Exception.Message }
|
|
finally { $Sync.Done = $true }
|
|
}
|
|
|
|
$rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
|
|
$rs.ApartmentState = "STA"; $rs.ThreadOptions = "ReuseThread"; $rs.Open()
|
|
$ps = [System.Management.Automation.PowerShell]::Create()
|
|
$ps.Runspace = $rs
|
|
[void]$ps.AddScript($bgFetch)
|
|
[void]$ps.AddArgument($script:_pkl.AdminUrl)
|
|
[void]$ps.AddArgument($script:_pkl.ClientId)
|
|
[void]$ps.AddArgument($sync)
|
|
$script:_pkl.RS = $rs
|
|
$script:_pkl.PS = $ps
|
|
$script:_pkl.Hnd = $ps.BeginInvoke()
|
|
|
|
$tmr = New-Object System.Windows.Forms.Timer
|
|
$tmr.Interval = 300
|
|
$script:_pkl.Timer = $tmr
|
|
|
|
$tmr.Add_Tick({
|
|
if ($script:_pkl.Sync.Done) {
|
|
$script:_pkl.Timer.Stop(); $script:_pkl.Timer.Dispose()
|
|
try { [void]$script:_pkl.PS.EndInvoke($script:_pkl.Hnd) } catch {}
|
|
try { $script:_pkl.RS.Close(); $script:_pkl.RS.Dispose() } catch {}
|
|
$script:_pkl.BtnLoad.Enabled = $true
|
|
if ($script:_pkl.Sync.Error) {
|
|
$script:_pkl.LblStatus.Text = "Erreur: $($script:_pkl.Sync.Error)"
|
|
$script:_pkl.LblStatus.ForeColor = [System.Drawing.Color]::Red
|
|
} else {
|
|
$script:_pkl.AllSites = @($script:_pkl.Sync.Sites)
|
|
_Pkl-Sort
|
|
_Pkl-Repopulate
|
|
$script:_pkl.BtnOK.Enabled = ($script:_pkl.Lv.Items.Count -gt 0)
|
|
}
|
|
} else {
|
|
$dot = "." * (([System.DateTime]::Now.Second % 4) + 1)
|
|
$script:_pkl.LblStatus.Text = "Chargement$dot"
|
|
}
|
|
})
|
|
$tmr.Start()
|
|
})
|
|
|
|
$result = if ($Owner) { $dlg.ShowDialog($Owner) } else { $dlg.ShowDialog() }
|
|
|
|
if ($result -eq "OK") { return @($script:_pkl.CheckedUrls) }
|
|
return @()
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ===== Template Management =====
|
|
|
|
function Get-TemplatesFilePath {
|
|
$dir = if ($script:DataFolder) { $script:DataFolder }
|
|
elseif ($PSScriptRoot) { $PSScriptRoot }
|
|
else { $PWD.Path }
|
|
return Join-Path $dir "Sharepoint_Templates.json"
|
|
}
|
|
|
|
function Load-Templates {
|
|
$path = Get-TemplatesFilePath
|
|
if (Test-Path $path) {
|
|
try {
|
|
$data = Get-Content $path -Raw | ConvertFrom-Json
|
|
if ($data.templates) { return @($data.templates) }
|
|
} catch {}
|
|
}
|
|
return @()
|
|
}
|
|
|
|
function Save-Templates {
|
|
param([array]$Templates)
|
|
$path = Get-TemplatesFilePath
|
|
@{ templates = @($Templates) } | ConvertTo-Json -Depth 20 | Set-Content $path -Encoding UTF8
|
|
}
|
|
|
|
# Script-scope helpers (accessible from all event handlers - no closure tricks)
|
|
function _Tpl-Repopulate {
|
|
$lv = $script:_tpl.Lv
|
|
$lv.BeginUpdate()
|
|
$lv.Items.Clear()
|
|
foreach ($t in $script:_tpl.Templates) {
|
|
$opts = @()
|
|
if ($t.options.structure) { $opts += "Arborescence" }
|
|
if ($t.options.permissions) { $opts += "Permissions" }
|
|
if ($t.options.settings) { $opts += "Parametres" }
|
|
if ($t.options.style) { $opts += "Style" }
|
|
$item = New-Object System.Windows.Forms.ListViewItem($t.name)
|
|
$item.Tag = $t
|
|
$srcText = if ($t.sourceUrl) { $t.sourceUrl } else { "" }
|
|
$datText = if ($t.capturedAt) { $t.capturedAt } else { "" }
|
|
[void]$item.SubItems.Add($srcText)
|
|
[void]$item.SubItems.Add($datText)
|
|
[void]$item.SubItems.Add(($opts -join ", "))
|
|
[void]$lv.Items.Add($item)
|
|
}
|
|
$lv.EndUpdate()
|
|
$script:_tpl.BtnDelete.Enabled = $false
|
|
$script:_tpl.BtnCreate.Enabled = $false
|
|
}
|
|
|
|
function _Tpl-RefreshCombo {
|
|
$cbo = $script:_tpl.CboCrTpl
|
|
$cbo.Items.Clear()
|
|
foreach ($t in $script:_tpl.Templates) { [void]$cbo.Items.Add($t.name) }
|
|
if ($cbo.Items.Count -gt 0) { $cbo.SelectedIndex = 0 }
|
|
}
|
|
|
|
function _Tpl-Log {
|
|
param([System.Windows.Forms.RichTextBox]$Box, [string]$Msg, [string]$Color = "LightGreen")
|
|
$Box.SelectionStart = $Box.TextLength
|
|
$Box.SelectionLength = 0
|
|
$Box.SelectionColor = [System.Drawing.Color]::$Color
|
|
$Box.AppendText("$(Get-Date -Format 'HH:mm:ss') - $Msg`n")
|
|
$Box.ScrollToCaret()
|
|
}
|
|
|
|
function Show-TemplateManager {
|
|
param(
|
|
[string]$DefaultSiteUrl = "",
|
|
[string]$ClientId = "",
|
|
[string]$TenantUrl = "",
|
|
[System.Windows.Forms.Form]$Owner = $null
|
|
)
|
|
|
|
$dlg = New-Object System.Windows.Forms.Form
|
|
$dlg.Text = "Gestionnaire de Templates SharePoint"
|
|
$dlg.Size = New-Object System.Drawing.Size(980, 700)
|
|
$dlg.StartPosition = "CenterParent"
|
|
$dlg.FormBorderStyle = "Sizable"
|
|
$dlg.MinimumSize = New-Object System.Drawing.Size(700, 560)
|
|
$dlg.BackColor = [System.Drawing.Color]::WhiteSmoke
|
|
|
|
# ── Template list ──────────────────────────────────────────────────────
|
|
$grpList = New-Object System.Windows.Forms.GroupBox
|
|
$grpList.Text = "Templates enregistres"
|
|
$grpList.Location = New-Object System.Drawing.Point(10, 8)
|
|
$grpList.Size = New-Object System.Drawing.Size(950, 174)
|
|
$grpList.Anchor = [System.Windows.Forms.AnchorStyles]"Top,Left,Right"
|
|
$grpList.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
|
|
|
|
$lv = New-Object System.Windows.Forms.ListView
|
|
$lv.Location = New-Object System.Drawing.Point(8, 22)
|
|
$lv.Size = New-Object System.Drawing.Size(820, 138)
|
|
$lv.Anchor = [System.Windows.Forms.AnchorStyles]"Top,Left,Right"
|
|
$lv.View = [System.Windows.Forms.View]::Details
|
|
$lv.FullRowSelect = $true
|
|
$lv.GridLines = $true
|
|
$lv.MultiSelect = $false
|
|
$lv.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Regular)
|
|
[void]$lv.Columns.Add("Nom", 200)
|
|
[void]$lv.Columns.Add("Site source", 280)
|
|
[void]$lv.Columns.Add("Date", 120)
|
|
[void]$lv.Columns.Add("Options capturees", 200)
|
|
|
|
$btnDelete = New-Object System.Windows.Forms.Button
|
|
$btnDelete.Text = "Supprimer"
|
|
$btnDelete.Location = New-Object System.Drawing.Point(836, 22)
|
|
$btnDelete.Size = New-Object System.Drawing.Size(106, 26)
|
|
$btnDelete.Anchor = [System.Windows.Forms.AnchorStyles]"Top,Right"
|
|
$btnDelete.Enabled = $false
|
|
|
|
$btnCreate = New-Object System.Windows.Forms.Button
|
|
$btnCreate.Text = "Creer depuis ce template >"
|
|
$btnCreate.Location = New-Object System.Drawing.Point(836, 56)
|
|
$btnCreate.Size = New-Object System.Drawing.Size(106, 42)
|
|
$btnCreate.Anchor = [System.Windows.Forms.AnchorStyles]"Top,Right"
|
|
$btnCreate.Enabled = $false
|
|
$btnCreate.BackColor = [System.Drawing.Color]::SteelBlue
|
|
$btnCreate.ForeColor = [System.Drawing.Color]::White
|
|
$btnCreate.FlatStyle = "Flat"
|
|
$btnCreate.Font = New-Object System.Drawing.Font("Segoe UI", 8, [System.Drawing.FontStyle]::Bold)
|
|
|
|
$grpList.Controls.AddRange(@($lv, $btnDelete, $btnCreate))
|
|
|
|
# ── Bottom tabs ────────────────────────────────────────────────────────
|
|
$btabs = New-Object System.Windows.Forms.TabControl
|
|
$btabs.Location = New-Object System.Drawing.Point(10, 190)
|
|
$btabs.Size = New-Object System.Drawing.Size(950, 464)
|
|
$btabs.Anchor = [System.Windows.Forms.AnchorStyles]"Top,Bottom,Left,Right"
|
|
$btabs.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
|
|
# ── Tab: Capturer ──────────────────────────────────────────────────────
|
|
$tabCap = New-Object System.Windows.Forms.TabPage
|
|
$tabCap.Text = " Capturer un template "
|
|
$tabCap.BackColor = [System.Drawing.Color]::WhiteSmoke
|
|
|
|
$mkLbl = {
|
|
param($t,$x,$y,$w=110)
|
|
$l = New-Object System.Windows.Forms.Label
|
|
$l.Text = $t; $l.Location = New-Object System.Drawing.Point($x,$y)
|
|
$l.Size = New-Object System.Drawing.Size($w,22); $l.TextAlign = "MiddleLeft"; $l
|
|
}
|
|
|
|
$lblCapSrc = & $mkLbl "Site source :" 10 18
|
|
$txtCapSrc = New-Object System.Windows.Forms.TextBox
|
|
$txtCapSrc.Location = New-Object System.Drawing.Point(128, 18)
|
|
$txtCapSrc.Size = New-Object System.Drawing.Size(560, 22)
|
|
$txtCapSrc.Text = $DefaultSiteUrl
|
|
$txtCapSrc.Font = New-Object System.Drawing.Font("Consolas", 9)
|
|
|
|
$lblCapName = & $mkLbl "Nom du template :" 10 48 120
|
|
$txtCapName = New-Object System.Windows.Forms.TextBox
|
|
$txtCapName.Location = New-Object System.Drawing.Point(134, 48)
|
|
$txtCapName.Size = New-Object System.Drawing.Size(300, 22)
|
|
$txtCapName.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
|
|
$grpOpts = New-Object System.Windows.Forms.GroupBox
|
|
$grpOpts.Text = "Elements a capturer"
|
|
$grpOpts.Location = New-Object System.Drawing.Point(10, 80)
|
|
$grpOpts.Size = New-Object System.Drawing.Size(680, 68)
|
|
$grpOpts.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
|
|
|
|
$mkChk = {
|
|
param($t,$x,$y,$w,$checked=$false)
|
|
$c = New-Object System.Windows.Forms.CheckBox
|
|
$c.Text = $t; $c.Location = New-Object System.Drawing.Point($x,$y)
|
|
$c.Size = New-Object System.Drawing.Size($w,20)
|
|
$c.Font = New-Object System.Drawing.Font("Segoe UI",9,[System.Drawing.FontStyle]::Regular)
|
|
$c.Checked = $checked; $c
|
|
}
|
|
|
|
$chkCapStruct = & $mkChk "Arborescence (bibliotheques et dossiers)" 10 22 300 $true
|
|
$chkCapPerms = & $mkChk "Permissions (groupes et roles)" 320 22 260
|
|
$chkCapSettings = & $mkChk "Parametres du site (titre, langue...)" 10 44 300 $true
|
|
$chkCapStyle = & $mkChk "Style (logo)" 320 44 200
|
|
$grpOpts.Controls.AddRange(@($chkCapStruct, $chkCapPerms, $chkCapSettings, $chkCapStyle))
|
|
|
|
$btnCapture = New-Object System.Windows.Forms.Button
|
|
$btnCapture.Text = "Lancer la capture"
|
|
$btnCapture.Location = New-Object System.Drawing.Point(10, 162)
|
|
$btnCapture.Size = New-Object System.Drawing.Size(155, 34)
|
|
$btnCapture.BackColor = [System.Drawing.Color]::FromArgb(16,124,16)
|
|
$btnCapture.ForeColor = [System.Drawing.Color]::White
|
|
$btnCapture.FlatStyle = "Flat"
|
|
$btnCapture.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
|
|
|
|
$txtCapLog = New-Object System.Windows.Forms.RichTextBox
|
|
$txtCapLog.Location = New-Object System.Drawing.Point(10, 206)
|
|
$txtCapLog.Size = New-Object System.Drawing.Size(918, 208)
|
|
$txtCapLog.Anchor = [System.Windows.Forms.AnchorStyles]"Top,Bottom,Left,Right"
|
|
$txtCapLog.ReadOnly = $true
|
|
$txtCapLog.BackColor = [System.Drawing.Color]::Black
|
|
$txtCapLog.ForeColor = [System.Drawing.Color]::LightGreen
|
|
$txtCapLog.Font = New-Object System.Drawing.Font("Consolas", 8)
|
|
$txtCapLog.ScrollBars = "Vertical"
|
|
|
|
$tabCap.Controls.AddRange(@($lblCapSrc, $txtCapSrc, $lblCapName, $txtCapName,
|
|
$grpOpts, $btnCapture, $txtCapLog))
|
|
|
|
# ── Tab: Creer depuis template ─────────────────────────────────────────
|
|
$tabCr = New-Object System.Windows.Forms.TabPage
|
|
$tabCr.Text = " Creer depuis un template "
|
|
$tabCr.BackColor = [System.Drawing.Color]::WhiteSmoke
|
|
|
|
$lblCrTpl = & $mkLbl "Template :" 10 18
|
|
$cboCrTpl = New-Object System.Windows.Forms.ComboBox
|
|
$cboCrTpl.Location = New-Object System.Drawing.Point(128, 16)
|
|
$cboCrTpl.Size = New-Object System.Drawing.Size(400, 24)
|
|
$cboCrTpl.DropDownStyle = "DropDownList"
|
|
$cboCrTpl.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
|
|
$lblCrTitle = & $mkLbl "Titre du site :" 10 48
|
|
$txtCrTitle = New-Object System.Windows.Forms.TextBox
|
|
$txtCrTitle.Location = New-Object System.Drawing.Point(128, 46)
|
|
$txtCrTitle.Size = New-Object System.Drawing.Size(300, 22)
|
|
$txtCrTitle.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
|
|
$lblCrAlias = & $mkLbl "Alias URL :" 10 78
|
|
$txtCrAlias = New-Object System.Windows.Forms.TextBox
|
|
$txtCrAlias.Location = New-Object System.Drawing.Point(128, 76)
|
|
$txtCrAlias.Size = New-Object System.Drawing.Size(200, 22)
|
|
$txtCrAlias.Font = New-Object System.Drawing.Font("Consolas", 9)
|
|
|
|
$lblCrAliasHint = New-Object System.Windows.Forms.Label
|
|
$lblCrAliasHint.Text = "(lettres, chiffres, tirets uniquement)"
|
|
$lblCrAliasHint.Location = New-Object System.Drawing.Point(336, 78)
|
|
$lblCrAliasHint.Size = New-Object System.Drawing.Size(250, 20)
|
|
$lblCrAliasHint.ForeColor = [System.Drawing.Color]::Gray
|
|
$lblCrAliasHint.Font = New-Object System.Drawing.Font("Segoe UI", 8, [System.Drawing.FontStyle]::Italic)
|
|
|
|
$lblCrType = & $mkLbl "Type :" 10 108
|
|
$radCrTeam = New-Object System.Windows.Forms.RadioButton
|
|
$radCrTeam.Text = "Team Site (avec groupe M365)"
|
|
$radCrTeam.Location = New-Object System.Drawing.Point(128, 108)
|
|
$radCrTeam.Size = New-Object System.Drawing.Size(220, 22)
|
|
$radCrTeam.Checked = $true
|
|
|
|
$radCrComm = New-Object System.Windows.Forms.RadioButton
|
|
$radCrComm.Text = "Communication Site"
|
|
$radCrComm.Location = New-Object System.Drawing.Point(358, 108)
|
|
$radCrComm.Size = New-Object System.Drawing.Size(200, 22)
|
|
|
|
$lblCrOwners = & $mkLbl "Proprietaires :" 10 138
|
|
$txtCrOwners = New-Object System.Windows.Forms.TextBox
|
|
$txtCrOwners.Location = New-Object System.Drawing.Point(128, 136)
|
|
$txtCrOwners.Size = New-Object System.Drawing.Size(550, 22)
|
|
$txtCrOwners.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
$txtCrOwners.PlaceholderText = "user1@domain.com, user2@domain.com"
|
|
|
|
$lblCrMembers = & $mkLbl "Membres :" 10 168
|
|
$txtCrMembers = New-Object System.Windows.Forms.TextBox
|
|
$txtCrMembers.Location = New-Object System.Drawing.Point(128, 166)
|
|
$txtCrMembers.Size = New-Object System.Drawing.Size(410, 22)
|
|
$txtCrMembers.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
$txtCrMembers.PlaceholderText = "user@domain.com, ..."
|
|
|
|
$btnCrCsv = New-Object System.Windows.Forms.Button
|
|
$btnCrCsv.Text = "Importer CSV..."
|
|
$btnCrCsv.Location = New-Object System.Drawing.Point(546, 164)
|
|
$btnCrCsv.Size = New-Object System.Drawing.Size(120, 26)
|
|
|
|
$grpApply = New-Object System.Windows.Forms.GroupBox
|
|
$grpApply.Text = "Appliquer depuis le template"
|
|
$grpApply.Location = New-Object System.Drawing.Point(10, 200)
|
|
$grpApply.Size = New-Object System.Drawing.Size(680, 60)
|
|
$grpApply.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
|
|
|
|
$chkApplyStruct = & $mkChk "Arborescence" 10 22 140 $true
|
|
$chkApplyPerms = & $mkChk "Permissions" 158 22 120
|
|
$chkApplySettings = & $mkChk "Parametres" 286 22 120 $true
|
|
$chkApplyStyle = & $mkChk "Style" 414 22 100
|
|
$grpApply.Controls.AddRange(@($chkApplyStruct, $chkApplyPerms, $chkApplySettings, $chkApplyStyle))
|
|
|
|
$btnCreateSite = New-Object System.Windows.Forms.Button
|
|
$btnCreateSite.Text = "Creer le site"
|
|
$btnCreateSite.Location = New-Object System.Drawing.Point(10, 272)
|
|
$btnCreateSite.Size = New-Object System.Drawing.Size(155, 34)
|
|
$btnCreateSite.BackColor = [System.Drawing.Color]::SteelBlue
|
|
$btnCreateSite.ForeColor = [System.Drawing.Color]::White
|
|
$btnCreateSite.FlatStyle = "Flat"
|
|
$btnCreateSite.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
|
|
|
|
$txtCreateLog = New-Object System.Windows.Forms.RichTextBox
|
|
$txtCreateLog.Location = New-Object System.Drawing.Point(10, 316)
|
|
$txtCreateLog.Size = New-Object System.Drawing.Size(918, 100)
|
|
$txtCreateLog.Anchor = [System.Windows.Forms.AnchorStyles]"Top,Bottom,Left,Right"
|
|
$txtCreateLog.ReadOnly = $true
|
|
$txtCreateLog.BackColor = [System.Drawing.Color]::Black
|
|
$txtCreateLog.ForeColor = [System.Drawing.Color]::LightGreen
|
|
$txtCreateLog.Font = New-Object System.Drawing.Font("Consolas", 8)
|
|
$txtCreateLog.ScrollBars = "Vertical"
|
|
|
|
$tabCr.Controls.AddRange(@(
|
|
$lblCrTpl, $cboCrTpl,
|
|
$lblCrTitle, $txtCrTitle,
|
|
$lblCrAlias, $txtCrAlias, $lblCrAliasHint,
|
|
$lblCrType, $radCrTeam, $radCrComm,
|
|
$lblCrOwners, $txtCrOwners,
|
|
$lblCrMembers, $txtCrMembers, $btnCrCsv,
|
|
$grpApply,
|
|
$btnCreateSite, $txtCreateLog
|
|
))
|
|
|
|
$btabs.TabPages.AddRange(@($tabCap, $tabCr))
|
|
$dlg.Controls.AddRange(@($grpList, $btabs))
|
|
|
|
# ── Init state ─────────────────────────────────────────────────────────
|
|
$script:_tpl = @{
|
|
Lv = $lv
|
|
BtnDelete = $btnDelete
|
|
BtnCreate = $btnCreate
|
|
CboCrTpl = $cboCrTpl
|
|
TxtCapLog = $txtCapLog
|
|
TxtCreateLog = $txtCreateLog
|
|
Templates = @(Load-Templates)
|
|
ClientId = $ClientId
|
|
TenantUrl = $TenantUrl
|
|
CapSync = $null; CapTimer = $null; CapRS = $null; CapPS = $null; CapHnd = $null
|
|
CrSync = $null; CrTimer = $null; CrRS = $null; CrPS = $null; CrHnd = $null
|
|
CapTplName = ""; CapSrcUrl = ""; CapOpts = $null
|
|
}
|
|
_Tpl-Repopulate
|
|
_Tpl-RefreshCombo
|
|
|
|
# ── Event handlers ─────────────────────────────────────────────────────
|
|
$lv.Add_SelectedIndexChanged({
|
|
$sel = $script:_tpl.Lv.SelectedItems.Count -gt 0
|
|
$script:_tpl.BtnDelete.Enabled = $sel
|
|
$script:_tpl.BtnCreate.Enabled = $sel
|
|
})
|
|
|
|
$btnDelete.Add_Click({
|
|
$item = $script:_tpl.Lv.SelectedItems[0]
|
|
if (-not $item) { return }
|
|
$name = $item.Text
|
|
$res = [System.Windows.Forms.MessageBox]::Show(
|
|
"Supprimer le template '$name' ?", "Confirmer", "YesNo", "Warning")
|
|
if ($res -ne "Yes") { return }
|
|
$script:_tpl.Templates = @($script:_tpl.Templates | Where-Object { $_.name -ne $name })
|
|
Save-Templates -Templates $script:_tpl.Templates
|
|
_Tpl-Repopulate
|
|
_Tpl-RefreshCombo
|
|
})
|
|
|
|
$btnCreate.Add_Click({
|
|
$item = $script:_tpl.Lv.SelectedItems[0]
|
|
if (-not $item) { return }
|
|
$t = $item.Tag
|
|
$idx = [array]::IndexOf(($script:_tpl.Templates | ForEach-Object { $_.name }), $t.name)
|
|
if ($idx -ge 0) { $script:_tpl.CboCrTpl.SelectedIndex = $idx }
|
|
$btabs.SelectedTab = $tabCr
|
|
})
|
|
|
|
# Auto-generate alias from title
|
|
$txtCrTitle.Add_TextChanged({
|
|
$a = $txtCrTitle.Text -replace '[^a-zA-Z0-9 \-]','' -replace '\s+','-' -replace '\-+','-'
|
|
$a = $a.ToLower().Trim('-')
|
|
if ($a.Length -gt 64) { $a = $a.Substring(0,64) }
|
|
$txtCrAlias.Text = $a
|
|
})
|
|
|
|
# CSV import for members
|
|
$btnCrCsv.Add_Click({
|
|
$ofd = New-Object System.Windows.Forms.OpenFileDialog
|
|
$ofd.Filter = "CSV (*.csv)|*.csv|Tous (*.*)|*.*"
|
|
$ofd.Title = "Importer des membres depuis CSV"
|
|
if ($ofd.ShowDialog() -ne "OK") { return }
|
|
try {
|
|
$rows = Import-Csv $ofd.FileName
|
|
$emails = $rows | ForEach-Object {
|
|
$r = $_
|
|
$v = if ($r.Email) { $r.Email } elseif ($r.email) { $r.email } `
|
|
elseif ($r.UPN) { $r.UPN } elseif ($r.upn) { $r.upn } `
|
|
elseif ($r.UserPrincipalName) { $r.UserPrincipalName } `
|
|
else { $r.userprincipalname }
|
|
$v
|
|
} | Where-Object { $_ } | Select-Object -Unique
|
|
$txtCrMembers.Text = ($emails -join ", ")
|
|
[System.Windows.Forms.MessageBox]::Show(
|
|
"$($emails.Count) membre(s) importe(s).", "Import CSV", "OK", "Information")
|
|
} catch {
|
|
[System.Windows.Forms.MessageBox]::Show(
|
|
"Erreur CSV: $($_.Exception.Message)", "Erreur", "OK", "Error")
|
|
}
|
|
})
|
|
|
|
# ── Capture button ─────────────────────────────────────────────────────
|
|
$btnCapture.Add_Click({
|
|
$srcUrl = $txtCapSrc.Text.Trim()
|
|
$tplName = $txtCapName.Text.Trim()
|
|
if ([string]::IsNullOrWhiteSpace($srcUrl)) {
|
|
[System.Windows.Forms.MessageBox]::Show("Veuillez saisir l'URL du site source.", "Champ manquant", "OK", "Warning")
|
|
return
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($tplName)) {
|
|
[System.Windows.Forms.MessageBox]::Show("Veuillez saisir un nom pour le template.", "Champ manquant", "OK", "Warning")
|
|
return
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($script:_tpl.ClientId)) {
|
|
[System.Windows.Forms.MessageBox]::Show("Client ID manquant dans le formulaire principal.", "Erreur", "OK", "Warning")
|
|
return
|
|
}
|
|
|
|
# Stash locals so timer tick can read them from script scope
|
|
$script:_tpl.CapTplName = $tplName
|
|
$script:_tpl.CapSrcUrl = $srcUrl
|
|
$script:_tpl.CapOpts = @{
|
|
structure = $chkCapStruct.Checked
|
|
permissions = $chkCapPerms.Checked
|
|
settings = $chkCapSettings.Checked
|
|
style = $chkCapStyle.Checked
|
|
}
|
|
|
|
$btnCapture.Enabled = $false
|
|
$script:_tpl.TxtCapLog.Clear()
|
|
|
|
$bgCapture = {
|
|
param($SiteUrl, $ClientId, $Opts, $Sync)
|
|
function BgLog([string]$m, [string]$c="LightGreen") {
|
|
$Sync.Queue.Enqueue([PSCustomObject]@{ Text=$m; Color=$c })
|
|
}
|
|
function Collect-Folders([string]$SiteRelUrl) {
|
|
$out = [System.Collections.Generic.List[object]]::new()
|
|
try {
|
|
$items = Get-PnPFolderItem -FolderSiteRelativeUrl $SiteRelUrl -ItemType Folder -ErrorAction SilentlyContinue
|
|
foreach ($fi in $items) {
|
|
if ($fi.Name -match '^_|^Forms$') { continue }
|
|
$sub = Collect-Folders "$SiteRelUrl/$($fi.Name)"
|
|
$out.Add(@{ name=$fi.Name; subfolders=@($sub) })
|
|
}
|
|
} catch {}
|
|
return @($out)
|
|
}
|
|
try {
|
|
Import-Module PnP.PowerShell -ErrorAction Stop
|
|
BgLog "Connexion a $SiteUrl..." "Yellow"
|
|
Connect-PnPOnline -Url $SiteUrl -Interactive -ClientId $ClientId
|
|
$web = Get-PnPWeb -Includes Title,Description,Language,SiteLogoUrl
|
|
$wSrl = $web.ServerRelativeUrl.TrimEnd('/')
|
|
$result = @{ settings=@{}; style=@{}; structure=@(); permissions=@(); folderPermissions=@() }
|
|
|
|
if ($Opts.settings) {
|
|
BgLog "Capture des parametres..." "Yellow"
|
|
$result.settings = @{
|
|
title = $web.Title
|
|
description = $web.Description
|
|
language = [int]$web.Language
|
|
}
|
|
BgLog " Titre: $($web.Title) | Langue: $($web.Language)" "Cyan"
|
|
}
|
|
|
|
if ($Opts.style) {
|
|
BgLog "Capture du style..." "Yellow"
|
|
$result.style = @{ logoUrl = $web.SiteLogoUrl }
|
|
BgLog " Logo: $($web.SiteLogoUrl)" "Cyan"
|
|
}
|
|
|
|
if ($Opts.structure) {
|
|
BgLog "Capture de l'arborescence..." "Yellow"
|
|
$lists = Get-PnPList | Where-Object {
|
|
!$_.Hidden -and ($_.BaseType -eq "DocumentLibrary" -or $_.BaseType -eq "GenericList")
|
|
}
|
|
$struct = [System.Collections.Generic.List[object]]::new()
|
|
foreach ($list in $lists) {
|
|
$rf = Get-PnPProperty -ClientObject $list -Property RootFolder
|
|
$srl = $rf.ServerRelativeUrl.Substring($wSrl.Length).TrimStart('/')
|
|
$fld = Collect-Folders $srl
|
|
$struct.Add(@{
|
|
name = $list.Title
|
|
type = $list.BaseType.ToString()
|
|
template = [int]$list.BaseTemplate
|
|
rootSiteRel = $srl # site-relative URL of library root
|
|
folders = @($fld)
|
|
})
|
|
BgLog " [$($list.BaseType)] $($list.Title) ($srl) - $($fld.Count) dossier(s)" "Cyan"
|
|
}
|
|
$result.structure = @($struct)
|
|
}
|
|
|
|
if ($Opts.permissions) {
|
|
BgLog "Capture des groupes site..." "Yellow"
|
|
$groups = Get-PnPSiteGroup
|
|
$permArr = [System.Collections.Generic.List[object]]::new()
|
|
foreach ($g in $groups) {
|
|
try {
|
|
$members = @(Get-PnPGroupMember -Identity $g.LoginName -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Title -ne "System Account" } |
|
|
Select-Object -ExpandProperty LoginName)
|
|
$roles = @(Get-PnPGroupPermissions -Identity $g.LoginName -ErrorAction SilentlyContinue |
|
|
Select-Object -ExpandProperty Name)
|
|
$permArr.Add(@{
|
|
groupName = $g.Title
|
|
loginName = $g.LoginName
|
|
roles = @($roles)
|
|
members = @($members)
|
|
})
|
|
BgLog " Groupe: $($g.Title) - $($members.Count) membre(s)" "Cyan"
|
|
} catch {}
|
|
}
|
|
$result.permissions = @($permArr)
|
|
|
|
# Capture des permissions uniques sur les dossiers
|
|
BgLog "Scan des permissions sur les dossiers..." "Yellow"
|
|
$scanLists = Get-PnPList | Where-Object {
|
|
!$_.Hidden -and ($_.BaseType -eq "DocumentLibrary" -or $_.BaseType -eq "GenericList")
|
|
}
|
|
$folderPerms = [System.Collections.Generic.List[object]]::new()
|
|
$ctx = Get-PnPContext
|
|
foreach ($scanList in $scanLists) {
|
|
$rf = Get-PnPProperty -ClientObject $scanList -Property RootFolder
|
|
$listSrl = $rf.ServerRelativeUrl.Substring($wSrl.Length).TrimStart('/')
|
|
try {
|
|
$items = Get-PnPListItem -List $scanList -PageSize 2000 -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.FileSystemObjectType -eq "Folder" }
|
|
foreach ($folder in $items) {
|
|
try {
|
|
$hasUnique = Get-PnPProperty -ClientObject $folder -Property HasUniqueRoleAssignments
|
|
if (-not $hasUnique) { continue }
|
|
$ra = Get-PnPProperty -ClientObject $folder -Property RoleAssignments
|
|
$folderRoleArr = [System.Collections.Generic.List[object]]::new()
|
|
foreach ($assignment in $ra) {
|
|
Get-PnPProperty -ClientObject $assignment.Member -Property Title,LoginName | Out-Null
|
|
Get-PnPProperty -ClientObject $assignment -Property RoleDefinitionBindings | Out-Null
|
|
$rNames = @($assignment.RoleDefinitionBindings |
|
|
Where-Object { $_.Name -ne "Limited Access" } |
|
|
Select-Object -ExpandProperty Name)
|
|
if ($rNames.Count -gt 0) {
|
|
$folderRoleArr.Add(@{
|
|
principal = $assignment.Member.Title
|
|
loginName = $assignment.Member.LoginName
|
|
roles = @($rNames)
|
|
})
|
|
}
|
|
}
|
|
$fileRef = $folder.FieldValues.FileRef
|
|
$relPath = $fileRef.Substring($wSrl.Length).TrimStart('/')
|
|
$folderPerms.Add(@{
|
|
listSiteRel = $listSrl
|
|
path = $relPath
|
|
perms = @($folderRoleArr)
|
|
})
|
|
BgLog " Perms uniques: $relPath ($($folderRoleArr.Count) entree(s))" "Cyan"
|
|
} catch {}
|
|
}
|
|
} catch { BgLog " Liste ignoree '$($scanList.Title)': $($_.Exception.Message)" "DarkGray" }
|
|
}
|
|
$result.folderPermissions = @($folderPerms)
|
|
BgLog " $($folderPerms.Count) dossier(s) avec permissions uniques captures" "LightGreen"
|
|
}
|
|
|
|
$Sync.Result = $result
|
|
BgLog "=== Capture terminee ! ===" "White"
|
|
} catch {
|
|
$Sync.Error = $_.Exception.Message
|
|
BgLog "Erreur: $($_.Exception.Message)" "Red"
|
|
} finally { $Sync.Done = $true }
|
|
}
|
|
|
|
$sync = [hashtable]::Synchronized(@{
|
|
Queue = [System.Collections.Generic.Queue[object]]::new()
|
|
Done = $false; Error = $null; Result = $null
|
|
})
|
|
$script:_tpl.CapSync = $sync
|
|
|
|
$rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
|
|
$rs.ApartmentState = "STA"; $rs.ThreadOptions = "ReuseThread"; $rs.Open()
|
|
$ps = [System.Management.Automation.PowerShell]::Create()
|
|
$ps.Runspace = $rs
|
|
[void]$ps.AddScript($bgCapture)
|
|
[void]$ps.AddArgument($srcUrl)
|
|
[void]$ps.AddArgument($script:_tpl.ClientId)
|
|
[void]$ps.AddArgument($script:_tpl.CapOpts)
|
|
[void]$ps.AddArgument($sync)
|
|
$script:_tpl.CapRS = $rs
|
|
$script:_tpl.CapPS = $ps
|
|
$script:_tpl.CapHnd = $ps.BeginInvoke()
|
|
|
|
$tmr = New-Object System.Windows.Forms.Timer
|
|
$tmr.Interval = 200
|
|
$script:_tpl.CapTimer = $tmr
|
|
$tmr.Add_Tick({
|
|
while ($script:_tpl.CapSync.Queue.Count -gt 0) {
|
|
$m = $script:_tpl.CapSync.Queue.Dequeue()
|
|
_Tpl-Log -Box $script:_tpl.TxtCapLog -Msg $m.Text -Color $m.Color
|
|
}
|
|
if ($script:_tpl.CapSync.Done) {
|
|
$script:_tpl.CapTimer.Stop(); $script:_tpl.CapTimer.Dispose()
|
|
while ($script:_tpl.CapSync.Queue.Count -gt 0) {
|
|
$m = $script:_tpl.CapSync.Queue.Dequeue()
|
|
_Tpl-Log -Box $script:_tpl.TxtCapLog -Msg $m.Text -Color $m.Color
|
|
}
|
|
try { [void]$script:_tpl.CapPS.EndInvoke($script:_tpl.CapHnd) } catch {}
|
|
try { $script:_tpl.CapRS.Close(); $script:_tpl.CapRS.Dispose() } catch {}
|
|
$btnCapture.Enabled = $true
|
|
if (-not $script:_tpl.CapSync.Error -and $script:_tpl.CapSync.Result) {
|
|
$r = $script:_tpl.CapSync.Result
|
|
$newTpl = [PSCustomObject]@{
|
|
name = $script:_tpl.CapTplName
|
|
sourceUrl = $script:_tpl.CapSrcUrl
|
|
capturedAt = (Get-Date -Format 'dd/MM/yyyy HH:mm')
|
|
options = $script:_tpl.CapOpts
|
|
settings = $r.settings
|
|
style = $r.style
|
|
structure = $r.structure
|
|
permissions = $r.permissions
|
|
folderPermissions = $r.folderPermissions
|
|
}
|
|
$n = $script:_tpl.CapTplName
|
|
$script:_tpl.Templates = @($script:_tpl.Templates | Where-Object { $_.name -ne $n }) + $newTpl
|
|
Save-Templates -Templates $script:_tpl.Templates
|
|
_Tpl-Repopulate
|
|
_Tpl-RefreshCombo
|
|
[System.Windows.Forms.MessageBox]::Show(
|
|
"Template '$n' sauvegarde avec succes.",
|
|
"Capture reussie", "OK", "Information")
|
|
}
|
|
}
|
|
})
|
|
$tmr.Start()
|
|
})
|
|
|
|
# ── Create site button ─────────────────────────────────────────────────
|
|
$btnCreateSite.Add_Click({
|
|
$tplIdx = $cboCrTpl.SelectedIndex
|
|
if ($tplIdx -lt 0 -or $tplIdx -ge $script:_tpl.Templates.Count) {
|
|
[System.Windows.Forms.MessageBox]::Show("Veuillez selectionner un template.", "Aucun template", "OK", "Warning")
|
|
return
|
|
}
|
|
$tpl = $script:_tpl.Templates[$tplIdx]
|
|
$title = $txtCrTitle.Text.Trim()
|
|
$alias = $txtCrAlias.Text.Trim()
|
|
$owners = @($txtCrOwners.Text -split '[,;]' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
|
$members = @($txtCrMembers.Text -split '[,;]' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
|
|
|
if ([string]::IsNullOrWhiteSpace($title)) {
|
|
[System.Windows.Forms.MessageBox]::Show("Veuillez saisir le titre du site.", "Champ manquant", "OK", "Warning"); return
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($alias)) {
|
|
[System.Windows.Forms.MessageBox]::Show("Veuillez saisir un alias URL.", "Champ manquant", "OK", "Warning"); return
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($script:_tpl.TenantUrl)) {
|
|
[System.Windows.Forms.MessageBox]::Show("Tenant URL manquant dans le formulaire principal.", "Erreur", "OK", "Warning"); return
|
|
}
|
|
if ($owners.Count -eq 0) {
|
|
[System.Windows.Forms.MessageBox]::Show("Veuillez saisir au moins un proprietaire.", "Champ manquant", "OK", "Warning"); return
|
|
}
|
|
|
|
$applyOpts = @{
|
|
structure = $chkApplyStruct.Checked -and $tpl.options.structure
|
|
permissions = $chkApplyPerms.Checked -and $tpl.options.permissions
|
|
settings = $chkApplySettings.Checked -and $tpl.options.settings
|
|
style = $chkApplyStyle.Checked -and $tpl.options.style
|
|
}
|
|
$isTeam = $radCrTeam.Checked
|
|
|
|
$btnCreateSite.Enabled = $false
|
|
$script:_tpl.TxtCreateLog.Clear()
|
|
|
|
$bgCreate = {
|
|
param($TenantUrl, $ClientId, $Title, $Alias, $IsTeam, $Owners, $Members, $ApplyOpts, $Tpl, $Sync)
|
|
function BgLog([string]$m, [string]$c="LightGreen") {
|
|
$Sync.Queue.Enqueue([PSCustomObject]@{ Text=$m; Color=$c })
|
|
}
|
|
function Apply-FolderTree([object[]]$Folders, [string]$ParentSrl) {
|
|
foreach ($f in $Folders) {
|
|
try {
|
|
Add-PnPFolder -Name $f.name -Folder $ParentSrl -ErrorAction SilentlyContinue | Out-Null
|
|
BgLog " + $($f.name)" "Cyan"
|
|
} catch {}
|
|
if ($f.subfolders -and $f.subfolders.Count -gt 0) {
|
|
Apply-FolderTree $f.subfolders "$ParentSrl/$($f.name)"
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
Import-Module PnP.PowerShell -ErrorAction Stop
|
|
$adminUrl = if ($TenantUrl -match '^(https?://[^.]+)(\.sharepoint\.com.*)') {
|
|
"$($Matches[1])-admin$($Matches[2])"
|
|
} else { $TenantUrl }
|
|
|
|
BgLog "Connexion au tenant admin..." "Yellow"
|
|
Connect-PnPOnline -Url $adminUrl -Interactive -ClientId $ClientId
|
|
|
|
BgLog "Creation du site '$Title' (alias: $Alias)..." "Yellow"
|
|
$newUrl = if ($IsTeam) {
|
|
New-PnPSite -Type TeamSite -Title $Title -Alias $Alias -Owners $Owners -Wait
|
|
} else {
|
|
$base = if ($TenantUrl -match '^(https?://[^.]+\.sharepoint\.com)') { $Matches[1] } else { $TenantUrl }
|
|
New-PnPSite -Type CommunicationSite -Title $Title -Url "$base/sites/$Alias" -Wait
|
|
}
|
|
$Sync.NewSiteUrl = $newUrl
|
|
BgLog "Site cree : $newUrl" "LightGreen"
|
|
|
|
BgLog "Connexion au nouveau site..." "Yellow"
|
|
Connect-PnPOnline -Url $newUrl -Interactive -ClientId $ClientId
|
|
$web = Get-PnPWeb
|
|
$wSrl = $web.ServerRelativeUrl.TrimEnd('/')
|
|
|
|
if ($ApplyOpts.settings -and $Tpl.settings -and $Tpl.settings.description) {
|
|
BgLog "Application des parametres..." "Yellow"
|
|
Set-PnPWeb -Description $Tpl.settings.description
|
|
}
|
|
|
|
if ($ApplyOpts.style -and $Tpl.style -and $Tpl.style.logoUrl) {
|
|
BgLog "Application du style (logo)..." "Yellow"
|
|
Set-PnPWeb -SiteLogoUrl $Tpl.style.logoUrl
|
|
}
|
|
|
|
if ($ApplyOpts.structure -and $Tpl.structure -and $Tpl.structure.Count -gt 0) {
|
|
BgLog "Application de l'arborescence..." "Yellow"
|
|
foreach ($lib in $Tpl.structure) {
|
|
BgLog " Bibliotheque: $($lib.name)" "Yellow"
|
|
$existing = Get-PnPList -Identity $lib.name -ErrorAction SilentlyContinue
|
|
if (-not $existing) {
|
|
try {
|
|
$tplType = if ($lib.template -eq 101 -or $lib.type -eq "DocumentLibrary") {
|
|
[Microsoft.SharePoint.Client.ListTemplateType]::DocumentLibrary
|
|
} else {
|
|
[Microsoft.SharePoint.Client.ListTemplateType]::GenericList
|
|
}
|
|
New-PnPList -Title $lib.name -Template $tplType | Out-Null
|
|
BgLog " Creee." "Cyan"
|
|
} catch { BgLog " Ignoree: $($_.Exception.Message)" "DarkGray" }
|
|
} else { BgLog " Deja existante." "DarkGray" }
|
|
|
|
if ($lib.folders -and $lib.folders.Count -gt 0) {
|
|
# Get actual root folder URL from target list (avoids display-name vs URL mismatch)
|
|
$targetList = Get-PnPList -Identity $lib.name -ErrorAction SilentlyContinue
|
|
if ($targetList) {
|
|
$listRf = Get-PnPProperty -ClientObject $targetList -Property RootFolder
|
|
$libBase = $listRf.ServerRelativeUrl.TrimEnd('/')
|
|
BgLog " Base URL: $libBase" "DarkGray"
|
|
Apply-FolderTree $lib.folders $libBase
|
|
} else {
|
|
BgLog " Liste '$($lib.name)' non trouvee, dossiers ignores." "DarkGray"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($Members -and $Members.Count -gt 0) {
|
|
BgLog "Ajout de $($Members.Count) membre(s)..." "Yellow"
|
|
$memberGroup = Get-PnPGroup | Where-Object { $_.Title -like "*Membres*" -or $_.Title -like "*Members*" } | Select-Object -First 1
|
|
if ($memberGroup) {
|
|
foreach ($m in $Members) {
|
|
try {
|
|
Add-PnPGroupMember -LoginName $m -Group $memberGroup.Title -ErrorAction SilentlyContinue
|
|
BgLog " + $m" "Cyan"
|
|
} catch { BgLog " Ignore $m" "DarkGray" }
|
|
}
|
|
} else { BgLog " Groupe membres non trouve, ajout ignore." "DarkGray" }
|
|
}
|
|
|
|
# Apply folder-level unique permissions
|
|
$fpList = $Tpl.folderPermissions
|
|
if ($ApplyOpts.permissions -and $fpList -and $fpList.Count -gt 0) {
|
|
BgLog "Application des permissions sur les dossiers ($($fpList.Count))..." "Yellow"
|
|
|
|
# Build map: source library rootSiteRel -> target library server-relative URL
|
|
$libMap = @{}
|
|
foreach ($lib in $Tpl.structure) {
|
|
if (-not $lib.rootSiteRel) { continue }
|
|
$tgtLib = Get-PnPList -Identity $lib.name -ErrorAction SilentlyContinue
|
|
if ($tgtLib) {
|
|
$tgtRf = Get-PnPProperty -ClientObject $tgtLib -Property RootFolder
|
|
$libMap[$lib.rootSiteRel] = $tgtRf.ServerRelativeUrl.TrimEnd('/')
|
|
}
|
|
}
|
|
|
|
$ctx = Get-PnPContext
|
|
foreach ($fp in $fpList) {
|
|
# Compute folder SRL on target using the library map
|
|
$srcLibSrl = $fp.listSiteRel
|
|
$tgtLibBase = $libMap[$srcLibSrl]
|
|
if (-not $tgtLibBase) {
|
|
BgLog " Bibliotheque non mappee pour '$srcLibSrl', dossier ignore." "DarkGray"
|
|
continue
|
|
}
|
|
# Folder path relative to library root (strip the lib prefix from fp.path)
|
|
$folderRelToLib = $fp.path.Substring($srcLibSrl.Length).TrimStart('/')
|
|
$folderSrl = "$tgtLibBase/$folderRelToLib"
|
|
|
|
try {
|
|
$folder = $ctx.Web.GetFolderByServerRelativeUrl($folderSrl)
|
|
$folderItem = $folder.ListItemAllFields
|
|
$ctx.Load($folderItem)
|
|
$ctx.ExecuteQuery()
|
|
|
|
# Break inheritance and clear all inherited permissions
|
|
$folderItem.BreakRoleInheritance($false, $false)
|
|
$ctx.ExecuteQuery()
|
|
|
|
# Apply each captured permission entry
|
|
foreach ($perm in $fp.perms) {
|
|
# Skip system accounts and source-specific SP groups (detected by failed EnsureUser)
|
|
try {
|
|
$principal = $ctx.Web.EnsureUser($perm.loginName)
|
|
$ctx.Load($principal)
|
|
$ctx.ExecuteQuery()
|
|
foreach ($roleName in $perm.roles) {
|
|
try {
|
|
$roleDef = $ctx.Web.RoleDefinitions.GetByName($roleName)
|
|
$bindings = New-Object Microsoft.SharePoint.Client.RoleDefinitionBindingCollection($ctx)
|
|
$bindings.Add($roleDef)
|
|
$folderItem.RoleAssignments.Add($principal, $bindings) | Out-Null
|
|
$ctx.ExecuteQuery()
|
|
BgLog " + $($perm.principal) [$roleName]" "Cyan"
|
|
} catch { BgLog " Role '$roleName' ignore pour '$($perm.principal)'" "DarkGray" }
|
|
}
|
|
} catch {
|
|
BgLog " Principal ignore (groupe source ou compte systeme) : '$($perm.principal)'" "DarkGray"
|
|
}
|
|
}
|
|
BgLog " OK: $folderRelToLib" "Cyan"
|
|
} catch { BgLog " Dossier ignore '$folderRelToLib': $($_.Exception.Message)" "DarkGray" }
|
|
}
|
|
}
|
|
|
|
BgLog "=== Site cree avec succes ! ===" "White"
|
|
} catch {
|
|
$Sync.Error = $_.Exception.Message
|
|
BgLog "Erreur: $($_.Exception.Message)" "Red"
|
|
} finally { $Sync.Done = $true }
|
|
}
|
|
|
|
$sync = [hashtable]::Synchronized(@{
|
|
Queue = [System.Collections.Generic.Queue[object]]::new()
|
|
Done = $false; Error = $null; NewSiteUrl = $null
|
|
})
|
|
$script:_tpl.CrSync = $sync
|
|
|
|
$rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
|
|
$rs.ApartmentState = "STA"; $rs.ThreadOptions = "ReuseThread"; $rs.Open()
|
|
$ps = [System.Management.Automation.PowerShell]::Create()
|
|
$ps.Runspace = $rs
|
|
[void]$ps.AddScript($bgCreate)
|
|
[void]$ps.AddArgument($script:_tpl.TenantUrl)
|
|
[void]$ps.AddArgument($script:_tpl.ClientId)
|
|
[void]$ps.AddArgument($title)
|
|
[void]$ps.AddArgument($alias)
|
|
[void]$ps.AddArgument($isTeam)
|
|
[void]$ps.AddArgument($owners)
|
|
[void]$ps.AddArgument($members)
|
|
[void]$ps.AddArgument($applyOpts)
|
|
[void]$ps.AddArgument($tpl)
|
|
[void]$ps.AddArgument($sync)
|
|
$script:_tpl.CrRS = $rs
|
|
$script:_tpl.CrPS = $ps
|
|
$script:_tpl.CrHnd = $ps.BeginInvoke()
|
|
|
|
$tmr = New-Object System.Windows.Forms.Timer
|
|
$tmr.Interval = 200
|
|
$script:_tpl.CrTimer = $tmr
|
|
$tmr.Add_Tick({
|
|
while ($script:_tpl.CrSync.Queue.Count -gt 0) {
|
|
$m = $script:_tpl.CrSync.Queue.Dequeue()
|
|
_Tpl-Log -Box $script:_tpl.TxtCreateLog -Msg $m.Text -Color $m.Color
|
|
}
|
|
if ($script:_tpl.CrSync.Done) {
|
|
$script:_tpl.CrTimer.Stop(); $script:_tpl.CrTimer.Dispose()
|
|
while ($script:_tpl.CrSync.Queue.Count -gt 0) {
|
|
$m = $script:_tpl.CrSync.Queue.Dequeue()
|
|
_Tpl-Log -Box $script:_tpl.TxtCreateLog -Msg $m.Text -Color $m.Color
|
|
}
|
|
try { [void]$script:_tpl.CrPS.EndInvoke($script:_tpl.CrHnd) } catch {}
|
|
try { $script:_tpl.CrRS.Close(); $script:_tpl.CrRS.Dispose() } catch {}
|
|
$btnCreateSite.Enabled = $true
|
|
if ($script:_tpl.CrSync.NewSiteUrl -and -not $script:_tpl.CrSync.Error) {
|
|
$url = $script:_tpl.CrSync.NewSiteUrl
|
|
$res = [System.Windows.Forms.MessageBox]::Show(
|
|
"Site cree avec succes !`n`n$url`n`nOuvrir dans le navigateur ?",
|
|
"Succes", "YesNo", "Information")
|
|
if ($res -eq "Yes") { Start-Process $url }
|
|
}
|
|
}
|
|
})
|
|
$tmr.Start()
|
|
})
|
|
|
|
if ($Owner) { [void]$dlg.ShowDialog($Owner) } else { [void]$dlg.ShowDialog() }
|
|
$dlg.Dispose()
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ===== HTML Export: Permissions =====
|
|
|
|
function Merge-PermissionRows([array]$Data) {
|
|
# Groups rows that share the same Users + Permissions + GrantedThrough into one merged row.
|
|
# Uses [ordered] to preserve insertion order without a separate list.
|
|
$map = [ordered]@{}
|
|
foreach ($row in $Data) {
|
|
$key = "$($row.Users)|$($row.Permissions)|$($row.GrantedThrough)"
|
|
if (-not $map.Contains($key)) {
|
|
$map[$key] = [PSCustomObject]@{
|
|
Locations = @()
|
|
Permissions = $row.Permissions
|
|
GrantedThrough = $row.GrantedThrough
|
|
Type = $row.Type
|
|
Users = $row.Users
|
|
UserLogins = if ($row.UserLogins) { $row.UserLogins } else { "" }
|
|
}
|
|
}
|
|
$map[$key].Locations += [PSCustomObject]@{
|
|
Object = [string]$row.Object
|
|
Title = [string]$row.Title
|
|
URL = if ($row.URL) { [string]$row.URL } else { "" }
|
|
HasUniquePermissions = $row.HasUniquePermissions
|
|
}
|
|
}
|
|
return @($map.Values)
|
|
}
|
|
|
|
function Export-PermissionsToHTML {
|
|
param([array]$Data, [string]$SiteTitle, [string]$SiteURL, [string]$OutputPath)
|
|
|
|
$generated = Get-Date -Format 'dd/MM/yyyy HH:mm'
|
|
$count = $Data.Count
|
|
$uniqueCount = ($Data | Where-Object { $_.HasUniquePermissions -eq 'TRUE' -or $_.HasUniquePermissions -eq $true } | Measure-Object).Count
|
|
$userCount = ($Data | ForEach-Object { $_.Users -split '; ' } | Where-Object { $_ } | Sort-Object -Unique | Measure-Object).Count
|
|
|
|
# Build pills HTML for a list of names + emails
|
|
function Build-Pills([string[]]$Names, [string[]]$Emails) {
|
|
$html = ""
|
|
for ($i = 0; $i -lt $Names.Count; $i++) {
|
|
$n = EscHtml $Names[$i]
|
|
$e = if ($Emails -and $i -lt $Emails.Count) { EscHtml $Emails[$i] } else { "" }
|
|
$html += "<span class='pill' data-email='$e'>$n</span>"
|
|
}
|
|
return $html
|
|
}
|
|
|
|
$mergedRows = Merge-PermissionRows $Data
|
|
$script:grpIdx = 0
|
|
$rows = ""
|
|
foreach ($mrow in $mergedRows) {
|
|
$locs = @($mrow.Locations)
|
|
|
|
# Dominant type badge (use first location's type - entries in a merged group are typically the same type)
|
|
$dominantType = $locs[0].Object
|
|
$badgeClass = switch -Regex ($dominantType) {
|
|
"Site Collection" { "bc"; break }
|
|
"^Site$" { "bs"; break }
|
|
"Folder" { "bf"; break }
|
|
Default { "bl" }
|
|
}
|
|
|
|
# Name / locations cell + Unique Permissions cell
|
|
if ($locs.Count -eq 1) {
|
|
$loc = $locs[0]
|
|
$isUnique = ($loc.HasUniquePermissions -eq 'TRUE' -or $loc.HasUniquePermissions -eq $true)
|
|
$uqClass = if ($isUnique) { "uq" } else { "inh" }
|
|
$uqText = if ($isUnique) { "✓ Unique" } else { "Inherited" }
|
|
$nameCell = if ($loc.URL) { "<a href='$(EscHtml $loc.URL)'>$(EscHtml $loc.Title)</a>" } else { EscHtml $loc.Title }
|
|
$uqCell = "<span class='$uqClass'>$uqText</span>"
|
|
} else {
|
|
$uqTotal = ($locs | Where-Object { $_.HasUniquePermissions -eq 'TRUE' -or $_.HasUniquePermissions -eq $true }).Count
|
|
$locHtml = "<div class='locs'>"
|
|
foreach ($loc in $locs) {
|
|
$isUnique = ($loc.HasUniquePermissions -eq 'TRUE' -or $loc.HasUniquePermissions -eq $true)
|
|
$uqMark = if ($isUnique) { "<span class='uq-sm'>✓</span>" } else { "<span class='inh-sm'>~</span>" }
|
|
$locBc = switch -Regex ($loc.Object) {
|
|
"Site Collection" { "bc"; break }
|
|
"^Site$" { "bs"; break }
|
|
"Folder" { "bf"; break }
|
|
Default { "bl" }
|
|
}
|
|
$locLink = if ($loc.URL) { "<a href='$(EscHtml $loc.URL)'>$(EscHtml $loc.Title)</a>" } else { EscHtml $loc.Title }
|
|
$locHtml += "<div class='loc'><span class='b $locBc'>$(EscHtml $loc.Object)</span> $locLink $uqMark</div>"
|
|
}
|
|
$locHtml += "</div>"
|
|
$nameCell = $locHtml
|
|
$uqCell = "<span class='loc-count'>$($locs.Count) emplacements<br><span class='inh-sm'>($uqTotal uniques)</span></span>"
|
|
}
|
|
|
|
# Build Users / Members cell
|
|
$names = if ($mrow.Users) { @($mrow.Users -split '; ') } else { @() }
|
|
$emails = if ($mrow.UserLogins) { @($mrow.UserLogins -split '; ') } else { @() }
|
|
$usersCell = ""
|
|
if ($names.Count -gt 0) {
|
|
$pills = Build-Pills $names $emails
|
|
if ($mrow.GrantedThrough -match '^SharePoint Group: (.+)$') {
|
|
$grpName = EscHtml $Matches[1]
|
|
$gid = "g$($script:grpIdx)"
|
|
$script:grpIdx++
|
|
$usersCell = "<div class='grp-wrap'><span class='grp-tog' onclick='toggleGrp(this,""$gid"")'>$grpName</span><div class='grp-members' id='$gid'>$pills</div></div>"
|
|
} else {
|
|
$usersCell = $pills
|
|
}
|
|
}
|
|
|
|
$rows += "<tr><td><span class='b $badgeClass'>$(EscHtml $dominantType)</span></td>"
|
|
$rows += "<td>$nameCell</td>"
|
|
$rows += "<td>$usersCell</td>"
|
|
$rows += "<td>$(EscHtml $mrow.Permissions)</td>"
|
|
$rows += "<td>$(EscHtml $mrow.GrantedThrough)</td>"
|
|
$rows += "<td>$uqCell</td></tr>`n"
|
|
}
|
|
|
|
$html = @"
|
|
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Permissions - $(EscHtml $SiteTitle)</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:'Segoe UI',Arial,sans-serif;background:#f0f2f5;color:#333;padding:30px}
|
|
h1{font-size:21px;font-weight:600;margin-bottom:6px}
|
|
.hdr{background:#0078d4;color:#fff;padding:22px 28px;border-radius:10px;margin-bottom:22px}
|
|
.hdr .sub{font-size:13px;opacity:.85;margin-top:4px}
|
|
.hdr a{color:#cce4ff}
|
|
.cards{display:flex;gap:14px;margin-bottom:22px}
|
|
.card{background:#fff;border-radius:8px;padding:16px 20px;flex:1;box-shadow:0 1px 4px rgba(0,0,0,.08);text-align:center}
|
|
.card .v{font-size:26px;font-weight:700;color:#0078d4}
|
|
.card .l{font-size:12px;color:#888;margin-top:3px}
|
|
.wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.08);overflow:hidden}
|
|
table{border-collapse:collapse;width:100%}
|
|
th{background:#0078d4;color:#fff;padding:11px 14px;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap}
|
|
td{padding:9px 14px;font-size:13px;border-bottom:1px solid #f0f0f0;vertical-align:top}
|
|
tr:last-child td{border-bottom:none}
|
|
tr:hover td{background:#f5f9ff}
|
|
a{color:#0078d4;text-decoration:none}
|
|
a:hover{text-decoration:underline}
|
|
.b{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600;white-space:nowrap}
|
|
.bc{background:#f3e5f5;color:#7b1fa2}
|
|
.bs{background:#e3f2fd;color:#1565c0}
|
|
.bl{background:#e8f5e9;color:#2e7d32}
|
|
.bf{background:#fff3e0;color:#e65100}
|
|
.uq{color:#c62828;font-weight:600}
|
|
.inh{color:#999}
|
|
.foot{margin-top:18px;text-align:center;font-size:12px;color:#bbb}
|
|
/* Multi-location rows */
|
|
.locs{display:flex;flex-direction:column;gap:4px}
|
|
.loc{display:flex;align-items:center;gap:5px;flex-wrap:wrap}
|
|
.uq-sm{color:#c62828;font-weight:700;font-size:11px}
|
|
.inh-sm{color:#bbb;font-size:11px}
|
|
.loc-count{color:#555;font-size:12px;line-height:1.6}
|
|
/* Pills */
|
|
.pill{display:inline-block;background:#e3f2fd;color:#1565c0;border-radius:20px;padding:2px 10px;font-size:12px;margin:2px 3px;cursor:default;user-select:none;white-space:nowrap}
|
|
.pill:hover{background:#bbdefb}
|
|
/* SP Group collapsible */
|
|
.grp-wrap{display:inline-block}
|
|
.grp-tog{cursor:pointer;font-weight:600;color:#0078d4;user-select:none;white-space:nowrap}
|
|
.grp-tog::before{content:'\25B6 ';font-size:10px}
|
|
.grp-tog.open::before{content:'\25BC '}
|
|
.grp-members{display:none;margin-top:6px}
|
|
.grp-members.open{display:block}
|
|
/* Context menu */
|
|
.cmenu{position:fixed;background:#fff;border:1px solid #ddd;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.15);z-index:9999;min-width:190px;font-size:13px;display:none}
|
|
.cmenu.show{display:block}
|
|
.cmenu-item{padding:9px 16px;cursor:pointer;display:flex;align-items:center;gap:8px}
|
|
.cmenu-item:hover{background:#f0f7ff}
|
|
.cmenu-sep{border-top:1px solid #eee;margin:3px 0}
|
|
</style></head><body>
|
|
<div class="hdr">
|
|
<h1>SharePoint Permissions Report</h1>
|
|
<div class="sub">Site: <a href="$(EscHtml $SiteURL)">$(EscHtml $SiteTitle)</a> • Generated: $generated</div>
|
|
</div>
|
|
<div class="cards">
|
|
<div class="card"><div class="v">$count</div><div class="l">Total Entries</div></div>
|
|
<div class="card"><div class="v">$uniqueCount</div><div class="l">Unique Permission Sets</div></div>
|
|
<div class="card"><div class="v">$userCount</div><div class="l">Distinct Users / Groups</div></div>
|
|
</div>
|
|
<div class="wrap"><table>
|
|
<thead><tr><th>Type</th><th>Name</th><th>Users / Members</th><th>Permission Level</th><th>Granted Through</th><th>Unique Permissions</th></tr></thead>
|
|
<tbody>
|
|
$rows
|
|
</tbody></table></div>
|
|
<div class="foot">Generated by SharePoint Toolbox</div>
|
|
|
|
<!-- Context menu -->
|
|
<div class="cmenu" id="cmenu">
|
|
<div class="cmenu-item" id="cmenu-copy">📋 Copier l'adresse email</div>
|
|
<div class="cmenu-sep"></div>
|
|
<div class="cmenu-item" id="cmenu-mailto">✉ Envoyer un email</div>
|
|
</div>
|
|
|
|
<script>
|
|
function toggleGrp(tog, id) {
|
|
tog.classList.toggle('open');
|
|
document.getElementById(id).classList.toggle('open');
|
|
}
|
|
|
|
var cmenu = document.getElementById('cmenu');
|
|
var cmenuEmail = '';
|
|
|
|
document.addEventListener('contextmenu', function(e) {
|
|
var pill = e.target.closest ? e.target.closest('.pill') : null;
|
|
if (!pill) { cmenu.classList.remove('show'); return; }
|
|
e.preventDefault();
|
|
cmenuEmail = pill.dataset.email || '';
|
|
var x = e.clientX, y = e.clientY;
|
|
cmenu.style.left = x + 'px'; cmenu.style.top = y + 'px';
|
|
cmenu.classList.add('show');
|
|
// Adjust if off-screen
|
|
var r = cmenu.getBoundingClientRect();
|
|
if (r.right > window.innerWidth) cmenu.style.left = (x - r.width) + 'px';
|
|
if (r.bottom > window.innerHeight) cmenu.style.top = (y - r.height) + 'px';
|
|
});
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (!e.target.closest('#cmenu')) cmenu.classList.remove('show');
|
|
});
|
|
|
|
document.getElementById('cmenu-copy').addEventListener('click', function() {
|
|
if (!cmenuEmail) return;
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(cmenuEmail).catch(function() { fallbackCopy(cmenuEmail); });
|
|
} else { fallbackCopy(cmenuEmail); }
|
|
cmenu.classList.remove('show');
|
|
});
|
|
|
|
document.getElementById('cmenu-mailto').addEventListener('click', function() {
|
|
if (cmenuEmail) window.location.href = 'mailto:' + cmenuEmail;
|
|
cmenu.classList.remove('show');
|
|
});
|
|
|
|
function fallbackCopy(text) {
|
|
var ta = document.createElement('textarea');
|
|
ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
|
|
document.body.appendChild(ta); ta.select();
|
|
try { document.execCommand('copy'); } catch(e) {}
|
|
document.body.removeChild(ta);
|
|
}
|
|
</script>
|
|
</body></html>
|
|
"@
|
|
$html | Out-File -FilePath $OutputPath -Encoding UTF8
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ===== HTML Export: Storage =====
|
|
|
|
function Export-StorageToHTML {
|
|
param([array]$Data, [string]$SiteTitle, [string]$SiteURL, [string]$OutputPath)
|
|
|
|
$generated = Get-Date -Format 'dd/MM/yyyy HH:mm'
|
|
$totalBytes = ($Data | Measure-Object -Property SizeBytes -Sum).Sum
|
|
$totalVersionBytes = ($Data | Where-Object { $_.VersionSizeBytes } | Measure-Object -Property VersionSizeBytes -Sum).Sum
|
|
if (-not $totalVersionBytes) { $totalVersionBytes = 0 }
|
|
$totalFiles = ($Data | Measure-Object -Property ItemCount -Sum).Sum
|
|
$libCount = $Data.Count
|
|
|
|
# Shared toggle-ID counter - must be unique across all levels of nesting
|
|
$script:togIdx = 0
|
|
|
|
# Recursively builds folder rows; each folder with children gets its own toggle
|
|
function Build-FolderRows([array]$Folders) {
|
|
$html = ""
|
|
foreach ($sf in ($Folders | Sort-Object SizeBytes -Descending)) {
|
|
$myIdx = $script:togIdx++
|
|
$hasSubs = $sf.SubFolders -and $sf.SubFolders.Count -gt 0
|
|
$sfSzClass = if ($sf.SizeBytes -ge 10GB) { "sz-lg" } elseif ($sf.SizeBytes -ge 1GB) { "sz-md" } else { "sz-sm" }
|
|
$sfLink = if ($sf.URL) { "<a href='$(EscHtml $sf.URL)'>📁 $(EscHtml $sf.Name)</a>" } else { "📁 $(EscHtml $sf.Name)" }
|
|
$togBtn = if ($hasSubs) { "<button class='tog' onclick='toggle($myIdx)' title='$($sf.SubFolders.Count) subfolder(s)'>▶</button>" } else { "<span class='tog-ph'></span>" }
|
|
|
|
$sfVerBytes = if ($sf.VersionSizeBytes) { $sf.VersionSizeBytes } else { 0 }
|
|
$sfVerPct = if ($sf.SizeBytes -gt 0 -and $sfVerBytes -gt 0) { [math]::Round($sfVerBytes / $sf.SizeBytes * 100, 0) } else { 0 }
|
|
$sfVerClass = if ($sfVerPct -ge 40) { "sz-lg" } elseif ($sfVerPct -ge 15) { "sz-md" } else { "" }
|
|
$sfVerTxt = if ($sfVerBytes -gt 0) { "<span class='$sfVerClass'>$(Format-Bytes $sfVerBytes)</span> <span class='pct'>($sfVerPct%)</span>" } else { "<span style='color:#bbb'>-</span>" }
|
|
$html += "<tr><td><div class='lib-name'>$togBtn $sfLink</div></td>"
|
|
$html += "<td class='num'>$($sf.ItemCount)</td>"
|
|
$html += "<td class='num $sfSzClass'>$(Format-Bytes $sf.SizeBytes)</td>"
|
|
$html += "<td class='num'>$sfVerTxt</td>"
|
|
$html += "<td class='num'>$(EscHtml $sf.LastModified)</td></tr>`n"
|
|
|
|
if ($hasSubs) {
|
|
$html += "<tr id='sf-$myIdx' class='sf-row'><td colspan='5'><div class='sf-wrap'>"
|
|
$html += "<table class='sf-tbl'><thead><tr><th>Folder</th><th style='text-align:right'>Files</th><th style='text-align:right'>Size</th><th style='text-align:right'>Versions</th><th style='text-align:right'>Last Modified</th></tr></thead>"
|
|
$html += "<tbody>$(Build-FolderRows $sf.SubFolders)</tbody></table></div></td></tr>`n"
|
|
}
|
|
}
|
|
return $html
|
|
}
|
|
|
|
$rows = ""
|
|
foreach ($row in ($Data | Sort-Object SizeBytes -Descending)) {
|
|
$myLibIdx = $script:togIdx++
|
|
$pct = if ($totalBytes -gt 0) { [math]::Round($row.SizeBytes / $totalBytes * 100, 1) } else { 0 }
|
|
$szClass = if ($row.SizeBytes -ge 10GB) { "sz-lg" } elseif ($row.SizeBytes -ge 1GB) { "sz-md" } else { "sz-sm" }
|
|
$hasFolders = $row.SubFolders -and $row.SubFolders.Count -gt 0
|
|
$toggleBtn = if ($hasFolders) { "<button class='tog' onclick='toggle($myLibIdx)' title='$($row.SubFolders.Count) subfolder(s)'>▶</button>" } else { "<span class='tog-ph'></span>" }
|
|
$nameCell = if ($row.LibraryURL) { "<a href='$(EscHtml $row.LibraryURL)'>$(EscHtml $row.Library)</a>" } else { EscHtml $row.Library }
|
|
|
|
$verBytes = if ($row.VersionSizeBytes) { $row.VersionSizeBytes } else { 0 }
|
|
$verPct = if ($row.SizeBytes -gt 0 -and $verBytes -gt 0) { [math]::Round($verBytes / $row.SizeBytes * 100, 0) } else { 0 }
|
|
$verClass = if ($verPct -ge 40) { "sz-lg" } elseif ($verPct -ge 15) { "sz-md" } else { "" }
|
|
$verTxt = if ($verBytes -gt 0) { "<span class='$verClass'>$(Format-Bytes $verBytes)</span> <span class='pct'>($verPct%)</span>" } else { "<span style='color:#bbb'>-</span>" }
|
|
$rows += "<tr><td><div class='lib-name'>$toggleBtn $nameCell</div></td>"
|
|
$rows += "<td>$(EscHtml $row.SiteTitle)</td>"
|
|
$rows += "<td class='num'>$($row.ItemCount)</td>"
|
|
$rows += "<td class='num $szClass'>$(Format-Bytes $row.SizeBytes)</td>"
|
|
$rows += "<td class='num'>$verTxt</td>"
|
|
$rows += "<td><div class='bw'><div class='br' style='width:$pct%'></div><span class='pct'>$pct%</span></div></td>"
|
|
$rows += "<td class='num'>$(EscHtml $row.LastModified)</td></tr>`n"
|
|
|
|
if ($hasFolders) {
|
|
$rows += "<tr id='sf-$myLibIdx' class='sf-row'><td colspan='7'><div class='sf-wrap'>"
|
|
$rows += "<table class='sf-tbl'><thead><tr><th>Folder</th><th style='text-align:right'>Files</th><th style='text-align:right'>Size</th><th style='text-align:right'>Versions</th><th style='text-align:right'>Last Modified</th></tr></thead>"
|
|
$rows += "<tbody>$(Build-FolderRows $row.SubFolders)</tbody></table></div></td></tr>`n"
|
|
}
|
|
}
|
|
|
|
$html = @"
|
|
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Storage - $(EscHtml $SiteTitle)</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:'Segoe UI',Arial,sans-serif;background:#f0f2f5;color:#333;padding:30px}
|
|
h1{font-size:21px;font-weight:600;margin-bottom:6px}
|
|
.hdr{background:linear-gradient(135deg,#107c10,#0a5e0a);color:#fff;padding:22px 28px;border-radius:10px;margin-bottom:22px}
|
|
.hdr .sub{font-size:13px;opacity:.85;margin-top:4px}
|
|
.hdr a{color:#b8f0b8}
|
|
.cards{display:flex;gap:14px;margin-bottom:22px}
|
|
.card{background:#fff;border-radius:8px;padding:16px 20px;flex:1;box-shadow:0 1px 4px rgba(0,0,0,.08);text-align:center}
|
|
.card .v{font-size:26px;font-weight:700;color:#107c10}
|
|
.card .l{font-size:12px;color:#888;margin-top:3px}
|
|
.wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.08);overflow:hidden}
|
|
table{border-collapse:collapse;width:100%}
|
|
th{background:#107c10;color:#fff;padding:11px 14px;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap}
|
|
td{padding:9px 14px;font-size:13px;border-bottom:1px solid #f0f0f0;vertical-align:middle}
|
|
tr:last-child td{border-bottom:none}
|
|
tr:hover td{background:#f5fdf5}
|
|
a{color:#107c10;text-decoration:none}
|
|
a:hover{text-decoration:underline}
|
|
.num{text-align:right;font-variant-numeric:tabular-nums}
|
|
.sz-lg{color:#c62828;font-weight:700}
|
|
.sz-md{color:#e65100;font-weight:600}
|
|
.sz-sm{color:#2e7d32}
|
|
.bw{display:flex;align-items:center;gap:8px;min-width:160px}
|
|
.br{height:10px;background:#107c10;border-radius:5px;min-width:2px}
|
|
.pct{font-size:12px;color:#666;white-space:nowrap}
|
|
.lib-name{display:flex;align-items:center;gap:6px}
|
|
.tog{background:none;border:none;cursor:pointer;color:#107c10;font-size:11px;padding:2px 4px;border-radius:3px;transition:transform .2s;flex-shrink:0;line-height:1}
|
|
.tog:hover{background:#e8f5e9}
|
|
.tog.open{transform:rotate(90deg)}
|
|
.tog-ph{display:inline-block;width:22px;flex-shrink:0}
|
|
.sf-row{display:none}
|
|
.sf-row>td{padding:0;background:#f3faf3;border-bottom:2px solid #c8e6c9}
|
|
.sf-wrap{padding:10px 16px 10px 46px}
|
|
.sf-tbl{border-collapse:collapse;width:100%}
|
|
.sf-tbl th{background:#d4edda;color:#1b5e20;padding:7px 12px;font-size:11px;text-transform:uppercase;letter-spacing:.4px}
|
|
.sf-tbl td{padding:7px 12px;font-size:12px;border-bottom:1px solid #e8f5e9;color:#444}
|
|
.sf-tbl tr:last-child td{border-bottom:none}
|
|
.sf-tbl tr:hover td{background:#eaf7ea}
|
|
.sf-tbl a{color:#2e7d32}
|
|
.foot{margin-top:18px;text-align:center;font-size:12px;color:#bbb}
|
|
</style>
|
|
<script>
|
|
function toggle(i){
|
|
var row=document.getElementById('sf-'+i);
|
|
var btn=row.previousElementSibling.querySelector('.tog');
|
|
var visible=window.getComputedStyle(row).display!=='none';
|
|
row.style.display=visible?'none':'table-row';
|
|
btn.classList.toggle('open',!visible);
|
|
}
|
|
</script>
|
|
</head><body>
|
|
<div class="hdr">
|
|
<h1>SharePoint Storage Metrics</h1>
|
|
<div class="sub">Site: <a href="$(EscHtml $SiteURL)">$(EscHtml $SiteTitle)</a> • Generated: $generated</div>
|
|
</div>
|
|
<div class="cards">
|
|
<div class="card"><div class="v">$(Format-Bytes $totalBytes)</div><div class="l">Total Storage Used</div></div>
|
|
<div class="card"><div class="v">$(Format-Bytes $totalVersionBytes)</div><div class="l">Version Storage</div></div>
|
|
<div class="card"><div class="v">$totalFiles</div><div class="l">Total Files</div></div>
|
|
<div class="card"><div class="v">$libCount</div><div class="l">Libraries / Sites Scanned</div></div>
|
|
</div>
|
|
<div class="wrap"><table>
|
|
<thead><tr><th>Library</th><th>Site</th><th style="text-align:right">Files</th><th style="text-align:right">Size</th><th style="text-align:right">Versions</th><th>Share of Total</th><th style="text-align:right">Last Modified</th></tr></thead>
|
|
<tbody>
|
|
$rows
|
|
</tbody></table></div>
|
|
<div class="foot">Generated by SharePoint Toolbox</div>
|
|
</body></html>
|
|
"@
|
|
$html | Out-File -FilePath $OutputPath -Encoding UTF8
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ===== PnP: Permissions =====
|
|
|
|
Function Get-PnPPermissions([Microsoft.SharePoint.Client.SecurableObject]$Object) {
|
|
Switch ($Object.TypedObject.ToString()) {
|
|
"Microsoft.SharePoint.Client.Web" {
|
|
$ObjectType = "Site"
|
|
$ObjectURL = $Object.URL
|
|
$ObjectTitle = $Object.Title
|
|
}
|
|
"Microsoft.SharePoint.Client.ListItem" {
|
|
$ObjectType = "Folder"
|
|
$Folder = Get-PnPProperty -ClientObject $Object -Property Folder
|
|
$ObjectTitle = $Object.Folder.Name
|
|
$ObjectURL = $("{0}{1}" -f $Web.Url.Replace($Web.ServerRelativeUrl,''), $Object.Folder.ServerRelativeUrl)
|
|
}
|
|
Default {
|
|
$ObjectType = $Object.BaseType
|
|
$ObjectTitle = $Object.Title
|
|
$RootFolder = Get-PnPProperty -ClientObject $Object -Property RootFolder
|
|
$ObjectURL = $("{0}{1}" -f $Web.Url.Replace($Web.ServerRelativeUrl,''), $RootFolder.ServerRelativeUrl)
|
|
}
|
|
}
|
|
|
|
Get-PnPProperty -ClientObject $Object -Property HasUniqueRoleAssignments, RoleAssignments
|
|
$HasUniquePermissions = $Object.HasUniqueRoleAssignments
|
|
|
|
Foreach ($RoleAssignment in $Object.RoleAssignments) {
|
|
Get-PnPProperty -ClientObject $RoleAssignment -Property RoleDefinitionBindings, Member
|
|
$PermissionType = $RoleAssignment.Member.PrincipalType
|
|
$PermissionLevels = ($RoleAssignment.RoleDefinitionBindings | Select-Object -ExpandProperty Name |
|
|
Where-Object { $_ -ne "Limited Access" }) -join "; "
|
|
If ($PermissionLevels.Length -eq 0) { Continue }
|
|
|
|
$entry = [PSCustomObject]@{
|
|
Object = $ObjectType
|
|
Title = $ObjectTitle
|
|
URL = $ObjectURL
|
|
HasUniquePermissions = $HasUniquePermissions
|
|
Permissions = $PermissionLevels
|
|
GrantedThrough = ""
|
|
Type = $PermissionType
|
|
Users = ""
|
|
UserLogins = ""
|
|
}
|
|
|
|
If ($PermissionType -eq "SharePointGroup") {
|
|
$grpLogin = $RoleAssignment.Member.LoginName
|
|
If ($grpLogin -match '^SharingLinks\.' -or $grpLogin -eq 'Limited Access System Group') { Continue }
|
|
$GroupMembers = Get-PnPGroupMember -Identity $grpLogin
|
|
If ($GroupMembers.count -eq 0) { Continue }
|
|
$filteredMembers = @($GroupMembers | Where-Object { $_.Title -ne "System Account" })
|
|
$GroupUsers = ($filteredMembers | Select-Object -ExpandProperty Title) -join "; "
|
|
$GroupEmails = ($filteredMembers | Select-Object -ExpandProperty Email) -join "; "
|
|
If ($GroupUsers.Length -eq 0) { Continue }
|
|
$entry.Users = $GroupUsers
|
|
$entry.UserLogins = $GroupEmails
|
|
$entry.GrantedThrough = "SharePoint Group: $grpLogin"
|
|
}
|
|
Else {
|
|
$entry.Users = $RoleAssignment.Member.Title
|
|
$entry.UserLogins = $RoleAssignment.Member.Email
|
|
$entry.GrantedThrough = "Direct Permissions"
|
|
}
|
|
|
|
$script:AllPermissions += $entry
|
|
}
|
|
}
|
|
|
|
Function Generate-PnPSitePermissionRpt {
|
|
[cmdletbinding()]
|
|
Param (
|
|
[String] $SiteURL,
|
|
[String] $ReportFile,
|
|
[switch] $Recursive,
|
|
[switch] $ScanFolders,
|
|
[switch] $IncludeInheritedPermissions
|
|
)
|
|
Try {
|
|
Write-Log "Connecting to SharePoint... (browser window will open)" "Yellow"
|
|
Connect-PnPOnline -Url $SiteURL -Interactive -ClientId $script:pnpCiD
|
|
$Web = Get-PnPWeb
|
|
|
|
Write-Log "Getting Site Collection Administrators..." "Yellow"
|
|
$SiteAdmins = Get-PnPSiteCollectionAdmin
|
|
$script:AllPermissions += [PSCustomObject]@{
|
|
Object = "Site Collection"
|
|
Title = $Web.Title
|
|
URL = $Web.URL
|
|
HasUniquePermissions = "TRUE"
|
|
Users = ($SiteAdmins | Select-Object -ExpandProperty Title) -join "; "
|
|
UserLogins = ($SiteAdmins | Select-Object -ExpandProperty Email) -join "; "
|
|
Type = "Site Collection Administrators"
|
|
Permissions = "Site Owner"
|
|
GrantedThrough = "Direct Permissions"
|
|
}
|
|
|
|
Function Get-PnPFolderPermission([Microsoft.SharePoint.Client.List]$List) {
|
|
Write-Log "`t`tScanning folders in: $($List.Title)" "Yellow"
|
|
$ListItems = Get-PnPListItem -List $List -PageSize 2000
|
|
$Folders = $ListItems | Where-Object {
|
|
($_.FileSystemObjectType -eq "Folder") -and
|
|
($_.FieldValues.FileLeafRef -ne "Forms") -and
|
|
(-Not($_.FieldValues.FileLeafRef.StartsWith("_")))
|
|
}
|
|
# Apply folder depth filter (999 = maximum / no limit)
|
|
If ($script:PermFolderDepth -lt 999) {
|
|
$rf = Get-PnPProperty -ClientObject $List -Property RootFolder
|
|
$rootSrl = $rf.ServerRelativeUrl.TrimEnd('/')
|
|
$Folders = $Folders | Where-Object {
|
|
$relPath = $_.FieldValues.FileRef.Substring($rootSrl.Length).TrimStart('/')
|
|
($relPath -split '/').Count -le $script:PermFolderDepth
|
|
}
|
|
}
|
|
$i = 0
|
|
ForEach ($Folder in $Folders) {
|
|
If ($IncludeInheritedPermissions) { Get-PnPPermissions -Object $Folder }
|
|
Else {
|
|
If ((Get-PnPProperty -ClientObject $Folder -Property HasUniqueRoleAssignments) -eq $true) {
|
|
Get-PnPPermissions -Object $Folder
|
|
}
|
|
}
|
|
$i++
|
|
Write-Progress -PercentComplete ($i / [Math]::Max($Folders.Count,1) * 100) `
|
|
-Activity "Folders in '$($List.Title)'" `
|
|
-Status "'$($Folder.FieldValues.FileLeafRef)' ($i of $($Folders.Count))" -Id 2 -ParentId 1
|
|
}
|
|
}
|
|
|
|
Function Get-PnPListPermission([Microsoft.SharePoint.Client.Web]$Web) {
|
|
$Lists = Get-PnPProperty -ClientObject $Web -Property Lists
|
|
$ExcludedLists = @(
|
|
"Access Requests","App Packages","appdata","appfiles","Apps in Testing","Cache Profiles",
|
|
"Composed Looks","Content and Structure Reports","Content type publishing error log",
|
|
"Converted Forms","Device Channels","Form Templates","fpdatasources",
|
|
"Get started with Apps for Office and SharePoint","List Template Gallery",
|
|
"Long Running Operation Status","Maintenance Log Library","Images","site collection images",
|
|
"Master Docs","Master Page Gallery","MicroFeed","NintexFormXml","Quick Deploy Items",
|
|
"Relationships List","Reusable Content","Reporting Metadata","Reporting Templates",
|
|
"Search Config List","Site Assets","Preservation Hold Library","Site Pages",
|
|
"Solution Gallery","Style Library","Suggested Content Browser Locations","Theme Gallery",
|
|
"TaxonomyHiddenList","User Information List","Web Part Gallery","wfpub","wfsvc",
|
|
"Workflow History","Workflow Tasks","Pages"
|
|
)
|
|
$c = 0
|
|
ForEach ($List in $Lists) {
|
|
If ($List.Hidden -eq $false -and $ExcludedLists -notcontains $List.Title) {
|
|
$c++
|
|
Write-Log "`tList ($c/$($Lists.Count)): $($List.Title)" "Cyan"
|
|
Write-Progress -PercentComplete ($c / [Math]::Max($Lists.Count,1) * 100) `
|
|
-Activity "Scanning lists in $($Web.URL)" `
|
|
-Status "'$($List.Title)' ($c of $($Lists.Count))" -Id 1
|
|
If ($ScanFolders) { Get-PnPFolderPermission -List $List }
|
|
If ($IncludeInheritedPermissions) { Get-PnPPermissions -Object $List }
|
|
Else {
|
|
If ((Get-PnPProperty -ClientObject $List -Property HasUniqueRoleAssignments) -eq $true) {
|
|
Get-PnPPermissions -Object $List
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Function Get-PnPWebPermission([Microsoft.SharePoint.Client.Web]$Web) {
|
|
Write-Log "Processing web: $($Web.URL)" "Yellow"
|
|
Get-PnPPermissions -Object $Web
|
|
Write-Log "`tScanning lists and libraries..." "Yellow"
|
|
Get-PnPListPermission($Web)
|
|
If ($Recursive) {
|
|
$Subwebs = Get-PnPProperty -ClientObject $Web -Property Webs
|
|
Foreach ($Subweb in $web.Webs) {
|
|
If ($IncludeInheritedPermissions) { Get-PnPWebPermission($Subweb) }
|
|
Else {
|
|
If ((Get-PnPProperty -ClientObject $SubWeb -Property HasUniqueRoleAssignments) -eq $true) {
|
|
Get-PnPWebPermission($Subweb)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Get-PnPWebPermission $Web
|
|
|
|
# Export based on chosen format
|
|
Write-Log "Writing output file..." "Yellow"
|
|
If ($script:PermFormat -eq "HTML") {
|
|
$outPath = [System.IO.Path]::ChangeExtension($ReportFile, ".html")
|
|
Export-PermissionsToHTML -Data $script:AllPermissions -SiteTitle $Web.Title -SiteURL $Web.URL -OutputPath $outPath
|
|
$script:PermOutputFile = $outPath
|
|
}
|
|
Else {
|
|
$mergedPerms = Merge-PermissionRows $script:AllPermissions
|
|
$mergedPerms | ForEach-Object {
|
|
$locs = @($_.Locations)
|
|
$uqN = ($locs | Where-Object { $_.HasUniquePermissions -eq 'TRUE' -or $_.HasUniquePermissions -eq $true }).Count
|
|
[PSCustomObject]@{
|
|
Object = ($locs | Select-Object -ExpandProperty Object -Unique) -join ', '
|
|
Title = ($locs | Select-Object -ExpandProperty Title) -join ' | '
|
|
URL = ($locs | Select-Object -ExpandProperty URL) -join ' | '
|
|
HasUniquePermissions = if ($locs.Count -eq 1) { $locs[0].HasUniquePermissions } else { "$uqN/$($locs.Count) uniques" }
|
|
Users = $_.Users
|
|
UserLogins = $_.UserLogins
|
|
Type = $_.Type
|
|
Permissions = $_.Permissions
|
|
GrantedThrough = $_.GrantedThrough
|
|
}
|
|
} | Export-Csv -Path $ReportFile -NoTypeInformation
|
|
$script:PermOutputFile = $ReportFile
|
|
}
|
|
|
|
Write-Log "Report generated successfully!" "LightGreen"
|
|
}
|
|
Catch {
|
|
Write-Log "Error: $($_.Exception.Message)" "Red"
|
|
throw
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ===== PnP: Storage Metrics =====
|
|
|
|
function Get-SiteStorageMetrics {
|
|
param([string]$SiteURL, [switch]$IncludeSubsites, [switch]$PerLibrary)
|
|
|
|
$script:storageResults = @()
|
|
|
|
# Recursively collects subfolders up to $MaxDepth levels deep
|
|
function Collect-FolderStorage([string]$FolderSiteRelUrl, [string]$WebBaseUrl, [int]$CurrentDepth) {
|
|
if ($CurrentDepth -ge $script:FolderDepth) { return @() }
|
|
$result = @()
|
|
try {
|
|
$items = Get-PnPFolderItem -FolderSiteRelativeUrl $FolderSiteRelUrl -ItemType Folder
|
|
foreach ($fi in $items) {
|
|
$fiSrl = "$FolderSiteRelUrl/$($fi.Name)"
|
|
try {
|
|
$fiSm = Get-PnPFolderStorageMetric -FolderSiteRelativeUrl $fiSrl
|
|
$children = Collect-FolderStorage -FolderSiteRelUrl $fiSrl -WebBaseUrl $WebBaseUrl -CurrentDepth ($CurrentDepth + 1)
|
|
$result += [PSCustomObject]@{
|
|
Name = $fi.Name
|
|
URL = "$($WebBaseUrl.TrimEnd('/'))/$fiSrl"
|
|
ItemCount = $fiSm.TotalFileCount
|
|
SizeBytes = $fiSm.TotalSize
|
|
LastModified = if ($fiSm.LastModified) { $fiSm.LastModified.ToString("dd/MM/yyyy HH:mm") } else { "N/A" }
|
|
SubFolders = $children
|
|
}
|
|
} catch {}
|
|
}
|
|
} catch {}
|
|
return $result
|
|
}
|
|
|
|
function Collect-WebStorage([string]$WebUrl) {
|
|
# Connect to this specific web (token is cached, no extra browser prompts)
|
|
Connect-PnPOnline -Url $WebUrl -Interactive -ClientId $script:pnpCiD
|
|
$webObj = Get-PnPWeb
|
|
$wTitle = $webObj.Title
|
|
$wUrl = $webObj.Url
|
|
# ServerRelativeUrl of the web (e.g. "/sites/MySite") - used to compute site-relative paths
|
|
$wSrl = $webObj.ServerRelativeUrl.TrimEnd('/')
|
|
|
|
if ($PerLibrary) {
|
|
$lists = Get-PnPList | Where-Object { $_.BaseType -eq "DocumentLibrary" -and !$_.Hidden }
|
|
foreach ($list in $lists) {
|
|
$rf = Get-PnPProperty -ClientObject $list -Property RootFolder
|
|
try {
|
|
# Convert server-relative (/sites/X/LibName) to site-relative (LibName)
|
|
$siteRelUrl = $rf.ServerRelativeUrl.Substring($wSrl.Length).TrimStart('/')
|
|
$sm = Get-PnPFolderStorageMetric -FolderSiteRelativeUrl $siteRelUrl
|
|
$libUrl = "$($wUrl.TrimEnd('/'))/$siteRelUrl"
|
|
|
|
# Recursively collect subfolders up to the configured depth
|
|
$subFolders = Collect-FolderStorage -FolderSiteRelUrl $siteRelUrl -WebBaseUrl $wUrl -CurrentDepth 0
|
|
|
|
$script:storageResults += [PSCustomObject]@{
|
|
SiteTitle = $wTitle
|
|
SiteURL = $wUrl
|
|
Library = $list.Title
|
|
LibraryURL = $libUrl
|
|
ItemCount = $sm.TotalFileCount
|
|
SizeBytes = $sm.TotalSize
|
|
SizeMB = [math]::Round($sm.TotalSize / 1MB, 1)
|
|
SizeGB = [math]::Round($sm.TotalSize / 1GB, 3)
|
|
LastModified = if ($sm.LastModified) { $sm.LastModified.ToString("dd/MM/yyyy HH:mm") } else { "N/A" }
|
|
SubFolders = $subFolders
|
|
}
|
|
Write-Log "`t $($list.Title): $(Format-Bytes $sm.TotalSize) ($($sm.TotalFileCount) files, $($subFolders.Count) folders)" "Cyan"
|
|
}
|
|
catch { Write-Log "`t Skipped '$($list.Title)': $($_.Exception.Message)" "DarkGray" }
|
|
}
|
|
}
|
|
else {
|
|
try {
|
|
$sm = Get-PnPFolderStorageMetric -FolderSiteRelativeUrl "/"
|
|
$script:storageResults += [PSCustomObject]@{
|
|
SiteTitle = $wTitle
|
|
SiteURL = $wUrl
|
|
Library = "(All Libraries)"
|
|
LibraryURL = $wUrl
|
|
ItemCount = $sm.TotalFileCount
|
|
SizeBytes = $sm.TotalSize
|
|
SizeMB = [math]::Round($sm.TotalSize / 1MB, 1)
|
|
SizeGB = [math]::Round($sm.TotalSize / 1GB, 3)
|
|
LastModified = if ($sm.LastModified) { $sm.LastModified.ToString("dd/MM/yyyy HH:mm") } else { "N/A" }
|
|
}
|
|
Write-Log "`t${wTitle}: $(Format-Bytes $sm.TotalSize) ($($sm.TotalFileCount) files)" "Cyan"
|
|
}
|
|
catch { Write-Log "`tSkipped '${wTitle}': $($_.Exception.Message)" "DarkGray" }
|
|
}
|
|
|
|
if ($IncludeSubsites) {
|
|
$subWebs = Get-PnPSubWeb
|
|
foreach ($sub in $subWebs) {
|
|
Write-Log "`tProcessing subsite: $($sub.Title)" "Yellow"
|
|
Collect-WebStorage $sub.Url
|
|
# Reconnect to parent so subsequent sibling subsites resolve correctly
|
|
Connect-PnPOnline -Url $WebUrl -Interactive -ClientId $script:pnpCiD
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Log "Collecting storage metrics for: $SiteURL" "Yellow"
|
|
Collect-WebStorage $SiteURL
|
|
return $script:storageResults
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ===== File Search =====
|
|
|
|
function Export-SearchResultsToHTML {
|
|
param([array]$Results, [string]$KQL, [string]$SiteUrl)
|
|
$rows = ""
|
|
foreach ($r in $Results) {
|
|
$title = EscHtml ($r.Title -replace '^$', '(sans titre)')
|
|
$path = EscHtml ($r.Path -replace '^$', '')
|
|
$ext = EscHtml ($r.FileExtension -replace '^$', '')
|
|
$created = EscHtml ($r.Created -replace '^$', '')
|
|
$modif = EscHtml ($r.LastModifiedTime -replace '^$', '')
|
|
$author = EscHtml ($r.Author -replace '^$', '')
|
|
$modby = EscHtml ($r.ModifiedBy -replace '^$', '')
|
|
$sizeB = [long]($r.Size -replace '[^0-9]','0' -replace '^$','0')
|
|
$sizeStr = EscHtml (Format-Bytes $sizeB)
|
|
$href = EscHtml $r.Path
|
|
$rows += "<tr><td><a href='$href' target='_blank'>$title</a></td>"
|
|
$rows += "<td><span class='ext'>$ext</span></td>"
|
|
$rows += "<td data-ts='$(EscHtml ($r.Created -replace '^$','0'))'>$created</td>"
|
|
$rows += "<td data-ts='$(EscHtml ($r.LastModifiedTime -replace '^$','0'))'>$modif</td>"
|
|
$rows += "<td>$author</td><td>$modby</td>"
|
|
$rows += "<td style='text-align:right' data-size='$sizeB'>$sizeStr</td></tr>`n"
|
|
}
|
|
$count = $Results.Count
|
|
$date = Get-Date -Format "dd/MM/yyyy HH:mm"
|
|
$kqlEsc = EscHtml $KQL
|
|
$siteEsc = EscHtml $SiteUrl
|
|
|
|
$html = @"
|
|
<!DOCTYPE html><html lang="fr"><head><meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Recherche de fichiers</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:'Segoe UI',Arial,sans-serif;background:#f0f2f5;color:#333;padding:30px}
|
|
h1{font-size:21px;font-weight:600;margin-bottom:6px}
|
|
.hdr{background:#0078d4;color:#fff;padding:22px 28px;border-radius:10px;margin-bottom:22px}
|
|
.hdr .sub{font-size:12px;opacity:.8;margin-top:6px;word-break:break-all}
|
|
.cards{display:flex;gap:14px;margin-bottom:22px}
|
|
.card{background:#fff;border-radius:8px;padding:16px 20px;flex:1;box-shadow:0 1px 4px rgba(0,0,0,.08);text-align:center}
|
|
.card .v{font-size:26px;font-weight:700;color:#0078d4}
|
|
.card .l{font-size:12px;color:#888;margin-top:3px}
|
|
.kql{background:#fff;border-radius:8px;padding:12px 16px;margin-bottom:18px;box-shadow:0 1px 4px rgba(0,0,0,.08);font-family:Consolas,monospace;font-size:12px;color:#333;word-break:break-all}
|
|
.kql strong{color:#0078d4}
|
|
.wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.08);overflow:hidden}
|
|
.srch{padding:12px 16px;border-bottom:1px solid #eee}
|
|
.srch input{width:100%;padding:6px 10px;border:1px solid #ccc;border-radius:4px;font-size:13px;outline:none}
|
|
.srch input:focus{border-color:#0078d4}
|
|
table{border-collapse:collapse;width:100%}
|
|
th{background:#0078d4;color:#fff;padding:10px 14px;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap;cursor:pointer}
|
|
th:hover{background:#005fa3}
|
|
td{padding:8px 14px;font-size:12px;border-bottom:1px solid #f0f0f0;vertical-align:middle}
|
|
tr:last-child td{border-bottom:none}
|
|
tr:hover td{background:#f5f9ff}
|
|
a{color:#0078d4;text-decoration:none}
|
|
a:hover{text-decoration:underline}
|
|
.ext{background:#e8f0fe;color:#1a73e8;border-radius:3px;padding:2px 6px;font-size:11px;font-family:Consolas,monospace;font-weight:600}
|
|
.si{font-size:10px;opacity:.7}
|
|
.footer{margin-top:18px;font-size:11px;color:#999;text-align:center}
|
|
.hidden{display:none}
|
|
</style>
|
|
</head><body>
|
|
<div class="hdr">
|
|
<h1>Recherche de fichiers SharePoint</h1>
|
|
<div class="sub">Site : <a href="$siteEsc" style="color:#cce4ff">$siteEsc</a> — Genere le $date</div>
|
|
</div>
|
|
<div class="cards">
|
|
<div class="card"><div class="v">$count</div><div class="l">Fichiers trouves</div></div>
|
|
</div>
|
|
<div class="kql"><strong>Requete KQL :</strong> $kqlEsc</div>
|
|
<div class="wrap">
|
|
<div class="srch"><input type="text" id="q" placeholder="Filtrer les resultats..." onkeyup="filterTable()"></div>
|
|
<table id="tbl">
|
|
<thead><tr>
|
|
<th onclick="sortTable(0)" data-col="0">Nom du fichier <span class="si"></span></th>
|
|
<th onclick="sortTable(1)" data-col="1">Extension <span class="si"></span></th>
|
|
<th onclick="sortTable(2)" data-col="2">Cree le <span class="si"></span></th>
|
|
<th onclick="sortTable(3)" data-col="3">Modifie le <span class="si"></span></th>
|
|
<th onclick="sortTable(4)" data-col="4">Cree par <span class="si"></span></th>
|
|
<th onclick="sortTable(5)" data-col="5">Modifie par <span class="si"></span></th>
|
|
<th onclick="sortTable(6)" data-col="6" style="text-align:right">Taille <span class="si"></span></th>
|
|
</tr></thead>
|
|
<tbody id="tbody">$rows</tbody>
|
|
</table></div>
|
|
<div class="footer">SharePoint Exporter — Recherche de fichiers</div>
|
|
<script>
|
|
var sortDir={};
|
|
function sortTable(c){
|
|
var tb=document.getElementById('tbody'),rows=Array.from(tb.rows).filter(function(r){return !r.classList.contains('hidden');});
|
|
var allRows=Array.from(tb.rows);
|
|
var asc=sortDir[c]=!sortDir[c];
|
|
rows.sort(function(a,b){
|
|
if(c===6){
|
|
return asc
|
|
? (parseFloat(a.cells[c].dataset.size||0)-parseFloat(b.cells[c].dataset.size||0))
|
|
: (parseFloat(b.cells[c].dataset.size||0)-parseFloat(a.cells[c].dataset.size||0));
|
|
}
|
|
if(c===2||c===3){
|
|
var dx=new Date(a.cells[c].dataset.ts||0).getTime()||0;
|
|
var dy=new Date(b.cells[c].dataset.ts||0).getTime()||0;
|
|
return asc?dx-dy:dy-dx;
|
|
}
|
|
var x=a.cells[c].innerText.trim(),y=b.cells[c].innerText.trim();
|
|
return asc?x.localeCompare(y,'fr',{numeric:true}):y.localeCompare(x,'fr',{numeric:true});
|
|
});
|
|
/* re-append sorted visible rows, keep hidden rows at the end */
|
|
var hidden=allRows.filter(function(r){return r.classList.contains('hidden');});
|
|
rows.concat(hidden).forEach(function(r){tb.appendChild(r);});
|
|
/* update sort indicators */
|
|
document.querySelectorAll('th[data-col] .si').forEach(function(s){s.textContent='';});
|
|
var th=document.querySelector('th[data-col="'+c+'"] .si');
|
|
if(th) th.textContent=asc?' \u25b2':' \u25bc';
|
|
}
|
|
function filterTable(){
|
|
var q=document.getElementById('q').value.toLowerCase();
|
|
Array.from(document.getElementById('tbody').rows).forEach(function(r){
|
|
r.classList.toggle('hidden', q && !r.innerText.toLowerCase().includes(q));
|
|
});
|
|
}
|
|
</script>
|
|
</body></html>
|
|
"@
|
|
return $html
|
|
}
|
|
|
|
function Export-DuplicatesToHTML {
|
|
param([array]$Groups, [string]$Mode, [string]$SiteUrl)
|
|
|
|
$totalItems = ($Groups | ForEach-Object { $_.Items.Count } | Measure-Object -Sum).Sum
|
|
$totalGroups = $Groups.Count
|
|
$date = Get-Date -Format "dd/MM/yyyy HH:mm"
|
|
$modeLabel = if ($Mode -eq "Folders") { "Dossiers" } else { "Fichiers" }
|
|
$siteEsc = EscHtml $SiteUrl
|
|
|
|
# Build group cards
|
|
$cards = ""
|
|
$gIdx = 0
|
|
foreach ($grp in $Groups | Sort-Object { $_.Items.Count } -Descending) {
|
|
$gIdx++
|
|
$name = EscHtml $grp.Name
|
|
$count = $grp.Items.Count
|
|
$items = $grp.Items
|
|
|
|
# Determine which columns to show
|
|
$hasSz = $items[0].PSObject.Properties.Name -contains "SizeBytes"
|
|
$hasCr = $items[0].PSObject.Properties.Name -contains "Created"
|
|
$hasMod = $items[0].PSObject.Properties.Name -contains "Modified"
|
|
$hasSub = $items[0].PSObject.Properties.Name -contains "FolderCount"
|
|
$hasFil = $items[0].PSObject.Properties.Name -contains "FileCount"
|
|
|
|
# Helper: check if all values in column are identical → green, else orange
|
|
$allSame = { param($prop)
|
|
$vals = @($items | ForEach-Object { $_.$prop })
|
|
($vals | Select-Object -Unique).Count -le 1
|
|
}
|
|
|
|
$szSame = if ($hasSz) { & $allSame "SizeBytes" } else { $true }
|
|
$crSame = if ($hasCr) { & $allSame "CreatedDay" } else { $true }
|
|
$modSame = if ($hasMod) { & $allSame "ModifiedDay" } else { $true }
|
|
$subSame = if ($hasSub) { & $allSame "FolderCount" } else { $true }
|
|
$filSame = if ($hasFil) { & $allSame "FileCount" } else { $true }
|
|
|
|
$thSz = if ($hasSz) { "<th class='$(if($szSame){"ok"}else{"diff"})'>Taille</th>" } else {""}
|
|
$thCr = if ($hasCr) { "<th class='$(if($crSame){"ok"}else{"diff"})'>Cree le</th>" } else {""}
|
|
$thMod = if ($hasMod) { "<th class='$(if($modSame){"ok"}else{"diff"})'>Modifie le</th>" } else {""}
|
|
$thSub = if ($hasSub) { "<th class='$(if($subSame){"ok"}else{"diff"})'>Sous-dossiers</th>" } else {""}
|
|
$thFil = if ($hasFil) { "<th class='$(if($filSame){"ok"}else{"diff"})'>Fichiers</th>" } else {""}
|
|
|
|
$rows = ""
|
|
foreach ($item in $items) {
|
|
$nm = EscHtml $item.Name
|
|
$path = EscHtml $item.Path
|
|
$href = EscHtml $item.Path
|
|
$lib = EscHtml $item.Library
|
|
|
|
$tdSz = if ($hasSz) {
|
|
$cls = if ($szSame) { "ok" } else { "diff" }
|
|
"<td class='$cls r'>$(EscHtml (Format-Bytes ([long]$item.SizeBytes)))</td>"
|
|
} else {""}
|
|
$tdCr = if ($hasCr) {
|
|
$cls = if ($crSame) { "ok" } else { "diff" }
|
|
"<td class='$cls'>$(EscHtml $item.Created)</td>"
|
|
} else {""}
|
|
$tdMod = if ($hasMod) {
|
|
$cls = if ($modSame) { "ok" } else { "diff" }
|
|
"<td class='$cls'>$(EscHtml $item.Modified)</td>"
|
|
} else {""}
|
|
$tdSub = if ($hasSub) {
|
|
$cls = if ($subSame) { "ok" } else { "diff" }
|
|
"<td class='$cls r'>$($item.FolderCount)</td>"
|
|
} else {""}
|
|
$tdFil = if ($hasFil) {
|
|
$cls = if ($filSame) { "ok" } else { "diff" }
|
|
"<td class='$cls r'>$($item.FileCount)</td>"
|
|
} else {""}
|
|
|
|
$rows += "<tr>"
|
|
$rows += "<td><a href='$href' target='_blank'>$nm</a><br><small class='lib'>$lib</small></td>"
|
|
$rows += "$tdSz$tdCr$tdMod$tdSub$tdFil"
|
|
$rows += "</tr>`n"
|
|
}
|
|
|
|
$diffBadge = if ($szSame -and $crSame -and $modSame -and $subSame -and $filSame) {
|
|
"<span class='badge-id'>Identiques</span>"
|
|
} else {
|
|
"<span class='badge-diff'>Differences detectees</span>"
|
|
}
|
|
|
|
$cards += @"
|
|
<div class="grp-card" id="g$gIdx">
|
|
<div class="grp-hdr" onclick="toggleGrp($gIdx)">
|
|
<span class="grp-name">$name</span>
|
|
<span class="grp-count">$count occurrences</span>
|
|
$diffBadge
|
|
<span class="grp-arrow" id="arr$gIdx">▼</span>
|
|
</div>
|
|
<div class="grp-body" id="gb$gIdx">
|
|
<table><thead><tr>
|
|
<th>Chemin</th>$thSz$thCr$thMod$thSub$thFil
|
|
</tr></thead><tbody>$rows</tbody></table>
|
|
</div></div>
|
|
"@
|
|
}
|
|
|
|
$html = @"
|
|
<!DOCTYPE html><html lang="fr"><head><meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Doublons $modeLabel - SharePoint</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:'Segoe UI',Arial,sans-serif;background:#f0f2f5;color:#333;padding:30px}
|
|
h1{font-size:21px;font-weight:600;margin-bottom:6px}
|
|
.hdr{background:#0078d4;color:#fff;padding:22px 28px;border-radius:10px;margin-bottom:22px}
|
|
.hdr .sub{font-size:12px;opacity:.8;margin-top:6px}
|
|
.cards{display:flex;gap:14px;margin-bottom:22px}
|
|
.card{background:#fff;border-radius:8px;padding:16px 20px;flex:1;box-shadow:0 1px 4px rgba(0,0,0,.08);text-align:center}
|
|
.card .v{font-size:26px;font-weight:700;color:#0078d4}
|
|
.card .l{font-size:12px;color:#888;margin-top:3px}
|
|
.srch{background:#fff;border-radius:8px;padding:10px 14px;margin-bottom:14px;box-shadow:0 1px 4px rgba(0,0,0,.08)}
|
|
.srch input{width:100%;padding:6px 10px;border:1px solid #ccc;border-radius:4px;font-size:13px;outline:none}
|
|
.srch input:focus{border-color:#0078d4}
|
|
.grp-card{background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin-bottom:12px;overflow:hidden}
|
|
.grp-hdr{display:flex;align-items:center;gap:10px;padding:12px 16px;cursor:pointer;background:#fff;border-bottom:1px solid #eee;user-select:none}
|
|
.grp-hdr:hover{background:#f5f9ff}
|
|
.grp-name{font-weight:600;font-size:14px;flex:1}
|
|
.grp-count{font-size:12px;color:#888;white-space:nowrap}
|
|
.grp-arrow{font-size:11px;color:#aaa;transition:transform .2s}
|
|
.grp-body{overflow-x:auto;max-height:500px;overflow-y:auto}
|
|
.grp-body.collapsed{display:none}
|
|
table{border-collapse:collapse;width:100%;min-width:500px}
|
|
th{background:#e8f0fe;color:#1a56a0;padding:8px 12px;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.4px;white-space:nowrap}
|
|
th.ok{background:#e6f4ea;color:#1e7e34}
|
|
th.diff{background:#fff3cd;color:#856404}
|
|
td{padding:7px 12px;font-size:12px;border-bottom:1px solid #f0f0f0;vertical-align:middle}
|
|
td.ok{background:#f0faf2}
|
|
td.diff{background:#fff8e6}
|
|
td.r{text-align:right}
|
|
tr:last-child td{border-bottom:none}
|
|
a{color:#0078d4;text-decoration:none}
|
|
a:hover{text-decoration:underline}
|
|
small.lib{color:#999;font-size:10px}
|
|
.badge-id{background:#e6f4ea;color:#1e7e34;border-radius:20px;padding:2px 8px;font-size:11px;font-weight:600}
|
|
.badge-diff{background:#fff3cd;color:#856404;border-radius:20px;padding:2px 8px;font-size:11px;font-weight:600}
|
|
.hidden{display:none}
|
|
.footer{margin-top:20px;font-size:11px;color:#999;text-align:center}
|
|
</style>
|
|
</head><body>
|
|
<div class="hdr">
|
|
<h1>Doublons de $modeLabel — SharePoint</h1>
|
|
<div class="sub">Site : <a href="$siteEsc" style="color:#cce4ff">$siteEsc</a> — Genere le $date</div>
|
|
</div>
|
|
<div class="cards">
|
|
<div class="card"><div class="v">$totalGroups</div><div class="l">Groupes de doublons</div></div>
|
|
<div class="card"><div class="v">$totalItems</div><div class="l">$modeLabel en double (total)</div></div>
|
|
</div>
|
|
<div class="srch"><input type="text" id="q" placeholder="Filtrer par nom..." onkeyup="filterGroups()"></div>
|
|
$cards
|
|
<div class="footer">SharePoint Exporter — Detection de doublons</div>
|
|
<script>
|
|
function toggleGrp(i){
|
|
var b=document.getElementById('gb'+i);
|
|
var a=document.getElementById('arr'+i);
|
|
b.classList.toggle('collapsed');
|
|
a.style.transform=b.classList.contains('collapsed')?'rotate(-90deg)':'';
|
|
}
|
|
function filterGroups(){
|
|
var q=document.getElementById('q').value.toLowerCase();
|
|
document.querySelectorAll('.grp-card').forEach(function(c){
|
|
var nm=c.querySelector('.grp-name').textContent.toLowerCase();
|
|
c.classList.toggle('hidden', q && !nm.includes(q));
|
|
});
|
|
}
|
|
</script>
|
|
</body></html>
|
|
"@
|
|
return $html
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ===== Internationalization =====
|
|
|
|
$script:LangDefault = @{
|
|
"profile" = "Profile:"
|
|
"tenant.url" = "Tenant URL:"
|
|
"client.id" = "Client ID:"
|
|
"site.url" = "Site URL:"
|
|
"output.folder" = "Output Folder:"
|
|
"btn.new" = "New"
|
|
"btn.save" = "Save"
|
|
"btn.rename" = "Rename"
|
|
"btn.delete" = "Del."
|
|
"btn.view.sites" = "View Sites"
|
|
"btn.browse" = "Browse..."
|
|
"tab.perms" = " Permissions "
|
|
"tab.storage" = " Storage "
|
|
"tab.templates" = " Templates "
|
|
"tab.search" = " File Search "
|
|
"tab.dupes" = " Duplicates "
|
|
"grp.scan.opts" = "Scan Options"
|
|
"chk.scan.folders" = "Scan Folders"
|
|
"chk.recursive" = "Recursive (subsites)"
|
|
"lbl.folder.depth" = "Folder depth:"
|
|
"chk.max.depth" = "Maximum (all levels)"
|
|
"chk.inherited.perms" = "Include Inherited Permissions"
|
|
"grp.export.fmt" = "Export Format"
|
|
"rad.csv.perms" = "CSV"
|
|
"rad.html.perms" = "HTML"
|
|
"btn.gen.perms" = "Generate Report"
|
|
"btn.open.perms" = "Open Report"
|
|
"chk.per.lib" = "Per-Library Breakdown"
|
|
"chk.subsites" = "Include Subsites"
|
|
"stor.note" = "Note: deeper folder scans on large sites may take several minutes."
|
|
"btn.gen.storage" = "Generate Metrics"
|
|
"btn.open.storage" = "Open Report"
|
|
"tpl.desc" = "Create templates from an existing site and apply them to create new sites."
|
|
"btn.manage.tpl" = "Manage templates..."
|
|
"tpl.count" = "template(s) saved - click to manage"
|
|
"grp.search.filters" = "Search Filters"
|
|
"lbl.extensions" = "Extension(s):"
|
|
"lbl.regex" = "Name / Regex:"
|
|
"chk.created.after" = "Created after:"
|
|
"chk.created.before" = "Created before:"
|
|
"chk.modified.after" = "Modified after:"
|
|
"chk.modified.before" = "Modified before:"
|
|
"lbl.created.by" = "Created by:"
|
|
"lbl.modified.by" = "Modified by:"
|
|
"lbl.library" = "Library:"
|
|
"grp.search.fmt" = "Export Format"
|
|
"lbl.max.results" = "Max results:"
|
|
"btn.run.search" = "Run Search"
|
|
"btn.open.search" = "Open Results"
|
|
"grp.dup.type" = "Duplicate Type"
|
|
"rad.dup.files" = "Duplicate files"
|
|
"rad.dup.folders" = "Duplicate folders"
|
|
"grp.dup.criteria" = "Comparison Criteria"
|
|
"lbl.dup.note" = "Name is always the primary criterion. Check additional criteria:"
|
|
"chk.dup.size" = "Same size"
|
|
"chk.dup.created" = "Same creation date"
|
|
"chk.dup.modified" = "Same modification date"
|
|
"chk.dup.subfolders" = "Same subfolder count"
|
|
"chk.dup.filecount" = "Same file count"
|
|
"grp.options" = "Options"
|
|
"chk.include.subsites" = "Include subsites"
|
|
"btn.run.scan" = "Run Scan"
|
|
"btn.open.results" = "Open Results"
|
|
"lbl.log" = "Log:"
|
|
"menu.settings" = "Settings"
|
|
"menu.json.folder" = "JSON Data Folder..."
|
|
"menu.language" = "Language"
|
|
"dlg.json.folder.desc" = "Select the storage folder for JSON files (profiles, templates)"
|
|
"dlg.folder.not.found" = "The folder '{0}' does not exist. Do you want to create it?"
|
|
"dlg.folder.not.found.title"= "Folder not found"
|
|
"msg.lang.applied" = "Language applied: {0}"
|
|
"msg.lang.applied.title" = "Language"
|
|
"ph.extensions" = "docx pdf xlsx"
|
|
"ph.regex" = "Ex: report.* or \.bak$"
|
|
"ph.created.by" = "First Last or email"
|
|
"ph.modified.by" = "First Last or email"
|
|
"ph.library" = "Optional relative path e.g. Shared Documents"
|
|
"ph.dup.lib" = "All (leave empty)"
|
|
}
|
|
|
|
$script:Lang = $null # null = use LangDefault
|
|
|
|
function T([string]$key) {
|
|
if ($script:Lang -and $script:Lang.$key) { return $script:Lang.$key }
|
|
if ($script:LangDefault.ContainsKey($key)) { return $script:LangDefault[$key] }
|
|
return $key
|
|
}
|
|
|
|
function Get-LangDir {
|
|
$base = if ($PSScriptRoot) { $PSScriptRoot } else { $PWD.Path }
|
|
return Join-Path $base "lang"
|
|
}
|
|
|
|
function Get-LangFiles {
|
|
$dir = Get-LangDir
|
|
if (-not (Test-Path $dir)) { return @() }
|
|
return @(Get-ChildItem -Path $dir -Filter "*.json" | ForEach-Object {
|
|
$code = $_.BaseName
|
|
$name = $code
|
|
try {
|
|
$data = Get-Content $_.FullName -Raw | ConvertFrom-Json
|
|
if ($data.'_name') { $name = $data.'_name' }
|
|
} catch {}
|
|
[PSCustomObject]@{ Code = $code; Name = $name; Path = $_.FullName }
|
|
})
|
|
}
|
|
|
|
function Load-Language([string]$LangCode) {
|
|
if ([string]::IsNullOrWhiteSpace($LangCode) -or $LangCode -eq "en") {
|
|
$script:Lang = $null
|
|
$script:CurrentLang = "en"
|
|
return
|
|
}
|
|
$dir = Get-LangDir
|
|
$path = Join-Path $dir "$LangCode.json"
|
|
if (-not (Test-Path $path)) { return }
|
|
try {
|
|
$data = Get-Content $path -Raw | ConvertFrom-Json
|
|
$ht = @{}
|
|
$data.PSObject.Properties | ForEach-Object { $ht[$_.Name] = $_.Value }
|
|
$script:Lang = $ht
|
|
$script:CurrentLang = $LangCode
|
|
} catch {}
|
|
}
|
|
|
|
function Update-UILanguage {
|
|
# Main labels
|
|
if ($script:i18nMap) {
|
|
foreach ($kv in $script:i18nMap.GetEnumerator()) {
|
|
$ctrl = $kv.Value.Control
|
|
$key = $kv.Value.Key
|
|
if ($ctrl -and !$ctrl.IsDisposed) { $ctrl.Text = T $key }
|
|
}
|
|
}
|
|
# Tab pages (Text property)
|
|
if ($script:i18nTabs) {
|
|
foreach ($kv in $script:i18nTabs.GetEnumerator()) {
|
|
$tab = $kv.Value.Control
|
|
$key = $kv.Value.Key
|
|
if ($tab -and !$tab.IsDisposed) { $tab.Text = T $key }
|
|
}
|
|
}
|
|
# Menu items
|
|
if ($script:i18nMenus) {
|
|
foreach ($kv in $script:i18nMenus.GetEnumerator()) {
|
|
$mi = $kv.Value.Control
|
|
$key = $kv.Value.Key
|
|
if ($mi) { $mi.Text = T $key }
|
|
}
|
|
}
|
|
# Placeholder texts
|
|
if ($script:i18nPlaceholders) {
|
|
foreach ($kv in $script:i18nPlaceholders.GetEnumerator()) {
|
|
$ctrl = $kv.Value.Control
|
|
$key = $kv.Value.Key
|
|
if ($ctrl -and !$ctrl.IsDisposed) { $ctrl.PlaceholderText = T $key }
|
|
}
|
|
}
|
|
}
|
|
|
|
$script:CurrentLang = "en"
|
|
|
|
#endregion
|
|
|
|
#region ===== GUI =====
|
|
|
|
$form = New-Object System.Windows.Forms.Form
|
|
$form.Text = "SharePoint Toolbox"
|
|
$form.Size = New-Object System.Drawing.Size(700, 840)
|
|
$form.StartPosition = "CenterScreen"
|
|
$form.FormBorderStyle = "FixedDialog"
|
|
$form.MaximizeBox = $false
|
|
$form.BackColor = [System.Drawing.Color]::WhiteSmoke
|
|
|
|
# ── MenuStrip ─────────────────────────────────────────────────────────────────
|
|
$menuStrip = New-Object System.Windows.Forms.MenuStrip
|
|
$menuStrip.BackColor = [System.Drawing.Color]::WhiteSmoke
|
|
$menuStrip.RenderMode = [System.Windows.Forms.ToolStripRenderMode]::System
|
|
|
|
$menuSettings = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
$menuSettings.Text = T "menu.settings"
|
|
$menuJsonFolder = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
$menuJsonFolder.Text = T "menu.json.folder"
|
|
[void]$menuSettings.DropDownItems.Add($menuJsonFolder)
|
|
|
|
$menuLang = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
$menuLang.Text = T "menu.language"
|
|
$menuLangEn = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
$menuLangEn.Text = "English (US)"
|
|
$menuLangEn.Tag = "en"
|
|
$menuLangEn.Checked = ($script:CurrentLang -eq "en")
|
|
[void]$menuLang.DropDownItems.Add($menuLangEn)
|
|
[void]$menuLang.DropDownItems.Add([System.Windows.Forms.ToolStripSeparator]::new())
|
|
foreach ($lf in (Get-LangFiles)) {
|
|
$mi = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
$mi.Text = $lf.Name
|
|
$mi.Tag = $lf.Code
|
|
$mi.Checked = ($script:CurrentLang -eq $lf.Code)
|
|
[void]$menuLang.DropDownItems.Add($mi)
|
|
}
|
|
[void]$menuStrip.Items.Add($menuSettings)
|
|
[void]$menuStrip.Items.Add($menuLang)
|
|
$form.MainMenuStrip = $menuStrip
|
|
|
|
# ── Label helper (positions offset +24 to account for MenuStrip) ──────────────
|
|
$lbl = { param($t,$x,$y)
|
|
$l = New-Object System.Windows.Forms.Label
|
|
$l.Text = $t; $l.Location = New-Object System.Drawing.Point($x,$y)
|
|
$l.Size = New-Object System.Drawing.Size(115,22); $l.TextAlign = "MiddleLeft"; $l
|
|
}
|
|
|
|
# ── Profile selector ──────────────────────────────────────────────────────────
|
|
$lblProfile = (& $lbl (T "profile") 20 46)
|
|
$cboProfile = New-Object System.Windows.Forms.ComboBox
|
|
$cboProfile.Location = New-Object System.Drawing.Point(140, 44)
|
|
$cboProfile.Size = New-Object System.Drawing.Size(248, 24)
|
|
$cboProfile.DropDownStyle = "DropDownList"
|
|
$cboProfile.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
|
|
$btnProfileNew = New-Object System.Windows.Forms.Button
|
|
$btnProfileNew.Text = T "btn.new"
|
|
$btnProfileNew.Location = New-Object System.Drawing.Point(396, 43)
|
|
$btnProfileNew.Size = New-Object System.Drawing.Size(60, 26)
|
|
|
|
$btnProfileSave = New-Object System.Windows.Forms.Button
|
|
$btnProfileSave.Text = T "btn.save"
|
|
$btnProfileSave.Location = New-Object System.Drawing.Point(460, 43)
|
|
$btnProfileSave.Size = New-Object System.Drawing.Size(60, 26)
|
|
|
|
$btnProfileRename = New-Object System.Windows.Forms.Button
|
|
$btnProfileRename.Text = T "btn.rename"
|
|
$btnProfileRename.Location = New-Object System.Drawing.Point(524, 43)
|
|
$btnProfileRename.Size = New-Object System.Drawing.Size(72, 26)
|
|
|
|
$btnProfileDelete = New-Object System.Windows.Forms.Button
|
|
$btnProfileDelete.Text = T "btn.delete"
|
|
$btnProfileDelete.Location = New-Object System.Drawing.Point(600, 43)
|
|
$btnProfileDelete.Size = New-Object System.Drawing.Size(62, 26)
|
|
|
|
$lblTenantUrl = (& $lbl (T "tenant.url") 20 76)
|
|
$txtTenantUrl = New-Object System.Windows.Forms.TextBox
|
|
$txtTenantUrl.Location = New-Object System.Drawing.Point(140, 76)
|
|
$txtTenantUrl.Size = New-Object System.Drawing.Size(400, 22)
|
|
$txtTenantUrl.Font = New-Object System.Drawing.Font("Consolas", 9)
|
|
|
|
$btnBrowseSites = New-Object System.Windows.Forms.Button
|
|
$btnBrowseSites.Text = T "btn.view.sites"
|
|
$btnBrowseSites.Location = New-Object System.Drawing.Point(548, 74)
|
|
$btnBrowseSites.Size = New-Object System.Drawing.Size(92, 26)
|
|
|
|
$lblClientId = (& $lbl (T "client.id") 20 108)
|
|
$txtClientId = New-Object System.Windows.Forms.TextBox
|
|
$txtClientId.Location = New-Object System.Drawing.Point(140, 108)
|
|
$txtClientId.Size = New-Object System.Drawing.Size(500, 22)
|
|
$txtClientId.Font = New-Object System.Drawing.Font("Consolas", 9)
|
|
|
|
$lblSiteURL = (& $lbl (T "site.url") 20 140)
|
|
$txtSiteURL = New-Object System.Windows.Forms.TextBox
|
|
$txtSiteURL.Location = New-Object System.Drawing.Point(140, 140)
|
|
$txtSiteURL.Size = New-Object System.Drawing.Size(500, 22)
|
|
|
|
$lblOutput = (& $lbl (T "output.folder") 20 172)
|
|
$txtOutput = New-Object System.Windows.Forms.TextBox
|
|
$txtOutput.Location = New-Object System.Drawing.Point(140, 172)
|
|
$txtOutput.Size = New-Object System.Drawing.Size(408, 22)
|
|
$txtOutput.Text = $PWD.Path
|
|
|
|
$btnBrowse = New-Object System.Windows.Forms.Button
|
|
$btnBrowse.Text = T "btn.browse"
|
|
$btnBrowse.Location = New-Object System.Drawing.Point(558, 170)
|
|
$btnBrowse.Size = New-Object System.Drawing.Size(82, 26)
|
|
|
|
$sep = New-Object System.Windows.Forms.Panel
|
|
$sep.Location = New-Object System.Drawing.Point(20, 206)
|
|
$sep.Size = New-Object System.Drawing.Size(642, 1)
|
|
$sep.BackColor = [System.Drawing.Color]::LightGray
|
|
|
|
# ── TabControl ─────────────────────────────────────────────────────────────────
|
|
$tabs = New-Object System.Windows.Forms.TabControl
|
|
$tabs.Location = New-Object System.Drawing.Point(10, 214)
|
|
$tabs.Size = New-Object System.Drawing.Size(662, 310)
|
|
$tabs.Font = New-Object System.Drawing.Font("Segoe UI", 9)
|
|
|
|
# helper: GroupBox
|
|
function New-Group($text, $x, $y, $w, $h) {
|
|
$g = New-Object System.Windows.Forms.GroupBox
|
|
$g.Text = $text; $g.Location = New-Object System.Drawing.Point($x,$y)
|
|
$g.Size = New-Object System.Drawing.Size($w,$h); $g
|
|
}
|
|
# helper: CheckBox
|
|
function New-Check($text, $x, $y, $w, $checked=$false) {
|
|
$c = New-Object System.Windows.Forms.CheckBox
|
|
$c.Text = $text; $c.Location = New-Object System.Drawing.Point($x,$y)
|
|
$c.Size = New-Object System.Drawing.Size($w,22); $c.Checked = $checked; $c
|
|
}
|
|
# helper: RadioButton
|
|
function New-Radio($text, $x, $y, $w, $checked=$false) {
|
|
$r = New-Object System.Windows.Forms.RadioButton
|
|
$r.Text = $text; $r.Location = New-Object System.Drawing.Point($x,$y)
|
|
$r.Size = New-Object System.Drawing.Size($w,22); $r.Checked = $checked; $r
|
|
}
|
|
# helper: Action button
|
|
function New-ActionBtn($text, $x, $y, $color) {
|
|
$b = New-Object System.Windows.Forms.Button
|
|
$b.Text = $text; $b.Location = New-Object System.Drawing.Point($x,$y)
|
|
$b.Size = New-Object System.Drawing.Size(155,34); $b.BackColor = $color
|
|
$b.ForeColor = [System.Drawing.Color]::White; $b.FlatStyle = "Flat"
|
|
$b.Font = New-Object System.Drawing.Font("Segoe UI",9,[System.Drawing.FontStyle]::Bold); $b
|
|
}
|
|
|
|
# ══ Tab 1: Permissions ════════════════════════════════════════════════════════
|
|
$tabPerms = New-Object System.Windows.Forms.TabPage
|
|
$tabPerms.Text = T "tab.perms"
|
|
$tabPerms.BackColor = [System.Drawing.Color]::WhiteSmoke
|
|
|
|
$grpPermOpts = New-Group (T "grp.scan.opts") 10 10 615 96
|
|
$chkScanFolders = New-Check (T "chk.scan.folders") 15 24 150 $true
|
|
$chkRecursive = New-Check (T "chk.recursive") 175 24 185
|
|
|
|
# Folder depth controls (only active when Scan Folders is checked)
|
|
$lblPermDepth = New-Object System.Windows.Forms.Label
|
|
$lblPermDepth.Text = T "lbl.folder.depth"
|
|
$lblPermDepth.Location = New-Object System.Drawing.Point(15, 50)
|
|
$lblPermDepth.Size = New-Object System.Drawing.Size(100, 22)
|
|
$lblPermDepth.TextAlign = "MiddleLeft"
|
|
|
|
$nudPermDepth = New-Object System.Windows.Forms.NumericUpDown
|
|
$nudPermDepth.Location = New-Object System.Drawing.Point(118, 50)
|
|
$nudPermDepth.Size = New-Object System.Drawing.Size(52, 22)
|
|
$nudPermDepth.Minimum = 1
|
|
$nudPermDepth.Maximum = 20
|
|
$nudPermDepth.Value = 1
|
|
|
|
$chkPermMaxDepth = New-Object System.Windows.Forms.CheckBox
|
|
$chkPermMaxDepth.Text = T "chk.max.depth"
|
|
$chkPermMaxDepth.Location = New-Object System.Drawing.Point(182, 52)
|
|
$chkPermMaxDepth.Size = New-Object System.Drawing.Size(180, 20)
|
|
|
|
$chkInheritedPerms = New-Check (T "chk.inherited.perms") 15 74 230
|
|
$grpPermOpts.Controls.AddRange(@($chkScanFolders, $chkRecursive, $lblPermDepth, $nudPermDepth, $chkPermMaxDepth, $chkInheritedPerms))
|
|
|
|
# Disable depth controls when Scan Folders is unchecked
|
|
$chkScanFolders.Add_CheckedChanged({
|
|
$on = $chkScanFolders.Checked
|
|
$lblPermDepth.Enabled = $on
|
|
$nudPermDepth.Enabled = $on -and -not $chkPermMaxDepth.Checked
|
|
$chkPermMaxDepth.Enabled = $on
|
|
})
|
|
# When Maximum is checked, grey out the spinner
|
|
$chkPermMaxDepth.Add_CheckedChanged({
|
|
$nudPermDepth.Enabled = $chkScanFolders.Checked -and -not $chkPermMaxDepth.Checked
|
|
})
|
|
|
|
$grpPermFmt = New-Group (T "grp.export.fmt") 10 114 615 58
|
|
$radPermCSV = New-Radio (T "rad.csv.perms") 15 24 280 $true
|
|
$radPermHTML = New-Radio (T "rad.html.perms") 305 24 290
|
|
$grpPermFmt.Controls.AddRange(@($radPermCSV, $radPermHTML))
|
|
|
|
$btnGenPerms = New-ActionBtn (T "btn.gen.perms") 10 184 ([System.Drawing.Color]::SteelBlue)
|
|
$btnOpenPerms = New-Object System.Windows.Forms.Button
|
|
$btnOpenPerms.Text = T "btn.open.perms"
|
|
$btnOpenPerms.Location = New-Object System.Drawing.Point(175, 184)
|
|
$btnOpenPerms.Size = New-Object System.Drawing.Size(120, 34)
|
|
$btnOpenPerms.Enabled = $false
|
|
|
|
$tabPerms.Controls.AddRange(@($grpPermOpts, $grpPermFmt, $btnGenPerms, $btnOpenPerms))
|
|
|
|
# ══ Tab 2: Storage Metrics ════════════════════════════════════════════════════
|
|
$tabStorage = New-Object System.Windows.Forms.TabPage
|
|
$tabStorage.Text = T "tab.storage"
|
|
$tabStorage.BackColor = [System.Drawing.Color]::WhiteSmoke
|
|
|
|
$grpStorOpts = New-Group (T "grp.scan.opts") 10 10 615 108
|
|
$chkStorPerLib = New-Check (T "chk.per.lib") 15 24 200 $true
|
|
$chkStorSubsites = New-Check (T "chk.subsites") 230 24 170
|
|
|
|
# Folder depth controls (only relevant in per-library mode)
|
|
$lblDepth = New-Object System.Windows.Forms.Label
|
|
$lblDepth.Text = T "lbl.folder.depth"
|
|
$lblDepth.Location = New-Object System.Drawing.Point(15, 52)
|
|
$lblDepth.Size = New-Object System.Drawing.Size(100, 22)
|
|
$lblDepth.TextAlign = "MiddleLeft"
|
|
|
|
$nudDepth = New-Object System.Windows.Forms.NumericUpDown
|
|
$nudDepth.Location = New-Object System.Drawing.Point(118, 52)
|
|
$nudDepth.Size = New-Object System.Drawing.Size(52, 22)
|
|
$nudDepth.Minimum = 1
|
|
$nudDepth.Maximum = 20
|
|
$nudDepth.Value = 1
|
|
|
|
$chkMaxDepth = New-Object System.Windows.Forms.CheckBox
|
|
$chkMaxDepth.Text = T "chk.max.depth"
|
|
$chkMaxDepth.Location = New-Object System.Drawing.Point(182, 54)
|
|
$chkMaxDepth.Size = New-Object System.Drawing.Size(180, 20)
|
|
|
|
$lblStorNote = New-Object System.Windows.Forms.Label
|
|
$lblStorNote.Text = T "stor.note"
|
|
$lblStorNote.Location = New-Object System.Drawing.Point(15, 80)
|
|
$lblStorNote.Size = New-Object System.Drawing.Size(580, 18)
|
|
$lblStorNote.ForeColor = [System.Drawing.Color]::Gray
|
|
$lblStorNote.Font = New-Object System.Drawing.Font("Segoe UI", 8, [System.Drawing.FontStyle]::Italic)
|
|
|
|
$grpStorOpts.Controls.AddRange(@($chkStorPerLib, $chkStorSubsites, $lblDepth, $nudDepth, $chkMaxDepth, $lblStorNote))
|
|
|
|
$grpStorFmt = New-Group (T "grp.export.fmt") 10 128 615 58
|
|
$radStorCSV = New-Radio (T "rad.csv.perms") 15 24 280 $true
|
|
$radStorHTML = New-Radio (T "rad.html.perms") 305 24 290
|
|
$grpStorFmt.Controls.AddRange(@($radStorCSV, $radStorHTML))
|
|
|
|
$msGreen = [System.Drawing.Color]::FromArgb(16,124,16)
|
|
$btnGenStorage = New-ActionBtn (T "btn.gen.storage") 10 200 $msGreen
|
|
$btnOpenStorage = New-Object System.Windows.Forms.Button
|
|
$btnOpenStorage.Text = T "btn.open.storage"
|
|
$btnOpenStorage.Location = New-Object System.Drawing.Point(175, 200)
|
|
$btnOpenStorage.Size = New-Object System.Drawing.Size(120, 34)
|
|
$btnOpenStorage.Enabled = $false
|
|
|
|
# Disable depth controls when Per-Library is unchecked
|
|
$chkStorPerLib.Add_CheckedChanged({
|
|
$on = $chkStorPerLib.Checked
|
|
$lblDepth.Enabled = $on
|
|
$nudDepth.Enabled = $on -and -not $chkMaxDepth.Checked
|
|
$chkMaxDepth.Enabled = $on
|
|
})
|
|
# When Maximum is checked, grey out the spinner
|
|
$chkMaxDepth.Add_CheckedChanged({
|
|
$nudDepth.Enabled = $chkStorPerLib.Checked -and -not $chkMaxDepth.Checked
|
|
})
|
|
|
|
$tabStorage.Controls.AddRange(@($grpStorOpts, $grpStorFmt, $btnGenStorage, $btnOpenStorage))
|
|
|
|
# ══ Tab 3: Templates ══════════════════════════════════════════════════════
|
|
$tabTemplates = New-Object System.Windows.Forms.TabPage
|
|
$tabTemplates.Text = T "tab.templates"
|
|
$tabTemplates.BackColor = [System.Drawing.Color]::WhiteSmoke
|
|
|
|
$lblTplDesc = New-Object System.Windows.Forms.Label
|
|
$lblTplDesc.Text = T "tpl.desc"
|
|
$lblTplDesc.Location = New-Object System.Drawing.Point(10, 18)
|
|
$lblTplDesc.Size = New-Object System.Drawing.Size(580, 20)
|
|
$lblTplDesc.ForeColor = [System.Drawing.Color]::DimGray
|
|
|
|
$lblTplCount = New-Object System.Windows.Forms.Label
|
|
$lblTplCount.Name = "lblTplCount"
|
|
$lblTplCount.Location = New-Object System.Drawing.Point(10, 44)
|
|
$lblTplCount.Size = New-Object System.Drawing.Size(380, 20)
|
|
$lblTplCount.ForeColor = [System.Drawing.Color]::DimGray
|
|
$lblTplCount.Font = New-Object System.Drawing.Font("Segoe UI", 8, [System.Drawing.FontStyle]::Italic)
|
|
|
|
$btnOpenTplMgr = New-Object System.Windows.Forms.Button
|
|
$btnOpenTplMgr.Text = T "btn.manage.tpl"
|
|
$btnOpenTplMgr.Location = New-Object System.Drawing.Point(10, 72)
|
|
$btnOpenTplMgr.Size = New-Object System.Drawing.Size(185, 34)
|
|
$btnOpenTplMgr.BackColor = [System.Drawing.Color]::FromArgb(50, 50, 120)
|
|
$btnOpenTplMgr.ForeColor = [System.Drawing.Color]::White
|
|
$btnOpenTplMgr.FlatStyle = "Flat"
|
|
$btnOpenTplMgr.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
|
|
|
|
$tabTemplates.Controls.AddRange(@($lblTplDesc, $lblTplCount, $btnOpenTplMgr))
|
|
|
|
# ══ Tab 4: Recherche de fichiers ══════════════════════════════════════════════
|
|
$tabSearch = New-Object System.Windows.Forms.TabPage
|
|
$tabSearch.Text = T "tab.search"
|
|
$tabSearch.BackColor = [System.Drawing.Color]::WhiteSmoke
|
|
|
|
# ── GroupBox Filtres ───────────────────────────────────────────────────────────
|
|
$grpSearchFilters = New-Group (T "grp.search.filters") 10 6 620 170
|
|
|
|
# Row 1 - Extension & Regex
|
|
$lblSrchExt = New-Object System.Windows.Forms.Label
|
|
$lblSrchExt.Text = T "lbl.extensions"
|
|
$lblSrchExt.Location = New-Object System.Drawing.Point(10, 24)
|
|
$lblSrchExt.Size = New-Object System.Drawing.Size(88, 22)
|
|
$lblSrchExt.TextAlign = "MiddleLeft"
|
|
$txtSrchExt = New-Object System.Windows.Forms.TextBox
|
|
$txtSrchExt.Location = New-Object System.Drawing.Point(100, 24)
|
|
$txtSrchExt.Size = New-Object System.Drawing.Size(120, 22)
|
|
$txtSrchExt.Font = New-Object System.Drawing.Font("Consolas", 9)
|
|
$txtSrchExt.PlaceholderText = T "ph.extensions"
|
|
|
|
$lblSrchRegex = New-Object System.Windows.Forms.Label
|
|
$lblSrchRegex.Text = T "lbl.regex"
|
|
$lblSrchRegex.Location = New-Object System.Drawing.Point(232, 24)
|
|
$lblSrchRegex.Size = New-Object System.Drawing.Size(88, 22)
|
|
$lblSrchRegex.TextAlign = "MiddleLeft"
|
|
$txtSrchRegex = New-Object System.Windows.Forms.TextBox
|
|
$txtSrchRegex.Location = New-Object System.Drawing.Point(322, 24)
|
|
$txtSrchRegex.Size = New-Object System.Drawing.Size(286, 22)
|
|
$txtSrchRegex.Font = New-Object System.Drawing.Font("Consolas", 9)
|
|
$txtSrchRegex.PlaceholderText = T "ph.regex"
|
|
|
|
# Row 2 - Created dates
|
|
$chkSrchCrA = New-Object System.Windows.Forms.CheckBox
|
|
$chkSrchCrA.Text = T "chk.created.after"
|
|
$chkSrchCrA.Location = New-Object System.Drawing.Point(10, 52)
|
|
$chkSrchCrA.Size = New-Object System.Drawing.Size(108, 22)
|
|
$dtpSrchCrA = New-Object System.Windows.Forms.DateTimePicker
|
|
$dtpSrchCrA.Location = New-Object System.Drawing.Point(120, 52)
|
|
$dtpSrchCrA.Size = New-Object System.Drawing.Size(130, 22)
|
|
$dtpSrchCrA.Format = [System.Windows.Forms.DateTimePickerFormat]::Short
|
|
$dtpSrchCrA.Enabled = $false
|
|
|
|
$chkSrchCrB = New-Object System.Windows.Forms.CheckBox
|
|
$chkSrchCrB.Text = T "chk.created.before"
|
|
$chkSrchCrB.Location = New-Object System.Drawing.Point(262, 52)
|
|
$chkSrchCrB.Size = New-Object System.Drawing.Size(108, 22)
|
|
$dtpSrchCrB = New-Object System.Windows.Forms.DateTimePicker
|
|
$dtpSrchCrB.Location = New-Object System.Drawing.Point(372, 52)
|
|
$dtpSrchCrB.Size = New-Object System.Drawing.Size(130, 22)
|
|
$dtpSrchCrB.Format = [System.Windows.Forms.DateTimePickerFormat]::Short
|
|
$dtpSrchCrB.Enabled = $false
|
|
|
|
$chkSrchCrA.Add_CheckedChanged({ $dtpSrchCrA.Enabled = $chkSrchCrA.Checked })
|
|
$chkSrchCrB.Add_CheckedChanged({ $dtpSrchCrB.Enabled = $chkSrchCrB.Checked })
|
|
|
|
# Row 3 - Modified dates
|
|
$chkSrchModA = New-Object System.Windows.Forms.CheckBox
|
|
$chkSrchModA.Text = T "chk.modified.after"
|
|
$chkSrchModA.Location = New-Object System.Drawing.Point(10, 80)
|
|
$chkSrchModA.Size = New-Object System.Drawing.Size(108, 22)
|
|
$dtpSrchModA = New-Object System.Windows.Forms.DateTimePicker
|
|
$dtpSrchModA.Location = New-Object System.Drawing.Point(120, 80)
|
|
$dtpSrchModA.Size = New-Object System.Drawing.Size(130, 22)
|
|
$dtpSrchModA.Format = [System.Windows.Forms.DateTimePickerFormat]::Short
|
|
$dtpSrchModA.Enabled = $false
|
|
|
|
$chkSrchModB = New-Object System.Windows.Forms.CheckBox
|
|
$chkSrchModB.Text = T "chk.modified.before"
|
|
$chkSrchModB.Location = New-Object System.Drawing.Point(262, 80)
|
|
$chkSrchModB.Size = New-Object System.Drawing.Size(108, 22)
|
|
$dtpSrchModB = New-Object System.Windows.Forms.DateTimePicker
|
|
$dtpSrchModB.Location = New-Object System.Drawing.Point(372, 80)
|
|
$dtpSrchModB.Size = New-Object System.Drawing.Size(130, 22)
|
|
$dtpSrchModB.Format = [System.Windows.Forms.DateTimePickerFormat]::Short
|
|
$dtpSrchModB.Enabled = $false
|
|
|
|
$chkSrchModA.Add_CheckedChanged({ $dtpSrchModA.Enabled = $chkSrchModA.Checked })
|
|
$chkSrchModB.Add_CheckedChanged({ $dtpSrchModB.Enabled = $chkSrchModB.Checked })
|
|
|
|
# Row 4 - Created by / Modified by
|
|
$lblSrchCrBy = New-Object System.Windows.Forms.Label
|
|
$lblSrchCrBy.Text = T "lbl.created.by"
|
|
$lblSrchCrBy.Location = New-Object System.Drawing.Point(10, 108)
|
|
$lblSrchCrBy.Size = New-Object System.Drawing.Size(70, 22)
|
|
$lblSrchCrBy.TextAlign = "MiddleLeft"
|
|
$txtSrchCrBy = New-Object System.Windows.Forms.TextBox
|
|
$txtSrchCrBy.Location = New-Object System.Drawing.Point(82, 108)
|
|
$txtSrchCrBy.Size = New-Object System.Drawing.Size(168, 22)
|
|
$txtSrchCrBy.PlaceholderText = T "ph.created.by"
|
|
|
|
$lblSrchModBy = New-Object System.Windows.Forms.Label
|
|
$lblSrchModBy.Text = T "lbl.modified.by"
|
|
$lblSrchModBy.Location = New-Object System.Drawing.Point(262, 108)
|
|
$lblSrchModBy.Size = New-Object System.Drawing.Size(82, 22)
|
|
$lblSrchModBy.TextAlign = "MiddleLeft"
|
|
$txtSrchModBy = New-Object System.Windows.Forms.TextBox
|
|
$txtSrchModBy.Location = New-Object System.Drawing.Point(346, 108)
|
|
$txtSrchModBy.Size = New-Object System.Drawing.Size(168, 22)
|
|
$txtSrchModBy.PlaceholderText = T "ph.modified.by"
|
|
|
|
# Row 5 - Library filter
|
|
$lblSrchLib = New-Object System.Windows.Forms.Label
|
|
$lblSrchLib.Text = T "lbl.library"
|
|
$lblSrchLib.Location = New-Object System.Drawing.Point(10, 136)
|
|
$lblSrchLib.Size = New-Object System.Drawing.Size(88, 22)
|
|
$lblSrchLib.TextAlign = "MiddleLeft"
|
|
$txtSrchLib = New-Object System.Windows.Forms.TextBox
|
|
$txtSrchLib.Location = New-Object System.Drawing.Point(100, 136)
|
|
$txtSrchLib.Size = New-Object System.Drawing.Size(508, 22)
|
|
$txtSrchLib.PlaceholderText = T "ph.library"
|
|
|
|
$grpSearchFilters.Controls.AddRange(@(
|
|
$lblSrchExt, $txtSrchExt, $lblSrchRegex, $txtSrchRegex,
|
|
$chkSrchCrA, $dtpSrchCrA, $chkSrchCrB, $dtpSrchCrB,
|
|
$chkSrchModA, $dtpSrchModA, $chkSrchModB, $dtpSrchModB,
|
|
$lblSrchCrBy, $txtSrchCrBy, $lblSrchModBy, $txtSrchModBy,
|
|
$lblSrchLib, $txtSrchLib
|
|
))
|
|
|
|
# ── GroupBox Format ────────────────────────────────────────────────────────────
|
|
$grpSearchFmt = New-Group (T "grp.search.fmt") 10 180 620 48
|
|
$radSrchCSV = New-Radio (T "rad.csv.perms") 15 22 130 $true
|
|
$radSrchHTML = New-Radio (T "rad.html.perms") 160 22 180
|
|
$lblSrchMax = New-Object System.Windows.Forms.Label
|
|
$lblSrchMax.Text = T "lbl.max.results"
|
|
$lblSrchMax.Location = New-Object System.Drawing.Point(360, 22)
|
|
$lblSrchMax.Size = New-Object System.Drawing.Size(96, 22)
|
|
$lblSrchMax.TextAlign = "MiddleLeft"
|
|
$nudSrchMax = New-Object System.Windows.Forms.NumericUpDown
|
|
$nudSrchMax.Location = New-Object System.Drawing.Point(458, 22)
|
|
$nudSrchMax.Size = New-Object System.Drawing.Size(70, 22)
|
|
$nudSrchMax.Minimum = 10
|
|
$nudSrchMax.Maximum = 50000
|
|
$nudSrchMax.Value = 500
|
|
$nudSrchMax.Increment = 100
|
|
$grpSearchFmt.Controls.AddRange(@($radSrchCSV, $radSrchHTML, $lblSrchMax, $nudSrchMax))
|
|
|
|
# ── Buttons ────────────────────────────────────────────────────────────────────
|
|
$btnSearch = New-ActionBtn (T "btn.run.search") 10 232 ([System.Drawing.Color]::FromArgb(0, 120, 212))
|
|
$btnOpenSearch = New-Object System.Windows.Forms.Button
|
|
$btnOpenSearch.Text = T "btn.open.search"
|
|
$btnOpenSearch.Location = New-Object System.Drawing.Point(175, 232)
|
|
$btnOpenSearch.Size = New-Object System.Drawing.Size(130, 34)
|
|
$btnOpenSearch.Enabled = $false
|
|
|
|
$tabSearch.Controls.AddRange(@($grpSearchFilters, $grpSearchFmt, $btnSearch, $btnOpenSearch))
|
|
|
|
# ══ Tab 5: Doublons ═══════════════════════════════════════════════════════════
|
|
$tabDupes = New-Object System.Windows.Forms.TabPage
|
|
$tabDupes.Text = T "tab.dupes"
|
|
$tabDupes.BackColor = [System.Drawing.Color]::WhiteSmoke
|
|
|
|
# ── GroupBox: Type de doublons (y=4, h=44 → bottom 48) ──────────────────────
|
|
$grpDupType = New-Group (T "grp.dup.type") 10 4 638 44
|
|
$radDupFiles = New-Radio (T "rad.dup.files") 10 16 190 $true
|
|
$radDupFolders = New-Radio (T "rad.dup.folders") 210 16 190
|
|
$grpDupType.Controls.AddRange(@($radDupFiles, $radDupFolders))
|
|
|
|
# ── GroupBox: Critères de comparaison (y=52, h=88 → bottom 140) ─────────────
|
|
$grpDupCrit = New-Group (T "grp.dup.criteria") 10 52 638 88
|
|
|
|
$lblDupNote = New-Object System.Windows.Forms.Label
|
|
$lblDupNote.Text = T "lbl.dup.note"
|
|
$lblDupNote.Location = New-Object System.Drawing.Point(10, 15)
|
|
$lblDupNote.Size = New-Object System.Drawing.Size(610, 16)
|
|
$lblDupNote.Font = New-Object System.Drawing.Font("Segoe UI", 8, [System.Drawing.FontStyle]::Italic)
|
|
$lblDupNote.ForeColor = [System.Drawing.Color]::DimGray
|
|
|
|
# Row 1 - criteres communs
|
|
$chkDupSize = New-Check (T "chk.dup.size") 10 34 148 $true
|
|
$chkDupCreated = New-Check (T "chk.dup.created") 164 34 208
|
|
$chkDupModified = New-Check (T "chk.dup.modified") 378 34 226
|
|
|
|
# Row 2 - criteres dossiers uniquement
|
|
$chkDupSubCount = New-Check (T "chk.dup.subfolders") 10 60 210
|
|
$chkDupFileCount = New-Check (T "chk.dup.filecount") 226 60 200
|
|
$chkDupSubCount.Enabled = $false
|
|
$chkDupFileCount.Enabled = $false
|
|
|
|
$grpDupCrit.Controls.AddRange(@($lblDupNote,
|
|
$chkDupSize, $chkDupCreated, $chkDupModified,
|
|
$chkDupSubCount, $chkDupFileCount))
|
|
|
|
# Toggle folder-only criteria based on radio selection
|
|
$radDupFiles.Add_CheckedChanged({
|
|
$chkDupSubCount.Enabled = -not $radDupFiles.Checked
|
|
$chkDupFileCount.Enabled = -not $radDupFiles.Checked
|
|
if ($radDupFiles.Checked) { $chkDupSubCount.Checked = $false; $chkDupFileCount.Checked = $false }
|
|
})
|
|
$radDupFolders.Add_CheckedChanged({
|
|
$chkDupSubCount.Enabled = $radDupFolders.Checked
|
|
$chkDupFileCount.Enabled = $radDupFolders.Checked
|
|
})
|
|
|
|
# ── GroupBox: Options (y=144, h=44 → bottom 188) ─────────────────────────────
|
|
$grpDupOpts = New-Group (T "grp.options") 10 144 638 44
|
|
$chkDupSubsites = New-Check (T "chk.include.subsites") 10 18 192
|
|
$lblDupLib = New-Object System.Windows.Forms.Label
|
|
$lblDupLib.Text = T "lbl.library"
|
|
$lblDupLib.Location = New-Object System.Drawing.Point(210, 18)
|
|
$lblDupLib.Size = New-Object System.Drawing.Size(88, 22)
|
|
$lblDupLib.TextAlign = "MiddleLeft"
|
|
$txtDupLib = New-Object System.Windows.Forms.TextBox
|
|
$txtDupLib.Location = New-Object System.Drawing.Point(300, 18)
|
|
$txtDupLib.Size = New-Object System.Drawing.Size(326, 22)
|
|
$txtDupLib.PlaceholderText = T "ph.dup.lib"
|
|
$grpDupOpts.Controls.AddRange(@($chkDupSubsites, $lblDupLib, $txtDupLib))
|
|
|
|
# ── GroupBox: Format (y=192, h=40 → bottom 232) ──────────────────────────────
|
|
$grpDupFmt = New-Group (T "grp.export.fmt") 10 192 638 40
|
|
$radDupCSV = New-Radio (T "rad.csv.perms") 10 16 130 $true
|
|
$radDupHTML = New-Radio (T "rad.html.perms") 155 16 200
|
|
$grpDupFmt.Controls.AddRange(@($radDupCSV, $radDupHTML))
|
|
|
|
# ── Buttons (y=236 → bottom 270, within 284px inner) ─────────────────────────
|
|
$btnScanDupes = New-ActionBtn (T "btn.run.scan") 10 236 ([System.Drawing.Color]::FromArgb(136, 0, 21))
|
|
$btnOpenDupes = New-Object System.Windows.Forms.Button
|
|
$btnOpenDupes.Text = T "btn.open.results"
|
|
$btnOpenDupes.Location = New-Object System.Drawing.Point(175, 236)
|
|
$btnOpenDupes.Size = New-Object System.Drawing.Size(130, 34)
|
|
$btnOpenDupes.Enabled = $false
|
|
|
|
$tabDupes.Controls.AddRange(@($grpDupType, $grpDupCrit, $grpDupOpts, $grpDupFmt, $btnScanDupes, $btnOpenDupes))
|
|
|
|
$tabs.TabPages.AddRange(@($tabPerms, $tabStorage, $tabTemplates, $tabSearch, $tabDupes))
|
|
|
|
# ── Progress bar ───────────────────────────────────────────────────────────────
|
|
$progressBar = New-Object System.Windows.Forms.ProgressBar
|
|
$progressBar.Location = New-Object System.Drawing.Point(20, 540)
|
|
$progressBar.Size = New-Object System.Drawing.Size(642, 16)
|
|
$progressBar.Style = "Marquee"
|
|
$progressBar.MarqueeAnimationSpeed = 0
|
|
|
|
# ── Log ────────────────────────────────────────────────────────────────────────
|
|
$lblLog = New-Object System.Windows.Forms.Label
|
|
$lblLog.Text = T "lbl.log"
|
|
$lblLog.Location = New-Object System.Drawing.Point(20, 564)
|
|
$lblLog.Size = New-Object System.Drawing.Size(60, 20)
|
|
|
|
$txtLog = New-Object System.Windows.Forms.RichTextBox
|
|
$txtLog.Location = New-Object System.Drawing.Point(20, 584)
|
|
$txtLog.Size = New-Object System.Drawing.Size(642, 208)
|
|
$txtLog.ReadOnly = $true
|
|
$txtLog.BackColor = [System.Drawing.Color]::Black
|
|
$txtLog.ForeColor = [System.Drawing.Color]::LightGreen
|
|
$txtLog.Font = New-Object System.Drawing.Font("Consolas", 9)
|
|
$txtLog.ScrollBars = "Vertical"
|
|
|
|
$script:LogBox = $txtLog
|
|
$script:txtClientId = $txtClientId
|
|
$script:txtSiteURL = $txtSiteURL
|
|
$script:txtTenantUrl = $txtTenantUrl
|
|
$script:cboProfile = $cboProfile
|
|
$script:btnBrowseSites = $btnBrowseSites
|
|
$script:Profiles = @()
|
|
$script:SelectedSites = @()
|
|
|
|
$form.Controls.AddRange(@(
|
|
$menuStrip,
|
|
$lblProfile, $cboProfile,
|
|
$btnProfileNew, $btnProfileSave, $btnProfileRename, $btnProfileDelete,
|
|
$lblTenantUrl, $txtTenantUrl, $btnBrowseSites,
|
|
$lblClientId, $txtClientId,
|
|
$lblSiteURL, $txtSiteURL,
|
|
$lblOutput, $txtOutput, $btnBrowse,
|
|
$sep, $tabs,
|
|
$progressBar,
|
|
$lblLog, $txtLog
|
|
))
|
|
|
|
# ── i18n control registration ──────────────────────────────────────────────────
|
|
$script:i18nMap = [System.Collections.Generic.Dictionary[string,object]]::new()
|
|
$script:i18nTabs = [System.Collections.Generic.Dictionary[string,object]]::new()
|
|
$script:i18nMenus = [System.Collections.Generic.Dictionary[string,object]]::new()
|
|
|
|
$_reg = {
|
|
param($dict, $ctrl, $key)
|
|
$dict[[System.Guid]::NewGuid().ToString()] = [PSCustomObject]@{ Control = $ctrl; Key = $key }
|
|
}
|
|
|
|
# Main labels & buttons
|
|
& $_reg $script:i18nMap $lblProfile "profile"
|
|
& $_reg $script:i18nMap $btnProfileNew "btn.new"
|
|
& $_reg $script:i18nMap $btnProfileSave "btn.save"
|
|
& $_reg $script:i18nMap $btnProfileRename "btn.rename"
|
|
& $_reg $script:i18nMap $btnProfileDelete "btn.delete"
|
|
& $_reg $script:i18nMap $btnBrowseSites "btn.view.sites"
|
|
& $_reg $script:i18nMap $lblTenantUrl "tenant.url"
|
|
& $_reg $script:i18nMap $lblClientId "client.id"
|
|
& $_reg $script:i18nMap $lblSiteURL "site.url"
|
|
& $_reg $script:i18nMap $lblOutput "output.folder"
|
|
& $_reg $script:i18nMap $btnBrowse "btn.browse"
|
|
& $_reg $script:i18nMap $lblLog "lbl.log"
|
|
|
|
# Permissions tab controls
|
|
& $_reg $script:i18nMap $grpPermOpts "grp.scan.opts"
|
|
& $_reg $script:i18nMap $chkScanFolders "chk.scan.folders"
|
|
& $_reg $script:i18nMap $chkRecursive "chk.recursive"
|
|
& $_reg $script:i18nMap $lblPermDepth "lbl.folder.depth"
|
|
& $_reg $script:i18nMap $chkPermMaxDepth "chk.max.depth"
|
|
& $_reg $script:i18nMap $chkInheritedPerms "chk.inherited.perms"
|
|
& $_reg $script:i18nMap $grpPermFmt "grp.export.fmt"
|
|
& $_reg $script:i18nMap $radPermCSV "rad.csv.perms"
|
|
& $_reg $script:i18nMap $radPermHTML "rad.html.perms"
|
|
& $_reg $script:i18nMap $btnGenPerms "btn.gen.perms"
|
|
& $_reg $script:i18nMap $btnOpenPerms "btn.open.perms"
|
|
|
|
# Storage tab controls
|
|
& $_reg $script:i18nMap $grpStorOpts "grp.scan.opts"
|
|
& $_reg $script:i18nMap $chkStorPerLib "chk.per.lib"
|
|
& $_reg $script:i18nMap $chkStorSubsites "chk.subsites"
|
|
& $_reg $script:i18nMap $lblDepth "lbl.folder.depth"
|
|
& $_reg $script:i18nMap $chkMaxDepth "chk.max.depth"
|
|
& $_reg $script:i18nMap $lblStorNote "stor.note"
|
|
& $_reg $script:i18nMap $grpStorFmt "grp.export.fmt"
|
|
& $_reg $script:i18nMap $radStorCSV "rad.csv.perms"
|
|
& $_reg $script:i18nMap $radStorHTML "rad.html.perms"
|
|
& $_reg $script:i18nMap $btnGenStorage "btn.gen.storage"
|
|
& $_reg $script:i18nMap $btnOpenStorage "btn.open.storage"
|
|
|
|
# Templates tab controls
|
|
& $_reg $script:i18nMap $lblTplDesc "tpl.desc"
|
|
& $_reg $script:i18nMap $btnOpenTplMgr "btn.manage.tpl"
|
|
|
|
# Search tab controls
|
|
& $_reg $script:i18nMap $grpSearchFilters "grp.search.filters"
|
|
& $_reg $script:i18nMap $lblSrchExt "lbl.extensions"
|
|
& $_reg $script:i18nMap $lblSrchRegex "lbl.regex"
|
|
& $_reg $script:i18nMap $chkSrchCrA "chk.created.after"
|
|
& $_reg $script:i18nMap $chkSrchCrB "chk.created.before"
|
|
& $_reg $script:i18nMap $chkSrchModA "chk.modified.after"
|
|
& $_reg $script:i18nMap $chkSrchModB "chk.modified.before"
|
|
& $_reg $script:i18nMap $lblSrchCrBy "lbl.created.by"
|
|
& $_reg $script:i18nMap $lblSrchModBy "lbl.modified.by"
|
|
& $_reg $script:i18nMap $lblSrchLib "lbl.library"
|
|
& $_reg $script:i18nMap $grpSearchFmt "grp.search.fmt"
|
|
& $_reg $script:i18nMap $lblSrchMax "lbl.max.results"
|
|
& $_reg $script:i18nMap $btnSearch "btn.run.search"
|
|
& $_reg $script:i18nMap $btnOpenSearch "btn.open.search"
|
|
|
|
# Duplicates tab controls
|
|
& $_reg $script:i18nMap $grpDupType "grp.dup.type"
|
|
& $_reg $script:i18nMap $radDupFiles "rad.dup.files"
|
|
& $_reg $script:i18nMap $radDupFolders "rad.dup.folders"
|
|
& $_reg $script:i18nMap $grpDupCrit "grp.dup.criteria"
|
|
& $_reg $script:i18nMap $lblDupNote "lbl.dup.note"
|
|
& $_reg $script:i18nMap $chkDupSize "chk.dup.size"
|
|
& $_reg $script:i18nMap $chkDupCreated "chk.dup.created"
|
|
& $_reg $script:i18nMap $chkDupModified "chk.dup.modified"
|
|
& $_reg $script:i18nMap $chkDupSubCount "chk.dup.subfolders"
|
|
& $_reg $script:i18nMap $chkDupFileCount "chk.dup.filecount"
|
|
& $_reg $script:i18nMap $grpDupOpts "grp.options"
|
|
& $_reg $script:i18nMap $chkDupSubsites "chk.include.subsites"
|
|
& $_reg $script:i18nMap $lblDupLib "lbl.library"
|
|
& $_reg $script:i18nMap $btnScanDupes "btn.run.scan"
|
|
& $_reg $script:i18nMap $btnOpenDupes "btn.open.results"
|
|
|
|
# Tab pages
|
|
& $_reg $script:i18nTabs $tabPerms "tab.perms"
|
|
& $_reg $script:i18nTabs $tabStorage "tab.storage"
|
|
& $_reg $script:i18nTabs $tabTemplates "tab.templates"
|
|
& $_reg $script:i18nTabs $tabSearch "tab.search"
|
|
& $_reg $script:i18nTabs $tabDupes "tab.dupes"
|
|
|
|
# Menu items
|
|
& $_reg $script:i18nMenus $menuSettings "menu.settings"
|
|
& $_reg $script:i18nMenus $menuJsonFolder "menu.json.folder"
|
|
& $_reg $script:i18nMenus $menuLang "menu.language"
|
|
|
|
# Placeholder texts
|
|
$script:i18nPlaceholders = [System.Collections.Generic.Dictionary[string,object]]::new()
|
|
& $_reg $script:i18nPlaceholders $txtSrchExt "ph.extensions"
|
|
& $_reg $script:i18nPlaceholders $txtSrchRegex "ph.regex"
|
|
& $_reg $script:i18nPlaceholders $txtSrchCrBy "ph.created.by"
|
|
& $_reg $script:i18nPlaceholders $txtSrchModBy "ph.modified.by"
|
|
& $_reg $script:i18nPlaceholders $txtSrchLib "ph.library"
|
|
& $_reg $script:i18nPlaceholders $txtDupLib "ph.dup.lib"
|
|
|
|
#endregion
|
|
|
|
#region ===== Event Handlers =====
|
|
|
|
# ── Profile Management ─────────────────────────────────────────────────────────
|
|
$cboProfile.Add_SelectedIndexChanged({
|
|
$idx = $cboProfile.SelectedIndex
|
|
Apply-Profile -idx $idx
|
|
})
|
|
|
|
$btnProfileNew.Add_Click({
|
|
$name = Show-InputDialog -Prompt "Nom du profil :" -Title "Nouveau profil" -Default "Nouveau profil" -Owner $form
|
|
if ([string]::IsNullOrWhiteSpace($name)) { return }
|
|
$newProfile = [PSCustomObject]@{
|
|
name = $name
|
|
clientId = $txtClientId.Text.Trim()
|
|
tenantUrl = $txtTenantUrl.Text.Trim()
|
|
}
|
|
$list = @($script:Profiles) + $newProfile
|
|
Save-Profiles -Profiles $list
|
|
Refresh-ProfileList
|
|
$idx = $cboProfile.Items.IndexOf($name)
|
|
if ($idx -ge 0) { $cboProfile.SelectedIndex = $idx }
|
|
})
|
|
|
|
$btnProfileSave.Add_Click({
|
|
$idx = $cboProfile.SelectedIndex
|
|
if ($idx -lt 0) {
|
|
[System.Windows.Forms.MessageBox]::Show("Selectionnez d'abord un profil ou creez-en un nouveau.", "Aucun profil selectionne", "OK", "Warning")
|
|
return
|
|
}
|
|
$script:Profiles[$idx].clientId = $txtClientId.Text.Trim()
|
|
if (-not $script:Profiles[$idx].PSObject.Properties['tenantUrl']) {
|
|
$script:Profiles[$idx] | Add-Member -NotePropertyName tenantUrl -NotePropertyValue ""
|
|
}
|
|
$script:Profiles[$idx].tenantUrl = $txtTenantUrl.Text.Trim()
|
|
Save-Profiles -Profiles $script:Profiles
|
|
[System.Windows.Forms.MessageBox]::Show("Profil '$($script:Profiles[$idx].name)' sauvegarde.", "Sauvegarde", "OK", "Information")
|
|
})
|
|
|
|
$btnProfileRename.Add_Click({
|
|
$idx = $cboProfile.SelectedIndex
|
|
if ($idx -lt 0) { return }
|
|
$oldName = $script:Profiles[$idx].name
|
|
$newName = Show-InputDialog -Prompt "Nouveau nom du profil :" -Title "Renommer le profil" -Default $oldName -Owner $form
|
|
if ([string]::IsNullOrWhiteSpace($newName) -or $newName -eq $oldName) { return }
|
|
$script:Profiles[$idx].name = $newName
|
|
Save-Profiles -Profiles $script:Profiles
|
|
Refresh-ProfileList
|
|
$idx2 = $cboProfile.Items.IndexOf($newName)
|
|
if ($idx2 -ge 0) { $cboProfile.SelectedIndex = $idx2 }
|
|
})
|
|
|
|
$btnProfileDelete.Add_Click({
|
|
$idx = $cboProfile.SelectedIndex
|
|
if ($idx -lt 0) { return }
|
|
$name = $script:Profiles[$idx].name
|
|
$res = [System.Windows.Forms.MessageBox]::Show("Supprimer le profil '$name' ?", "Confirmer la suppression", "YesNo", "Warning")
|
|
if ($res -ne "Yes") { return }
|
|
$list = @($script:Profiles | Where-Object { $_.name -ne $name })
|
|
Save-Profiles -Profiles $list
|
|
Refresh-ProfileList
|
|
})
|
|
|
|
$btnBrowse.Add_Click({
|
|
$dlg = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
$dlg.SelectedPath = $txtOutput.Text
|
|
if ($dlg.ShowDialog() -eq "OK") { $txtOutput.Text = $dlg.SelectedPath }
|
|
})
|
|
|
|
$menuJsonFolder.Add_Click({
|
|
$dlg = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
$dlg.Description = T "dlg.json.folder.desc"
|
|
$dlg.SelectedPath = if ($script:DataFolder -and (Test-Path $script:DataFolder)) {
|
|
$script:DataFolder
|
|
} else {
|
|
if ($PSScriptRoot) { $PSScriptRoot } else { $PWD.Path }
|
|
}
|
|
if ($dlg.ShowDialog() -ne "OK") { return }
|
|
$newDir = $dlg.SelectedPath
|
|
if (-not (Test-Path $newDir)) {
|
|
$msg = (T "dlg.folder.not.found") -f $newDir
|
|
$res = [System.Windows.Forms.MessageBox]::Show(
|
|
$msg, (T "dlg.folder.not.found.title"), "YesNo", "Question")
|
|
if ($res -eq "Yes") {
|
|
try { New-Item -ItemType Directory -Path $newDir | Out-Null }
|
|
catch {
|
|
[System.Windows.Forms.MessageBox]::Show(
|
|
$_.Exception.Message, "Error", "OK", "Error")
|
|
return
|
|
}
|
|
} else { return }
|
|
}
|
|
$script:DataFolder = $newDir
|
|
Save-Settings -DataFolder $newDir -Lang $script:CurrentLang
|
|
Refresh-ProfileList
|
|
$n = (Load-Templates).Count
|
|
$lblTplCount.Text = "$n $(T 'tpl.count')"
|
|
})
|
|
|
|
# ── Language menu handlers ─────────────────────────────────────────────────────
|
|
function Switch-AppLanguage([string]$code) {
|
|
Load-Language $code
|
|
Update-UILanguage
|
|
foreach ($mi in $menuLang.DropDownItems) {
|
|
if ($mi -is [System.Windows.Forms.ToolStripMenuItem]) {
|
|
$mi.Checked = ($mi.Tag -eq $script:CurrentLang)
|
|
}
|
|
}
|
|
Save-Settings -DataFolder $script:DataFolder -Lang $script:CurrentLang
|
|
$n = (Load-Templates).Count
|
|
$lblTplCount.Text = "$n $(T 'tpl.count')"
|
|
}
|
|
|
|
$menuLangEn.Add_Click({ Switch-AppLanguage "en" })
|
|
foreach ($mi in @($menuLang.DropDownItems | Where-Object { $_ -is [System.Windows.Forms.ToolStripMenuItem] -and $_.Tag -ne "en" })) {
|
|
$mi.Add_Click({ Switch-AppLanguage $args[0].Tag })
|
|
}
|
|
|
|
$btnBrowseSites.Add_Click({
|
|
$tenantUrl = $txtTenantUrl.Text.Trim()
|
|
$clientId = $txtClientId.Text.Trim()
|
|
if ([string]::IsNullOrWhiteSpace($tenantUrl)) {
|
|
[System.Windows.Forms.MessageBox]::Show(
|
|
"Veuillez renseigner le Tenant URL (ex: https://contoso.sharepoint.com).",
|
|
"Tenant URL manquant", "OK", "Warning")
|
|
return
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($clientId)) {
|
|
[System.Windows.Forms.MessageBox]::Show(
|
|
"Veuillez renseigner le Client ID.", "Client ID manquant", "OK", "Warning")
|
|
return
|
|
}
|
|
$selected = Show-SitePicker -TenantUrl $tenantUrl -ClientId $clientId -Owner $form
|
|
if ($selected -and $selected.Count -gt 0) {
|
|
$script:SelectedSites = @($selected)
|
|
# Populate Site URL with first selection for compatibility
|
|
$txtSiteURL.Text = $selected[0]
|
|
$n = $selected.Count
|
|
$btnBrowseSites.Text = if ($n -eq 1) { "Voir les sites" } else { "Sites ($n)" }
|
|
}
|
|
})
|
|
|
|
# ── Permissions ────────────────────────────────────────────────────────────────
|
|
$btnGenPerms.Add_Click({
|
|
if (-not (Validate-Inputs)) { return }
|
|
|
|
# Build site list: picker selection takes priority, else txtSiteURL
|
|
$siteUrls = if ($script:SelectedSites -and $script:SelectedSites.Count -gt 0) {
|
|
@($script:SelectedSites)
|
|
} else {
|
|
@($txtSiteURL.Text.Trim())
|
|
}
|
|
$siteUrls = @($siteUrls | Where-Object { $_ })
|
|
|
|
$script:pnpCiD = $txtClientId.Text.Trim()
|
|
$script:PermFormat = if ($radPermHTML.Checked) { "HTML" } else { "CSV" }
|
|
|
|
$script:PermFolderDepth = if (-not $chkScanFolders.Checked) { 0 }
|
|
elseif ($chkPermMaxDepth.Checked) { 999 }
|
|
else { [int]$nudPermDepth.Value }
|
|
|
|
$btnGenPerms.Enabled = $false
|
|
$btnOpenPerms.Enabled = $false
|
|
$txtLog.Clear()
|
|
$progressBar.MarqueeAnimationSpeed = 30
|
|
|
|
$depthLabel = if ($script:PermFolderDepth -ge 999) { "Maximum" } elseif ($script:PermFolderDepth -eq 0) { "N/A (folder scan off)" } else { $script:PermFolderDepth }
|
|
Write-Log "=== PERMISSIONS REPORT ===" "White"
|
|
Write-Log "Sites : $($siteUrls.Count)" "Gray"
|
|
Write-Log "Format : $($script:PermFormat)" "Gray"
|
|
Write-Log "Folder depth : $depthLabel" "Gray"
|
|
Write-Log ("-" * 52) "DarkGray"
|
|
|
|
$lastFile = $null
|
|
foreach ($SiteURL in $siteUrls) {
|
|
$SiteName = ($SiteURL -split '/')[-1]; if (!$SiteName) { $SiteName = "root" }
|
|
$csvPath = Join-Path $txtOutput.Text "$SiteName-Permissions.csv"
|
|
$script:PermOutputFile = $csvPath
|
|
$script:AllPermissions = @()
|
|
|
|
Write-Log ""
|
|
Write-Log "--- Site: $SiteURL" "White"
|
|
|
|
$params = @{ SiteURL = $SiteURL; ReportFile = $csvPath }
|
|
if ($chkScanFolders.Checked) { $params.ScanFolders = $true }
|
|
if ($chkRecursive.Checked) { $params.Recursive = $true }
|
|
if ($chkInheritedPerms.Checked) { $params.IncludeInheritedPermissions = $true }
|
|
|
|
try {
|
|
Generate-PnPSitePermissionRpt @params
|
|
Write-Log ("-" * 52) "DarkGray"
|
|
Write-Log "Done! $($script:AllPermissions.Count) entries -- Saved: $csvPath" "Cyan"
|
|
$lastFile = $csvPath
|
|
$btnOpenPerms.Enabled = $true
|
|
}
|
|
catch { Write-Log "Failed ($SiteURL): $($_.Exception.Message)" "Red" }
|
|
}
|
|
|
|
$script:PermOutputFile = $lastFile
|
|
$btnGenPerms.Enabled = $true
|
|
$progressBar.MarqueeAnimationSpeed = 0
|
|
})
|
|
|
|
$btnOpenPerms.Add_Click({
|
|
if ($script:PermOutputFile -and (Test-Path $script:PermOutputFile)) {
|
|
Start-Process $script:PermOutputFile
|
|
}
|
|
})
|
|
|
|
# ── Storage ────────────────────────────────────────────────────────────────────
|
|
|
|
# Background worker scriptblock (shared across all site scans)
|
|
$script:_StorBgWork = {
|
|
param($SiteURL, $ClientId, $InclSub, $PerLib, $FolderDepth, $Sync)
|
|
|
|
$script:_bgSync = $Sync
|
|
$script:_bgDepth = $FolderDepth
|
|
|
|
function BgLog([string]$msg, [string]$color = "LightGreen") {
|
|
$script:_bgSync.Queue.Enqueue([PSCustomObject]@{ Text = $msg; Color = $color })
|
|
}
|
|
function Format-Bytes([long]$b) {
|
|
if ($b -ge 1GB) { return "$([math]::Round($b/1GB,2)) GB" }
|
|
if ($b -ge 1MB) { return "$([math]::Round($b/1MB,1)) MB" }
|
|
if ($b -ge 1KB) { return "$([math]::Round($b/1KB,0)) KB" }
|
|
return "$b B"
|
|
}
|
|
function Collect-FolderStorage([string]$SiteRelUrl, [string]$WebBaseUrl, [int]$Depth) {
|
|
if ($Depth -ge $script:_bgDepth) { return @() }
|
|
$out = [System.Collections.Generic.List[object]]::new()
|
|
try {
|
|
$items = Get-PnPFolderItem -FolderSiteRelativeUrl $SiteRelUrl -ItemType Folder -ErrorAction SilentlyContinue
|
|
foreach ($fi in $items) {
|
|
$childUrl = "$SiteRelUrl/$($fi.Name)"
|
|
try {
|
|
$sm = Get-PnPFolderStorageMetric -FolderSiteRelativeUrl $childUrl -ErrorAction SilentlyContinue
|
|
$sub = Collect-FolderStorage -SiteRelUrl $childUrl -WebBaseUrl $WebBaseUrl -Depth ($Depth + 1)
|
|
$out.Add([PSCustomObject]@{
|
|
Name = $fi.Name
|
|
URL = "$($WebBaseUrl.TrimEnd('/'))/$childUrl"
|
|
ItemCount = $sm.TotalFileCount
|
|
SizeBytes = $sm.TotalSize
|
|
VersionSizeBytes = [math]::Max([long]0, $sm.TotalSize - $sm.TotalFileStreamSize)
|
|
LastModified = if ($sm.LastModified) { $sm.LastModified.ToString("dd/MM/yyyy HH:mm") } else { "N/A" }
|
|
SubFolders = $sub
|
|
})
|
|
} catch {}
|
|
}
|
|
} catch {}
|
|
return @($out)
|
|
}
|
|
$script:_bgResults = [System.Collections.Generic.List[object]]::new()
|
|
function Collect-WebStorage([string]$WebUrl, [bool]$PerLib, [bool]$InclSub, [string]$ClientId) {
|
|
Connect-PnPOnline -Url $WebUrl -Interactive -ClientId $ClientId
|
|
$web = Get-PnPWeb
|
|
$wUrl = $web.Url
|
|
$wSrl = $web.ServerRelativeUrl.TrimEnd('/')
|
|
BgLog "Site : $($web.Title)" "Yellow"
|
|
if ($PerLib) {
|
|
$lists = Get-PnPList | Where-Object { $_.BaseType -eq "DocumentLibrary" -and !$_.Hidden }
|
|
BgLog "`t$($lists.Count) bibliotheque(s) trouvee(s)" "Gray"
|
|
foreach ($list in $lists) {
|
|
$rf = Get-PnPProperty -ClientObject $list -Property RootFolder
|
|
try {
|
|
$srl = $rf.ServerRelativeUrl.Substring($wSrl.Length).TrimStart('/')
|
|
$sm = Get-PnPFolderStorageMetric -FolderSiteRelativeUrl $srl
|
|
$libUrl = "$($wUrl.TrimEnd('/'))/$srl"
|
|
$subs = Collect-FolderStorage -SiteRelUrl $srl -WebBaseUrl $wUrl -Depth 0
|
|
$verBytes = [math]::Max([long]0, $sm.TotalSize - $sm.TotalFileStreamSize)
|
|
$script:_bgResults.Add([PSCustomObject]@{
|
|
SiteTitle = $web.Title
|
|
SiteURL = $wUrl
|
|
Library = $list.Title
|
|
LibraryURL = $libUrl
|
|
ItemCount = $sm.TotalFileCount
|
|
SizeBytes = $sm.TotalSize
|
|
VersionSizeBytes = $verBytes
|
|
SizeMB = [math]::Round($sm.TotalSize / 1MB, 1)
|
|
VersionSizeMB = [math]::Round($verBytes / 1MB, 1)
|
|
SizeGB = [math]::Round($sm.TotalSize / 1GB, 3)
|
|
LastModified = if ($sm.LastModified) { $sm.LastModified.ToString("dd/MM/yyyy HH:mm") } else { "N/A" }
|
|
SubFolders = $subs
|
|
})
|
|
BgLog "`t $($list.Title): $(Format-Bytes $sm.TotalSize) [versions: $(Format-Bytes $verBytes)] ($($sm.TotalFileCount) files)" "Cyan"
|
|
} catch { BgLog "`t '$($list.Title)' skipped: $($_.Exception.Message)" "DarkGray" }
|
|
}
|
|
} else {
|
|
try {
|
|
$sm = Get-PnPFolderStorageMetric -FolderSiteRelativeUrl "/"
|
|
$verBytes = [math]::Max([long]0, $sm.TotalSize - $sm.TotalFileStreamSize)
|
|
$script:_bgResults.Add([PSCustomObject]@{
|
|
SiteTitle = $web.Title
|
|
SiteURL = $wUrl
|
|
Library = "(All Libraries)"
|
|
LibraryURL = $wUrl
|
|
ItemCount = $sm.TotalFileCount
|
|
SizeBytes = $sm.TotalSize
|
|
VersionSizeBytes = $verBytes
|
|
SizeMB = [math]::Round($sm.TotalSize / 1MB, 1)
|
|
VersionSizeMB = [math]::Round($verBytes / 1MB, 1)
|
|
SizeGB = [math]::Round($sm.TotalSize / 1GB, 3)
|
|
LastModified = if ($sm.LastModified) { $sm.LastModified.ToString("dd/MM/yyyy HH:mm") } else { "N/A" }
|
|
})
|
|
BgLog "`t$(Format-Bytes $sm.TotalSize) [versions: $(Format-Bytes $verBytes)] -- $($sm.TotalFileCount) files" "Cyan"
|
|
} catch { BgLog "`tIgnored: $($_.Exception.Message)" "DarkGray" }
|
|
}
|
|
if ($InclSub) {
|
|
$subwebs = Get-PnPSubWeb
|
|
foreach ($sub in $subwebs) {
|
|
BgLog "`tSubsite : $($sub.Title)" "Yellow"
|
|
Collect-WebStorage -WebUrl $sub.Url -PerLib $PerLib -InclSub $InclSub -ClientId $ClientId
|
|
Connect-PnPOnline -Url $WebUrl -Interactive -ClientId $ClientId
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
Import-Module PnP.PowerShell -ErrorAction Stop
|
|
Collect-WebStorage -WebUrl $SiteURL -PerLib $PerLib -InclSub $InclSub -ClientId $ClientId
|
|
$web = Get-PnPWeb
|
|
$Sync.WebTitle = $web.Title
|
|
$Sync.WebURL = $web.URL
|
|
$Sync.Data = @($script:_bgResults)
|
|
} catch {
|
|
$Sync.Error = $_.Exception.Message
|
|
BgLog "Error : $($_.Exception.Message)" "Red"
|
|
} finally {
|
|
$Sync.Done = $true
|
|
}
|
|
}
|
|
|
|
# Launches scan for next site in queue; called from button click and timer Done block
|
|
function Start-NextStorageScan {
|
|
if ($script:_StorSiteQueue.Count -eq 0) {
|
|
Write-Log "=== All sites processed ===" "Cyan"
|
|
$btnGenStorage.Enabled = $true
|
|
$progressBar.MarqueeAnimationSpeed = 0
|
|
return
|
|
}
|
|
|
|
$nextUrl = $script:_StorSiteQueue.Dequeue()
|
|
$siteName = ($nextUrl -split '/')[-1]; if (!$siteName) { $siteName = "root" }
|
|
$ext = if ($script:_StorFmt -eq "HTML") { ".html" } else { ".csv" }
|
|
$outFile = Join-Path $script:_StorOutFolder "$siteName-Storage$ext"
|
|
$script:_StorOut = $outFile
|
|
|
|
Write-Log ""
|
|
Write-Log "--- Site: $nextUrl" "White"
|
|
Write-Log " Output: $outFile" "Gray"
|
|
Write-Log ("-" * 52) "DarkGray"
|
|
|
|
$script:_StorSyn = [hashtable]::Synchronized(@{
|
|
Queue = [System.Collections.Generic.Queue[object]]::new()
|
|
Done = $false
|
|
Error = $null
|
|
Data = $null
|
|
WebTitle = ""
|
|
WebURL = ""
|
|
})
|
|
|
|
$rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
|
|
$rs.ApartmentState = "STA"; $rs.ThreadOptions = "ReuseThread"; $rs.Open()
|
|
$ps = [System.Management.Automation.PowerShell]::Create()
|
|
$ps.Runspace = $rs
|
|
[void]$ps.AddScript($script:_StorBgWork)
|
|
[void]$ps.AddArgument($nextUrl)
|
|
[void]$ps.AddArgument($script:_StorClientId)
|
|
[void]$ps.AddArgument($script:_StorInclSub)
|
|
[void]$ps.AddArgument($script:_StorPerLib)
|
|
[void]$ps.AddArgument($script:_StorDepth)
|
|
[void]$ps.AddArgument($script:_StorSyn)
|
|
$script:_StorRS = $rs
|
|
$script:_StorPS = $ps
|
|
$script:_StorHnd = $ps.BeginInvoke()
|
|
|
|
$script:_StorTimer = New-Object System.Windows.Forms.Timer
|
|
$script:_StorTimer.Interval = 200
|
|
$script:_StorTimer.Add_Tick({
|
|
while ($script:_StorSyn.Queue.Count -gt 0) {
|
|
$m = $script:_StorSyn.Queue.Dequeue()
|
|
Write-Log $m.Text $m.Color
|
|
}
|
|
if ($script:_StorSyn.Done) {
|
|
$script:_StorTimer.Stop(); $script:_StorTimer.Dispose()
|
|
while ($script:_StorSyn.Queue.Count -gt 0) {
|
|
$m = $script:_StorSyn.Queue.Dequeue(); Write-Log $m.Text $m.Color
|
|
}
|
|
try { [void]$script:_StorPS.EndInvoke($script:_StorHnd) } catch {}
|
|
try { $script:_StorRS.Close(); $script:_StorRS.Dispose() } catch {}
|
|
|
|
if ($script:_StorSyn.Error) {
|
|
Write-Log "Failed: $($script:_StorSyn.Error)" "Red"
|
|
} elseif ($script:_StorSyn.Data -and $script:_StorSyn.Data.Count -gt 0) {
|
|
$data = $script:_StorSyn.Data
|
|
Write-Log "Writing output..." "Yellow"
|
|
if ($script:_StorFmt -eq "HTML") {
|
|
Export-StorageToHTML -Data $data -SiteTitle $script:_StorSyn.WebTitle `
|
|
-SiteURL $script:_StorSyn.WebURL -OutputPath $script:_StorOut
|
|
} else {
|
|
$data | Select-Object SiteTitle,SiteURL,Library,ItemCount,SizeMB,VersionSizeMB,SizeGB,LastModified |
|
|
Export-Csv -Path $script:_StorOut -NoTypeInformation
|
|
}
|
|
Write-Log "Done! $($data.Count) libs -- $(Format-Bytes (($data | Measure-Object -Property SizeBytes -Sum).Sum))" "Cyan"
|
|
Write-Log "Saved: $script:_StorOut" "White"
|
|
$script:_StorLastOut = $script:_StorOut
|
|
$btnOpenStorage.Enabled = $true
|
|
} else {
|
|
Write-Log "No data -- check permissions or URL." "Orange"
|
|
}
|
|
# Process next site in queue (if any)
|
|
Start-NextStorageScan
|
|
}
|
|
})
|
|
$script:_StorTimer.Start()
|
|
}
|
|
|
|
$btnGenStorage.Add_Click({
|
|
if (-not (Validate-Inputs)) { return }
|
|
|
|
# Build site list: picker selection takes priority, else txtSiteURL
|
|
$siteUrls = if ($script:SelectedSites -and $script:SelectedSites.Count -gt 0) {
|
|
@($script:SelectedSites)
|
|
} else {
|
|
@($txtSiteURL.Text.Trim())
|
|
}
|
|
$siteUrls = @($siteUrls | Where-Object { $_ })
|
|
|
|
# Store common scan parameters as script vars (read by Start-NextStorageScan)
|
|
$script:_StorClientId = $txtClientId.Text.Trim()
|
|
$script:pnpCiD = $script:_StorClientId
|
|
$script:_StorInclSub = $chkStorSubsites.Checked
|
|
$script:_StorPerLib = $chkStorPerLib.Checked
|
|
$script:_StorDepth = if (-not $chkStorPerLib.Checked) { 0 } elseif ($chkMaxDepth.Checked) { 999 } else { [int]$nudDepth.Value }
|
|
$script:_StorFmt = if ($radStorHTML.Checked) { "HTML" } else { "CSV" }
|
|
$script:_StorOutFolder = $txtOutput.Text
|
|
$script:_StorLastOut = $null
|
|
|
|
# Build queue
|
|
$script:_StorSiteQueue = [System.Collections.Generic.Queue[string]]::new()
|
|
foreach ($u in $siteUrls) { $script:_StorSiteQueue.Enqueue($u) }
|
|
|
|
$btnGenStorage.Enabled = $false
|
|
$btnOpenStorage.Enabled = $false
|
|
$txtLog.Clear()
|
|
$progressBar.MarqueeAnimationSpeed = 30
|
|
|
|
$depthLabel = if ($script:_StorDepth -ge 999) { "Maximum" } elseif ($script:_StorDepth -eq 0) { "N/A" } else { $script:_StorDepth }
|
|
Write-Log "=== STORAGE METRICS ===" "White"
|
|
Write-Log "Sites : $($siteUrls.Count)" "Gray"
|
|
Write-Log "Format : $($script:_StorFmt)" "Gray"
|
|
Write-Log "Folder depth : $depthLabel" "Gray"
|
|
Write-Log ("-" * 52) "DarkGray"
|
|
|
|
Start-NextStorageScan
|
|
})
|
|
|
|
$btnOpenStorage.Add_Click({
|
|
$f = $script:_StorLastOut
|
|
if ($f -and (Test-Path $f)) { Start-Process $f }
|
|
})
|
|
|
|
# ── Templates ───────────────────────────────────────────────────────────────
|
|
$btnOpenTplMgr.Add_Click({
|
|
Show-TemplateManager `
|
|
-DefaultSiteUrl $txtSiteURL.Text.Trim() `
|
|
-ClientId $txtClientId.Text.Trim() `
|
|
-TenantUrl $txtTenantUrl.Text.Trim() `
|
|
-Owner $form
|
|
# Refresh count after dialog closes
|
|
$n = (Load-Templates).Count
|
|
$lblTplCount.Text = "$n template(s) enregistre(s) -- cliquez pour gerer"
|
|
})
|
|
|
|
# ── Recherche de fichiers ───────────────────────────────────────────────────
|
|
$btnSearch.Add_Click({
|
|
if (-not (Validate-Inputs)) { return }
|
|
|
|
$siteUrl = if ($script:SelectedSites -and $script:SelectedSites.Count -gt 0) {
|
|
$script:SelectedSites[0]
|
|
} else { $txtSiteURL.Text.Trim() }
|
|
|
|
if ([string]::IsNullOrWhiteSpace($siteUrl)) {
|
|
[System.Windows.Forms.MessageBox]::Show("Veuillez saisir une URL de site.", "URL manquante", "OK", "Warning")
|
|
return
|
|
}
|
|
|
|
# Validate regex before launching
|
|
$regexStr = $txtSrchRegex.Text.Trim()
|
|
if ($regexStr) {
|
|
try { [void][System.Text.RegularExpressions.Regex]::new($regexStr) }
|
|
catch {
|
|
[System.Windows.Forms.MessageBox]::Show(
|
|
"Expression reguliere invalide :`n$($_.Exception.Message)",
|
|
"Regex invalide", "OK", "Error")
|
|
return
|
|
}
|
|
}
|
|
|
|
$filters = @{
|
|
Extensions = @($txtSrchExt.Text.Trim() -split '[,\s]+' | Where-Object { $_ } | ForEach-Object { $_.TrimStart('.').ToLower() })
|
|
Regex = $regexStr
|
|
CreatedAfter = if ($chkSrchCrA.Checked) { $dtpSrchCrA.Value.Date } else { $null }
|
|
CreatedBefore = if ($chkSrchCrB.Checked) { $dtpSrchCrB.Value.Date } else { $null }
|
|
ModifiedAfter = if ($chkSrchModA.Checked) { $dtpSrchModA.Value.Date } else { $null }
|
|
ModifiedBefore= if ($chkSrchModB.Checked) { $dtpSrchModB.Value.Date } else { $null }
|
|
CreatedBy = $txtSrchCrBy.Text.Trim()
|
|
ModifiedBy = $txtSrchModBy.Text.Trim()
|
|
Library = $txtSrchLib.Text.Trim()
|
|
MaxResults = [int]$nudSrchMax.Value
|
|
Format = if ($radSrchHTML.Checked) { "HTML" } else { "CSV" }
|
|
OutFolder = $txtOutput.Text.Trim()
|
|
SiteUrl = $siteUrl
|
|
ClientId = $txtClientId.Text.Trim()
|
|
}
|
|
|
|
$btnSearch.Enabled = $false
|
|
$btnOpenSearch.Enabled = $false
|
|
$txtLog.Clear()
|
|
$progressBar.MarqueeAnimationSpeed = 30
|
|
Write-Log "=== RECHERCHE DE FICHIERS ===" "White"
|
|
Write-Log "Site : $siteUrl" "Gray"
|
|
if ($filters.Extensions.Count -gt 0) { Write-Log "Extensions : $($filters.Extensions -join ', ')" "Gray" }
|
|
if ($filters.Regex) { Write-Log "Regex : $($filters.Regex)" "Gray" }
|
|
if ($filters.CreatedAfter) { Write-Log "Cree apres : $($filters.CreatedAfter.ToString('dd/MM/yyyy'))" "Gray" }
|
|
if ($filters.CreatedBefore) { Write-Log "Cree avant : $($filters.CreatedBefore.ToString('dd/MM/yyyy'))" "Gray" }
|
|
if ($filters.ModifiedAfter) { Write-Log "Modifie apres: $($filters.ModifiedAfter.ToString('dd/MM/yyyy'))" "Gray" }
|
|
if ($filters.ModifiedBefore){ Write-Log "Modifie avant: $($filters.ModifiedBefore.ToString('dd/MM/yyyy'))" "Gray" }
|
|
if ($filters.CreatedBy) { Write-Log "Cree par : $($filters.CreatedBy)" "Gray" }
|
|
if ($filters.ModifiedBy) { Write-Log "Modifie par : $($filters.ModifiedBy)" "Gray" }
|
|
if ($filters.Library) { Write-Log "Bibliotheque : $($filters.Library)" "Gray" }
|
|
Write-Log "Max resultats: $($filters.MaxResults)" "Gray"
|
|
Write-Log ("-" * 52) "DarkGray"
|
|
|
|
$bgSearch = {
|
|
param($Filters, $Sync)
|
|
function BgLog([string]$m, [string]$c = "LightGreen") {
|
|
$Sync.Queue.Enqueue(@{ Text = $m; Color = $c })
|
|
}
|
|
try {
|
|
Import-Module PnP.PowerShell -ErrorAction Stop
|
|
Connect-PnPOnline -Url $Filters.SiteUrl -Interactive -ClientId $Filters.ClientId
|
|
|
|
# Build KQL query
|
|
$kqlParts = @("ContentType:Document")
|
|
if ($Filters.Extensions -and $Filters.Extensions.Count -gt 0) {
|
|
$extParts = $Filters.Extensions | ForEach-Object { "FileExtension:$_" }
|
|
$kqlParts += "($($extParts -join ' OR '))"
|
|
}
|
|
if ($Filters.CreatedAfter) { $kqlParts += "Created>=$($Filters.CreatedAfter.ToString('yyyy-MM-dd'))" }
|
|
if ($Filters.CreatedBefore) { $kqlParts += "Created<=$($Filters.CreatedBefore.ToString('yyyy-MM-dd'))" }
|
|
if ($Filters.ModifiedAfter) { $kqlParts += "Write>=$($Filters.ModifiedAfter.ToString('yyyy-MM-dd'))" }
|
|
if ($Filters.ModifiedBefore) { $kqlParts += "Write<=$($Filters.ModifiedBefore.ToString('yyyy-MM-dd'))" }
|
|
if ($Filters.CreatedBy) { $kqlParts += "Author:""$($Filters.CreatedBy)""" }
|
|
if ($Filters.ModifiedBy) { $kqlParts += "ModifiedBy:""$($Filters.ModifiedBy)""" }
|
|
if ($Filters.Library) {
|
|
$libPath = "$($Filters.SiteUrl.TrimEnd('/'))/$($Filters.Library.TrimStart('/'))"
|
|
$kqlParts += "Path:""$libPath*"""
|
|
}
|
|
|
|
$kql = $kqlParts -join " AND "
|
|
BgLog "Requete KQL : $kql" "Yellow"
|
|
$Sync.KQL = $kql
|
|
|
|
$selectProps = @("Title","Path","Author","LastModifiedTime","FileExtension","Created","ModifiedBy","Size")
|
|
$allResults = [System.Collections.Generic.List[object]]::new()
|
|
$startRow = 0
|
|
$batchSize = 500
|
|
|
|
do {
|
|
$batch = Submit-PnPSearchQuery -Query $kql `
|
|
-StartRow $startRow -MaxResults $batchSize `
|
|
-SelectProperties $selectProps -TrimDuplicates $false
|
|
$hits = @($batch.ResultRows)
|
|
foreach ($h in $hits) { $allResults.Add($h) }
|
|
BgLog " Lot $([math]::Floor($startRow/$batchSize)+1) : $($hits.Count) resultats (total: $($allResults.Count))" "Cyan"
|
|
$startRow += $batchSize
|
|
} while ($hits.Count -eq $batchSize -and $allResults.Count -lt $Filters.MaxResults)
|
|
|
|
# Client-side regex filter on file path/name
|
|
if ($Filters.Regex) {
|
|
$rx = [System.Text.RegularExpressions.Regex]::new(
|
|
$Filters.Regex,
|
|
[System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
|
|
$allResults = [System.Collections.Generic.List[object]]@(
|
|
$allResults | Where-Object { $rx.IsMatch($_.Path) }
|
|
)
|
|
BgLog " Apres filtre regex : $($allResults.Count) resultats" "Cyan"
|
|
}
|
|
|
|
# Cap at MaxResults
|
|
if ($allResults.Count -gt $Filters.MaxResults) {
|
|
$allResults = [System.Collections.Generic.List[object]]@(
|
|
$allResults | Select-Object -First $Filters.MaxResults
|
|
)
|
|
}
|
|
|
|
BgLog "$($allResults.Count) fichier(s) trouves" "LightGreen"
|
|
$Sync.Results = @($allResults)
|
|
} catch {
|
|
$Sync.Error = $_.Exception.Message
|
|
BgLog "Erreur : $($_.Exception.Message)" "Red"
|
|
} finally {
|
|
$Sync.Done = $true
|
|
}
|
|
}
|
|
|
|
$sync = [hashtable]::Synchronized(@{
|
|
Queue = [System.Collections.Generic.Queue[object]]::new()
|
|
Done = $false
|
|
Error = $null
|
|
Results = $null
|
|
KQL = ""
|
|
})
|
|
$script:_SrchSync = $sync
|
|
$script:_SrchFilters = $filters
|
|
|
|
$rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
|
|
$rs.ApartmentState = "STA"; $rs.ThreadOptions = "ReuseThread"; $rs.Open()
|
|
$ps = [System.Management.Automation.PowerShell]::Create()
|
|
$ps.Runspace = $rs
|
|
[void]$ps.AddScript($bgSearch)
|
|
[void]$ps.AddArgument($filters)
|
|
[void]$ps.AddArgument($sync)
|
|
$script:_SrchRS = $rs
|
|
$script:_SrchPS = $ps
|
|
$script:_SrchHnd = $ps.BeginInvoke()
|
|
|
|
$tmr = New-Object System.Windows.Forms.Timer
|
|
$tmr.Interval = 250
|
|
$script:_SrchTimer = $tmr
|
|
$tmr.Add_Tick({
|
|
while ($script:_SrchSync.Queue.Count -gt 0) {
|
|
$m = $script:_SrchSync.Queue.Dequeue()
|
|
Write-Log $m.Text $m.Color
|
|
}
|
|
if ($script:_SrchSync.Done) {
|
|
$script:_SrchTimer.Stop(); $script:_SrchTimer.Dispose()
|
|
while ($script:_SrchSync.Queue.Count -gt 0) {
|
|
$m = $script:_SrchSync.Queue.Dequeue(); Write-Log $m.Text $m.Color
|
|
}
|
|
try { [void]$script:_SrchPS.EndInvoke($script:_SrchHnd) } catch {}
|
|
try { $script:_SrchRS.Close(); $script:_SrchRS.Dispose() } catch {}
|
|
$btnSearch.Enabled = $true
|
|
$progressBar.MarqueeAnimationSpeed = 0
|
|
|
|
if ($script:_SrchSync.Error) {
|
|
Write-Log "Echec : $($script:_SrchSync.Error)" "Red"
|
|
return
|
|
}
|
|
|
|
$results = $script:_SrchSync.Results
|
|
$kql = $script:_SrchSync.KQL
|
|
$f = $script:_SrchFilters
|
|
|
|
if (-not $results -or $results.Count -eq 0) {
|
|
Write-Log "Aucun fichier trouve avec ces criteres." "Orange"
|
|
return
|
|
}
|
|
|
|
$stamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
|
$outDir = $f.OutFolder
|
|
if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir | Out-Null }
|
|
|
|
if ($f.Format -eq "HTML") {
|
|
$outFile = Join-Path $outDir "FileSearch_$stamp.html"
|
|
$html = Export-SearchResultsToHTML -Results $results -KQL $kql -SiteUrl $f.SiteUrl
|
|
$html | Set-Content -Path $outFile -Encoding UTF8
|
|
} else {
|
|
$outFile = Join-Path $outDir "FileSearch_$stamp.csv"
|
|
$results | ForEach-Object {
|
|
[PSCustomObject]@{
|
|
Title = $_.Title
|
|
Path = $_.Path
|
|
FileExtension = $_.FileExtension
|
|
Created = $_.Created
|
|
LastModifiedTime= $_.LastModifiedTime
|
|
Author = $_.Author
|
|
ModifiedBy = $_.ModifiedBy
|
|
SizeBytes = $_.Size
|
|
}
|
|
} | Export-Csv -Path $outFile -NoTypeInformation -Encoding UTF8
|
|
}
|
|
|
|
Write-Log "Sauvegarde : $outFile" "White"
|
|
$script:_SrchLastOut = $outFile
|
|
$btnOpenSearch.Enabled = $true
|
|
}
|
|
})
|
|
$tmr.Start()
|
|
})
|
|
|
|
$btnOpenSearch.Add_Click({
|
|
$f = $script:_SrchLastOut
|
|
if ($f -and (Test-Path $f)) { Start-Process $f }
|
|
})
|
|
|
|
# ── Scan de doublons ────────────────────────────────────────────────────────
|
|
$btnScanDupes.Add_Click({
|
|
if (-not (Validate-Inputs)) { return }
|
|
|
|
$siteUrl = if ($script:SelectedSites -and $script:SelectedSites.Count -gt 0) {
|
|
$script:SelectedSites[0]
|
|
} else { $txtSiteURL.Text.Trim() }
|
|
if ([string]::IsNullOrWhiteSpace($siteUrl)) {
|
|
[System.Windows.Forms.MessageBox]::Show("Veuillez saisir une URL de site.", "URL manquante", "OK", "Warning")
|
|
return
|
|
}
|
|
|
|
$dupFilters = @{
|
|
Mode = if ($radDupFolders.Checked) { "Folders" } else { "Files" }
|
|
MatchSize = $chkDupSize.Checked
|
|
MatchCreated = $chkDupCreated.Checked
|
|
MatchMod = $chkDupModified.Checked
|
|
MatchSubDir = $chkDupSubCount.Checked
|
|
MatchFiles = $chkDupFileCount.Checked
|
|
IncludeSubs = $chkDupSubsites.Checked
|
|
Library = $txtDupLib.Text.Trim()
|
|
Format = if ($radDupHTML.Checked) { "HTML" } else { "CSV" }
|
|
OutFolder = $txtOutput.Text.Trim()
|
|
SiteUrl = $siteUrl
|
|
ClientId = $txtClientId.Text.Trim()
|
|
}
|
|
|
|
$btnScanDupes.Enabled = $false
|
|
$btnOpenDupes.Enabled = $false
|
|
$txtLog.Clear()
|
|
$progressBar.MarqueeAnimationSpeed = 30
|
|
Write-Log "=== SCAN DE DOUBLONS ===" "White"
|
|
Write-Log "Mode : $($dupFilters.Mode)" "Gray"
|
|
Write-Log "Site : $siteUrl" "Gray"
|
|
Write-Log "Criteres : Nom (toujours)$(if($dupFilters.MatchSize){', Taille'})$(if($dupFilters.MatchCreated){', Cree le'})$(if($dupFilters.MatchMod){', Modifie le'})$(if($dupFilters.MatchSubDir){', Nb sous-doss.'})$(if($dupFilters.MatchFiles){', Nb fichiers'})" "Gray"
|
|
Write-Log ("-" * 52) "DarkGray"
|
|
|
|
$bgDupScan = {
|
|
param($Filters, $Sync)
|
|
function BgLog([string]$m, [string]$c = "LightGreen") {
|
|
$Sync.Queue.Enqueue(@{ Text = $m; Color = $c })
|
|
}
|
|
function MakeKey($name, $item, $f) {
|
|
$parts = @($name.ToLower())
|
|
if ($f.MatchSize -and $null -ne $item.SizeBytes) { $parts += [string]$item.SizeBytes }
|
|
if ($f.MatchCreated -and $null -ne $item.CreatedDay) { $parts += $item.CreatedDay }
|
|
if ($f.MatchMod -and $null -ne $item.ModifiedDay){ $parts += $item.ModifiedDay }
|
|
if ($f.MatchSubDir -and $null -ne $item.FolderCount){ $parts += [string]$item.FolderCount }
|
|
if ($f.MatchFiles -and $null -ne $item.FileCount) { $parts += [string]$item.FileCount }
|
|
return $parts -join "|"
|
|
}
|
|
try {
|
|
Import-Module PnP.PowerShell -ErrorAction Stop
|
|
Connect-PnPOnline -Url $Filters.SiteUrl -Interactive -ClientId $Filters.ClientId
|
|
|
|
$allItems = [System.Collections.Generic.List[object]]::new()
|
|
|
|
if ($Filters.Mode -eq "Files") {
|
|
# ── Files: use Search API ──────────────────────────────────
|
|
$kql = "ContentType:Document"
|
|
if ($Filters.Library) {
|
|
$kql += " AND Path:""$($Filters.SiteUrl.TrimEnd('/'))/$($Filters.Library.TrimStart('/'))*"""
|
|
}
|
|
BgLog "Requete KQL : $kql" "Yellow"
|
|
$startRow = 0; $batchSize = 500
|
|
do {
|
|
$batch = Submit-PnPSearchQuery -Query $kql `
|
|
-StartRow $startRow -MaxResults $batchSize `
|
|
-SelectProperties @("Title","Path","Author","LastModifiedTime","FileExtension","Created","Size") `
|
|
-TrimDuplicates $false
|
|
$hits = @($batch.ResultRows)
|
|
foreach ($h in $hits) {
|
|
$fname = [System.IO.Path]::GetFileName($h.Path)
|
|
try { $crDay = ([DateTime]$h.Created).Date.ToString('yyyy-MM-dd') } catch { $crDay = "" }
|
|
try { $modDay = ([DateTime]$h.LastModifiedTime).Date.ToString('yyyy-MM-dd') } catch { $modDay = "" }
|
|
$sizeB = [long]($h.Size -replace '[^0-9]','0' -replace '^$','0')
|
|
$allItems.Add([PSCustomObject]@{
|
|
Name = $fname
|
|
Path = $h.Path
|
|
Library = ""
|
|
SizeBytes = $sizeB
|
|
Created = $h.Created
|
|
Modified = $h.LastModifiedTime
|
|
CreatedDay = $crDay
|
|
ModifiedDay = $modDay
|
|
})
|
|
}
|
|
BgLog " $($hits.Count) fichiers recuperes (total: $($allItems.Count))" "Cyan"
|
|
$startRow += $batchSize
|
|
} while ($hits.Count -eq $batchSize)
|
|
|
|
} else {
|
|
# ── Folders: use Get-PnPListItem ───────────────────────────
|
|
$webUrls = @($Filters.SiteUrl)
|
|
if ($Filters.IncludeSubs) {
|
|
$subs = Get-PnPSubWeb -Recurse -ErrorAction SilentlyContinue
|
|
if ($subs) { $webUrls += @($subs | Select-Object -ExpandProperty Url) }
|
|
}
|
|
|
|
foreach ($webUrl in $webUrls) {
|
|
BgLog "Scan web : $webUrl" "Yellow"
|
|
Connect-PnPOnline -Url $webUrl -Interactive -ClientId $Filters.ClientId
|
|
$lists = Get-PnPList | Where-Object {
|
|
!$_.Hidden -and $_.BaseType -eq "DocumentLibrary" -and
|
|
(-not $Filters.Library -or $_.Title -like "*$($Filters.Library)*")
|
|
}
|
|
foreach ($list in $lists) {
|
|
BgLog " Bibliotheque : $($list.Title)" "Cyan"
|
|
try {
|
|
$folderItems = Get-PnPListItem -List $list -PageSize 2000 -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.FileSystemObjectType -eq "Folder" }
|
|
foreach ($fi in $folderItems) {
|
|
$fv = $fi.FieldValues
|
|
$fname = $fv.FileLeafRef
|
|
try { $crDay = ([DateTime]$fv.Created).Date.ToString('yyyy-MM-dd') } catch { $crDay = "" }
|
|
try { $modDay = ([DateTime]$fv.Modified).Date.ToString('yyyy-MM-dd') } catch { $modDay = "" }
|
|
$subCount = [int]($fv.FolderChildCount)
|
|
$fileCount = [int]($fv.ItemChildCount) - $subCount
|
|
if ($fileCount -lt 0) { $fileCount = 0 }
|
|
$allItems.Add([PSCustomObject]@{
|
|
Name = $fname
|
|
Path = "$($Filters.SiteUrl.TrimEnd('/'))/$($fv.FileRef.TrimStart('/'))"
|
|
Library = $list.Title
|
|
SizeBytes = $null
|
|
Created = $fv.Created
|
|
Modified = $fv.Modified
|
|
CreatedDay = $crDay
|
|
ModifiedDay = $modDay
|
|
FolderCount = $subCount
|
|
FileCount = $fileCount
|
|
})
|
|
}
|
|
BgLog " $($folderItems.Count) dossier(s)" "Cyan"
|
|
} catch { BgLog " Ignore : $($_.Exception.Message)" "DarkGray" }
|
|
}
|
|
}
|
|
}
|
|
|
|
BgLog "$($allItems.Count) element(s) collecte(s), recherche des doublons..." "Yellow"
|
|
|
|
# Group by computed key and keep only groups with ≥ 2
|
|
$grouped = $allItems | Group-Object { MakeKey $_.Name $_ $Filters } |
|
|
Where-Object { $_.Count -ge 2 } |
|
|
ForEach-Object {
|
|
[PSCustomObject]@{
|
|
Key = $_.Name
|
|
Name = $_.Group[0].Name
|
|
Items = @($_.Group)
|
|
}
|
|
}
|
|
|
|
BgLog "$($grouped.Count) groupe(s) de doublons trouve(s)" "LightGreen"
|
|
$Sync.Groups = @($grouped)
|
|
|
|
} catch {
|
|
$Sync.Error = $_.Exception.Message
|
|
BgLog "Erreur : $($_.Exception.Message)" "Red"
|
|
} finally { $Sync.Done = $true }
|
|
}
|
|
|
|
$dupSync = [hashtable]::Synchronized(@{
|
|
Queue = [System.Collections.Generic.Queue[object]]::new()
|
|
Done = $false; Error = $null; Groups = $null
|
|
})
|
|
$script:_DupSync = $dupSync
|
|
$script:_DupFilters = $dupFilters
|
|
|
|
$rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
|
|
$rs.ApartmentState = "STA"; $rs.ThreadOptions = "ReuseThread"; $rs.Open()
|
|
$ps = [System.Management.Automation.PowerShell]::Create()
|
|
$ps.Runspace = $rs
|
|
[void]$ps.AddScript($bgDupScan)
|
|
[void]$ps.AddArgument($dupFilters)
|
|
[void]$ps.AddArgument($dupSync)
|
|
$script:_DupRS = $rs
|
|
$script:_DupPS = $ps
|
|
$script:_DupHnd = $ps.BeginInvoke()
|
|
|
|
$tmr = New-Object System.Windows.Forms.Timer
|
|
$tmr.Interval = 250
|
|
$script:_DupTimer = $tmr
|
|
$tmr.Add_Tick({
|
|
while ($script:_DupSync.Queue.Count -gt 0) {
|
|
$m = $script:_DupSync.Queue.Dequeue(); Write-Log $m.Text $m.Color
|
|
}
|
|
if ($script:_DupSync.Done) {
|
|
$script:_DupTimer.Stop(); $script:_DupTimer.Dispose()
|
|
while ($script:_DupSync.Queue.Count -gt 0) {
|
|
$m = $script:_DupSync.Queue.Dequeue(); Write-Log $m.Text $m.Color
|
|
}
|
|
try { [void]$script:_DupPS.EndInvoke($script:_DupHnd) } catch {}
|
|
try { $script:_DupRS.Close(); $script:_DupRS.Dispose() } catch {}
|
|
$btnScanDupes.Enabled = $true
|
|
$progressBar.MarqueeAnimationSpeed = 0
|
|
|
|
if ($script:_DupSync.Error) {
|
|
Write-Log "Echec : $($script:_DupSync.Error)" "Red"
|
|
return
|
|
}
|
|
|
|
$groups = $script:_DupSync.Groups
|
|
if (-not $groups -or $groups.Count -eq 0) {
|
|
Write-Log "Aucun doublon detecte avec ces criteres." "Orange"
|
|
return
|
|
}
|
|
|
|
$f = $script:_DupFilters
|
|
$stamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
|
$outDir = $f.OutFolder
|
|
if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir | Out-Null }
|
|
|
|
if ($f.Format -eq "HTML") {
|
|
$outFile = Join-Path $outDir "Duplicates_$($f.Mode)_$stamp.html"
|
|
$html = Export-DuplicatesToHTML -Groups $groups -Mode $f.Mode -SiteUrl $f.SiteUrl
|
|
$html | Set-Content -Path $outFile -Encoding UTF8
|
|
} else {
|
|
$outFile = Join-Path $outDir "Duplicates_$($f.Mode)_$stamp.csv"
|
|
$groups | ForEach-Object {
|
|
$grp = $_
|
|
$grp.Items | ForEach-Object {
|
|
[PSCustomObject]@{
|
|
DuplicateGroup = $grp.Name
|
|
Name = $_.Name
|
|
Path = $_.Path
|
|
Library = $_.Library
|
|
SizeBytes = $_.SizeBytes
|
|
Created = $_.Created
|
|
Modified = $_.Modified
|
|
FolderCount = $_.FolderCount
|
|
FileCount = $_.FileCount
|
|
}
|
|
}
|
|
} | Export-Csv -Path $outFile -NoTypeInformation -Encoding UTF8
|
|
}
|
|
|
|
Write-Log "Sauvegarde : $outFile" "White"
|
|
$script:_DupLastOut = $outFile
|
|
$btnOpenDupes.Enabled = $true
|
|
}
|
|
})
|
|
$tmr.Start()
|
|
})
|
|
|
|
$btnOpenDupes.Add_Click({
|
|
$f = $script:_DupLastOut
|
|
if ($f -and (Test-Path $f)) { Start-Process $f }
|
|
})
|
|
|
|
#endregion
|
|
|
|
# ── Initialisation : chargement des settings ───────────────────────────────
|
|
$_settings = Load-Settings
|
|
$script:DataFolder = if ($_settings.dataFolder -and (Test-Path $_settings.dataFolder)) {
|
|
$_settings.dataFolder
|
|
} elseif ($PSScriptRoot) { $PSScriptRoot } else { $PWD.Path }
|
|
|
|
# Load saved language (applies T() translations and updates all registered controls)
|
|
$_savedLang = if ($_settings.lang) { $_settings.lang } else { "en" }
|
|
if ($_savedLang -ne "en") {
|
|
Load-Language $_savedLang
|
|
Update-UILanguage
|
|
foreach ($mi in $menuLang.DropDownItems) {
|
|
if ($mi -is [System.Windows.Forms.ToolStripMenuItem]) {
|
|
$mi.Checked = ($mi.Tag -eq $script:CurrentLang)
|
|
}
|
|
}
|
|
}
|
|
|
|
Refresh-ProfileList
|
|
$n = (Load-Templates).Count
|
|
$lblTplCount.Text = "$n $(T 'tpl.count')"
|
|
|
|
[System.Windows.Forms.Application]::Run($form)
|