-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRemove-ExcelSheetProtection.ps1
More file actions
316 lines (251 loc) · 11.6 KB
/
Copy pathRemove-ExcelSheetProtection.ps1
File metadata and controls
316 lines (251 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
<#
.SYNOPSIS
Entfernt den Blattschutz (Worksheet Protection) aus einer oder mehreren
Tabellen einer .xlsx-Datei durch direkte XML-Manipulation.
.DESCRIPTION
Das Skript behandelt die .xlsx-Datei als ZIP-Archiv, entpackt sie in einen
GUID-basierten temporären Ordner, analysiert die interne XML-Struktur
(workbook.xml + workbook.xml.rels) und entfernt das <sheetProtection>-Element
aus den gewählten Arbeitsblatt-XMLs.
Die XML-Dateien werden encoding-sicher (UTF-8 ohne BOM) gespeichert.
Das Zusammenpacken erfolgt direkt über die .NET-Klasse
[System.IO.Compression.ZipFile], um den fragilen Set-Location/*-Trick
zu vermeiden.
Der temporäre Ordner wird im finally-Block stets restlos bereinigt.
.PARAMETER ExcelFilePath
Pflichtparameter. Vollständiger oder relativer Pfad zur .xlsx-Quelldatei.
.PARAMETER OutputFolder
Optionaler Zielordner für die neue Ausgabedatei.
Wird dieser nicht angegeben, wird der Ordner der Quelldatei verwendet.
Existiert der Ordner nicht, wird eine Rückfrage gestellt.
.PARAMETER UnlockAll
Switch-Parameter. Falls gesetzt, werden alle Arbeitsblätter ohne
grafische Auswahl automatisch entsperrt.
.EXAMPLE
.\Remove-ExcelSheetProtection.ps1 -ExcelFilePath "C:\Daten\Analyse.xlsx"
.EXAMPLE
.\Remove-ExcelSheetProtection.ps1 -ExcelFilePath "C:\Daten\Analyse.xlsx" -UnlockAll
.EXAMPLE
.\Remove-ExcelSheetProtection.ps1 -ExcelFilePath "C:\Daten\Analyse.xlsx" -OutputFolder "C:\Export"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, HelpMessage = "Pfad zur .xlsx-Quelldatei")]
[string]$ExcelFilePath,
[Parameter(Mandatory = $false, HelpMessage = "Optionaler Zielordner (Standard: Ordner der Quelldatei)")]
[string]$OutputFolder,
[Parameter(Mandatory = $false, HelpMessage = "Alle Blätter automatisch ohne Auswahl entsperren")]
[switch]$UnlockAll
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.IO.Compression.FileSystem
# ============================================================
# Hilfsfunktion: XML encoding-sicher als UTF-8 ohne BOM speichern
# ============================================================
function Save-XmlUtf8NoBom {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[System.Xml.XmlDocument]$XmlDoc,
[Parameter(Mandatory = $true)]
[string]$FilePath
)
$writerSettings = New-Object System.Xml.XmlWriterSettings
$writerSettings.Encoding = New-Object System.Text.UTF8Encoding($false) # $false = kein BOM
$writerSettings.Indent = $false
$xmlWriter = [System.Xml.XmlWriter]::Create($FilePath, $writerSettings)
try {
$XmlDoc.Save($xmlWriter)
}
finally {
$xmlWriter.Close()
}
}
# ============================================================
# Region 1: Quelldatei validieren
# ============================================================
if (-not (Test-Path -LiteralPath $ExcelFilePath)) {
Write-Error "Quelldatei nicht gefunden: $($ExcelFilePath)"
return
}
$sourceFile = Get-Item -LiteralPath $ExcelFilePath
if ($sourceFile.Extension -ine '.xlsx') {
Write-Error "Die angegebene Datei ist keine .xlsx-Datei: $($sourceFile.Name)"
return
}
Write-Host "Quelldatei: $($sourceFile.FullName)" -ForegroundColor Cyan
# ============================================================
# Region 2: Zielordner bestimmen und Schreibrechte prüfen
# ============================================================
if ([string]::IsNullOrWhiteSpace($OutputFolder)) {
$targetDir = $sourceFile.DirectoryName
}
else {
$targetDir = $OutputFolder
}
if (-not (Test-Path -LiteralPath $targetDir)) {
Write-Host "Zielordner existiert nicht: $($targetDir)" -ForegroundColor Yellow
$answer = Read-Host "Ordner jetzt erstellen? (J/N)"
if ($answer -match '^[Jj]') {
$null = New-Item -ItemType Directory -Path $targetDir -ErrorAction Stop
Write-Host "Ordner erstellt: $($targetDir)" -ForegroundColor Green
}
else {
Write-Host "Abgebrochen." -ForegroundColor Red
return
}
}
$testFilePath = Join-Path $targetDir "$([Guid]::NewGuid().ToString()).tmp"
try {
$null = New-Item -Path $testFilePath -ItemType File -ErrorAction Stop
Remove-Item -LiteralPath $testFilePath -Force -ErrorAction Stop
}
catch {
Write-Host "FEHLER: Keine Schreibrechte im Zielordner: $($targetDir)" -ForegroundColor Red
return
}
# ============================================================
# Region 3: Temporären Ordner anlegen
# (wird VOR dem try-Block deklariert, damit finally
# ihn immer kennt, auch bei frühen Fehlern)
# ============================================================
$guid = [Guid]::NewGuid().ToString()
$tempFolder = Join-Path ([System.IO.Path]::GetTempPath()) "ExcelUnprotect_$($guid)"
$null = New-Item -ItemType Directory -Path $tempFolder
Write-Host "Temporärer Arbeitsordner: $($tempFolder)" -ForegroundColor DarkGray
# ============================================================
# Aktuellen Pfad VOR dem try-Block sichern, damit der
# finally-Block ihn stets wiederherstellen kann
# ============================================================
$savedLocation = Get-Location
try {
# ----------------------------------------------------------
# Region 4: Entpacken
# ----------------------------------------------------------
Write-Host "Entpacke '$($sourceFile.Name)'..." -ForegroundColor Cyan
Expand-Archive -LiteralPath $sourceFile.FullName -DestinationPath $tempFolder -Force
# ----------------------------------------------------------
# Region 5: Interne XML-Pfade bestimmen und validieren
# ----------------------------------------------------------
$xlFolder = Join-Path $tempFolder "xl"
$workbookXmlPath = Join-Path $xlFolder "workbook.xml"
$relsXmlPath = Join-Path $xlFolder (Join-Path "_rels" "workbook.xml.rels")
if (-not (Test-Path -LiteralPath $workbookXmlPath)) {
throw "Ungültige Excel-Struktur: workbook.xml nicht gefunden."
}
if (-not (Test-Path -LiteralPath $relsXmlPath)) {
throw "Ungültige Excel-Struktur: workbook.xml.rels nicht gefunden."
}
# ----------------------------------------------------------
# Region 6: Sheet-Mapping aufbauen
# Anzeigename → physische XML-Datei
#
# WICHTIG: Das r:id-Attribut liegt im Relationships-Namespace.
# Direktzugriff über $sheet.id ist zufällig und falsch.
# Korrekt: GetAttribute("id", $rNs)
# ----------------------------------------------------------
[xml]$wbDoc = Get-Content -LiteralPath $workbookXmlPath -Encoding UTF8
[xml]$relDoc = Get-Content -LiteralPath $relsXmlPath -Encoding UTF8
$mainNs = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
$rNs = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
$nsManager = New-Object System.Xml.XmlNamespaceManager($wbDoc.NameTable)
$nsManager.AddNamespace("main", $mainNs)
$nsManager.AddNamespace("r", $rNs)
$sheetMapping = New-Object System.Collections.Generic.List[PSCustomObject]
foreach ($sheetNode in $wbDoc.SelectNodes("//main:sheet", $nsManager)) {
# r:id korrekt über den Relationships-Namespace abfragen
$rId = $sheetNode.GetAttribute("id", $rNs)
$relNode = $relDoc.Relationships.Relationship | Where-Object { $_.Id -eq $rId }
if ($null -ne $relNode) {
$target = $relNode.Target
# Target kann relativ ("worksheets/sheet1.xml") oder absolut sein
if ([System.IO.Path]::IsPathRooted($target)) {
$xmlPath = $target
}
else {
$xmlPath = Join-Path $xlFolder $target
}
$sheetMapping.Add([PSCustomObject]@{
Blattname = $sheetNode.name
XmlPfad = $xmlPath
})
}
}
Write-Host "$($sheetMapping.Count) Arbeitsblatt/Arbeitsblätter erkannt." -ForegroundColor Cyan
if ($sheetMapping.Count -eq 0) {
throw "Es wurden keine Arbeitsblätter in der Datei gefunden."
}
# ----------------------------------------------------------
# Region 7: Blattauswahl
# ----------------------------------------------------------
if ($UnlockAll.IsPresent) {
Write-Host "Modus: Alle Blätter automatisch entsperren." -ForegroundColor Yellow
$selectedSheets = $sheetMapping
}
else {
Write-Host "Modus: Manuelle Auswahl via Out-GridView." -ForegroundColor Yellow
$selectedSheets = $sheetMapping | Out-GridView -Title "Blätter zum Entsperren auswählen (Mehrfachauswahl möglich)" -PassThru
if ($null -eq $selectedSheets -or @($selectedSheets).Count -eq 0) {
Write-Host "Keine Blätter ausgewählt. Vorgang wird abgebrochen." -ForegroundColor Yellow
return
}
}
# ----------------------------------------------------------
# Region 8: sheetProtection-Tag aus den gewählten XMLs entfernen
# ----------------------------------------------------------
$removedCount = 0
$skippedCount = 0
foreach ($sheet in $selectedSheets) {
if (-not (Test-Path -LiteralPath $sheet.XmlPfad)) {
Write-Host " [WARNUNG] XML-Datei nicht gefunden: $($sheet.XmlPfad)" -ForegroundColor Yellow
continue
}
[xml]$sheetXml = Get-Content -LiteralPath $sheet.XmlPfad -Encoding UTF8
$protNode = $sheetXml.SelectSingleNode("//*[local-name()='sheetProtection']")
if ($null -ne $protNode) {
$null = $protNode.ParentNode.RemoveChild($protNode)
Save-XmlUtf8NoBom -XmlDoc $sheetXml -FilePath $sheet.XmlPfad
Write-Host " [OK] Schutz entfernt: '$($sheet.Blattname)'" -ForegroundColor Green
$removedCount++
}
else {
Write-Host " [INFO] Kein Schutz vorhanden: '$($sheet.Blattname)'" -ForegroundColor DarkGray
$skippedCount++
}
}
Write-Host "Zusammenfassung: $($removedCount) Blatt/Blätter entsperrt, $($skippedCount) ohne Schutz übersprungen." -ForegroundColor Cyan
# ----------------------------------------------------------
# Region 9: Zusammenpacken mit ZipFile (kein Set-Location-Trick)
# ZipFile::CreateFromDirectory ist LiteralPath-sicher
# und benötigt keine Änderung des aktuellen Verzeichnisses.
# ----------------------------------------------------------
$newFileName = "$($sourceFile.BaseName)_unprotected.xlsx"
$finalPath = Join-Path $targetDir $newFileName
if (Test-Path -LiteralPath $finalPath) {
Write-Host "Bestehende Ausgabedatei wird überschrieben: $($finalPath)" -ForegroundColor Yellow
Remove-Item -LiteralPath $finalPath -Force
}
Write-Host "Erstelle Ausgabedatei..." -ForegroundColor Cyan
# $false = Basisordnernamen NICHT in das Archiv einschließen
[System.IO.Compression.ZipFile]::CreateFromDirectory(
$tempFolder,
$finalPath,
[System.IO.Compression.CompressionLevel]::Optimal,
$false
)
Write-Host "Erfolgreich erstellt: $($finalPath)" -ForegroundColor Green
}
catch {
Write-Error "Fehler während der Verarbeitung: $($_.Exception.Message)"
}
finally {
# Ursprüngliches Verzeichnis wiederherstellen
Set-Location -LiteralPath $savedLocation
# Temporären Ordner restlos bereinigen
if (Test-Path -LiteralPath $tempFolder) {
Write-Host "Bereinige temporären Ordner..." -ForegroundColor DarkGray
Remove-Item -LiteralPath $tempFolder -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "Temporärer Ordner entfernt." -ForegroundColor DarkGray
}
}