#Requires -Version 5.1 <# .SYNOPSIS Installiert bzw. entfernt das Firefox-Add-on "Swyx Tab Bridge" auf einem Windows-Rechner. Laeuft lokal oder ueber SSH (siehe deploy-windows.sh). .DESCRIPTION Zwei Installationsarten: -Mode Policy (Default, empfohlen fuer den Dauerbetrieb) Schreibt eine Enterprise-Policy nach \distribution\policies.json mit installation_mode = force_installed. Das Add-on wird beim naechsten Firefox-Start automatisch, ohne Rueckfrage und fuer alle Profile des Rechners installiert und kann vom Benutzer nicht deaktiviert werden. Benoetigt Schreibrecht im Firefox-Installationsverzeichnis (bei einer systemweiten Installation also Administratorrechte, bei einer Benutzerinstallation unter %LOCALAPPDATA% keine) und ein signiertes .xpi. -Mode Profile Legt das .xpi als \extensions\.xpi ab. Ohne Adminrechte moeglich, wirkt nur auf die Profile des Zielbenutzers, und Firefox fragt beim naechsten Start einmal nach Bestaetigung. Firefox Release und ESR installieren nur signierte Add-ons. Ein unsigniertes Paket laesst sich nur mit -AllowUnsigned und nur auf Firefox ESR, Developer Edition oder Nightly betreiben; -AllowUnsigned hinterlegt dafuer eine autoconfig-Datei, die xpinstall.signatures.required auf false sperrt. .PARAMETER XpiPath Pfad zum .xpi (lokal auf dem Windows-Rechner). Pflicht ausser bei -Uninstall. .PARAMETER Mode Policy (Default) oder Profile. .PARAMETER ExtensionId Add-on-ID, muss zu browser_specific_settings.gecko.id im Manifest passen. .PARAMETER FirefoxDir Installationsverzeichnis von Firefox. Wird normalerweise automatisch erkannt; nur noetig, wenn mehrere Installationen gefunden werden. .PARAMETER TargetUser Benutzer, dessen Profile bei -Mode Profile bespielt werden. Default: der aktuelle Benutzer. Fremde Benutzer erfordern Adminrechte. .PARAMETER AllowUnsigned Signaturpruefung per autoconfig abschalten (nur ESR / Developer Edition). .PARAMETER StopFirefox Laufende firefox.exe-Prozesse beenden, damit die Installation sofort greift. Firefox stellt die Sitzung beim naechsten Start wieder her. .PARAMETER Uninstall Policy-Eintrag, hinterlegtes .xpi und Profil-Kopien wieder entfernen. .EXAMPLE .\Install-SwyxTabBridge.ps1 -XpiPath .\swyx_tab_bridge-1.0.0.xpi .EXAMPLE .\Install-SwyxTabBridge.ps1 -XpiPath .\swyx.xpi -Mode Profile -StopFirefox .EXAMPLE .\Install-SwyxTabBridge.ps1 -Uninstall .NOTES Exitcodes: 0 = Erfolg, 1 = Fehler, 2 = Erfolg, aber Firefox-Neustart noetig. Die Datei ist bewusst rein ASCII, damit Windows PowerShell 5.1 sie unabhaengig von der Codepage korrekt liest. #> [CmdletBinding(SupportsShouldProcess)] param( [string]$XpiPath, [ValidateSet('Policy', 'Profile')] [string]$Mode = 'Policy', [string]$ExtensionId = 'swyx-tab-bridge@appcreation.de', [string]$FirefoxDir, [string]$TargetUser = $env:USERNAME, [switch]$AllowUnsigned, [switch]$StopFirefox, [switch]$Uninstall ) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # Ablageort des .xpi fuer den Policy-Modus. Muss fuer alle Benutzer lesbar sein, # weil Firefox die Datei bei jedem Profil-Start ueber die file:-URL liest. $Script:XpiStore = Join-Path $env:ProgramData 'SwyxTabBridge' $Script:RestartNeeded = $false # ------------------------------------------------------------------ Ausgabe # Ausgabe bewusst ueber [Console] statt Write-Host: bei umgeleitetem stdout - # genau der Fall bei "ssh host powershell ..." - serialisiert Windows PowerShell # den Information-Stream sonst als CLIXML und die Ausgabe wird unlesbar. function Write-Line { param([string]$Message = '') [Console]::Out.WriteLine($Message) } function Write-Step { param([string]$Message) Write-Line ''; Write-Line "==> $Message" } function Write-Info { param([string]$Message) Write-Line " $Message" } function Write-Good { param([string]$Message) Write-Line " [ok] $Message" } function Write-Note { param([string]$Message) Write-Line " [!] $Message" } # ------------------------------------------------------------------ Helfer function Test-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($identity) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } # Massgeblich ist nicht "ist der Benutzer Admin", sondern "darf hier geschrieben # werden": eine Firefox-Benutzerinstallation unter %LOCALAPPDATA% laesst sich # ohne jedes Sonderrecht mit einer Policy versehen. function Test-DirectoryWritable { param([string]$Path) $probeRoot = $Path while ($probeRoot -and -not (Test-Path -LiteralPath $probeRoot)) { $probeRoot = Split-Path -Parent $probeRoot } if (-not $probeRoot) { return $false } $probe = Join-Path $probeRoot ([System.IO.Path]::GetRandomFileName()) try { [System.IO.File]::WriteAllText($probe, 'probe') Remove-Item -LiteralPath $probe -Force return $true } catch { return $false } } function ConvertTo-FileUrl { param([string]$Path) return ([uri](Resolve-Path -LiteralPath $Path).ProviderPath).AbsoluteUri } # JSON -> verschachtelte Hashtables. ConvertFrom-Json liefert PSCustomObjects, # die sich nicht sinnvoll zusammenfuehren lassen; -AsHashtable gibt es erst ab # PowerShell 6, hier laeuft aber oft noch Windows PowerShell 5.1. function ConvertTo-HashtableDeep { param($InputObject) if ($null -eq $InputObject) { return $null } if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { $list = @() foreach ($item in $InputObject) { $list += ,(ConvertTo-HashtableDeep $item) } return ,$list } if ($InputObject -is [psobject] -and $InputObject.PSObject.Properties.Name.Count -gt 0 -and $InputObject.GetType().Name -eq 'PSCustomObject') { $map = @{} foreach ($property in $InputObject.PSObject.Properties) { $map[$property.Name] = ConvertTo-HashtableDeep $property.Value } return $map } return $InputObject } function Write-JsonFile { param([string]$Path, $Data) $json = $Data | ConvertTo-Json -Depth 32 $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($Path, $json, $utf8NoBom) } function Backup-File { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { return } $stamp = (Get-Date).ToString('yyyyMMdd-HHmmss') $backup = "$Path.bak-$stamp" Copy-Item -LiteralPath $Path -Destination $backup -Force Write-Info "Sicherung: $backup" } # ------------------------------------------------------------------ Firefox finden function Get-FirefoxInstallation { param([string]$Explicit) if ($Explicit) { $exe = Join-Path $Explicit 'firefox.exe' if (-not (Test-Path -LiteralPath $exe)) { throw "In '$Explicit' liegt keine firefox.exe." } return @( New-FirefoxInfo -Directory $Explicit ) } $directories = New-Object System.Collections.Generic.List[string] # Registrierung: HK{LM,CU}\SOFTWARE\[WOW6432Node\]Mozilla\\\Main # HKCU deckt Benutzerinstallationen nach %LOCALAPPDATA% ab - der Standard, # wenn Firefox ohne Adminrechte installiert wurde. foreach ($root in @('HKLM:\SOFTWARE\Mozilla', 'HKLM:\SOFTWARE\WOW6432Node\Mozilla', 'HKCU:\SOFTWARE\Mozilla', 'HKCU:\SOFTWARE\WOW6432Node\Mozilla')) { if (-not (Test-Path -LiteralPath $root)) { continue } foreach ($product in (Get-ChildItem -LiteralPath $root -ErrorAction SilentlyContinue)) { foreach ($version in (Get-ChildItem -LiteralPath $product.PSPath -ErrorAction SilentlyContinue)) { $main = Join-Path $version.PSPath 'Main' if (-not (Test-Path -LiteralPath $main)) { continue } $value = (Get-ItemProperty -LiteralPath $main -ErrorAction SilentlyContinue).'Install Directory' if ($value -and (Test-Path -LiteralPath (Join-Path $value 'firefox.exe'))) { $directories.Add($value) } } } } # Fallback: Standardpfade, inklusive Benutzerinstallation in %LOCALAPPDATA% foreach ($base in @($env:ProgramFiles, ${env:ProgramFiles(x86)}, $env:LOCALAPPDATA)) { if (-not $base) { continue } foreach ($name in @('Mozilla Firefox', 'Firefox Developer Edition', 'Firefox Nightly')) { $candidate = Join-Path $base $name if (Test-Path -LiteralPath (Join-Path $candidate 'firefox.exe')) { $directories.Add($candidate) } } } $unique = $directories | Sort-Object -Unique return @($unique | ForEach-Object { New-FirefoxInfo -Directory $_ }) } function New-FirefoxInfo { param([string]$Directory) $exe = Join-Path $Directory 'firefox.exe' $info = (Get-Item -LiteralPath $exe).VersionInfo $product = $info.ProductName $version = $info.ProductVersion # Der Kanal steht nirgends direkt; application.ini verraet ihn indirekt. # Reihenfolge ist wichtig: Developer Edition wird aus mozilla-beta gebaut, # erlaubt aber - anders als Beta - unsignierte Add-ons. Deshalb hat der # RemotingName (dort "firefox-dev") das letzte Wort. $channel = 'release' $appIni = Join-Path $Directory 'application.ini' if (Test-Path -LiteralPath $appIni) { $content = Get-Content -LiteralPath $appIni -Raw if ($content -match '(?m)^SourceRepository=.*mozilla-esr') { $channel = 'esr' } elseif ($content -match '(?m)^SourceRepository=.*mozilla-beta') { $channel = 'beta' } elseif ($content -match '(?m)^SourceRepository=.*mozilla-central') { $channel = 'nightly' } if ($content -match '(?m)^RemotingName=(.+)$') { $remoting = $Matches[1].Trim() if ($remoting -match '(?i)dev') { $channel = 'developer' } elseif ($remoting -match '(?i)nightly') { $channel = 'nightly' } } } return [pscustomobject]@{ Directory = $Directory Exe = $exe Product = $product Version = $version Channel = $channel } } function Select-FirefoxInstallation { param([string]$Explicit) $found = @(Get-FirefoxInstallation -Explicit $Explicit) if ($found.Count -eq 0) { throw "Keine Firefox-Installation gefunden. Pfad mit -FirefoxDir angeben." } if ($found.Count -gt 1) { Write-Note "Mehrere Firefox-Installationen gefunden:" foreach ($item in $found) { Write-Note " $($item.Directory) ($($item.Product) $($item.Version))" } throw "Bitte die gewuenschte Installation mit -FirefoxDir angeben." } return $found[0] } # ------------------------------------------------------------------ Profile finden function Get-FirefoxProfile { param([string]$User) if ($User -eq $env:USERNAME) { $roaming = $env:APPDATA } else { $roaming = Join-Path (Join-Path $env:SystemDrive "Users\$User") 'AppData\Roaming' } $profilesIni = Join-Path $roaming 'Mozilla\Firefox\profiles.ini' if (-not (Test-Path -LiteralPath $profilesIni)) { throw "Keine profiles.ini fuer Benutzer '$User' gefunden ($profilesIni). Firefox muss dort mindestens einmal gestartet worden sein." } $firefoxRoot = Split-Path -Parent $profilesIni $section = $null $sections = @{} foreach ($line in (Get-Content -LiteralPath $profilesIni)) { $line = $line.Trim() if ($line -match '^\[(.+)\]$') { $section = $Matches[1] $sections[$section] = @{} continue } if ($section -and $line -match '^([^=]+)=(.*)$') { $sections[$section][$Matches[1].Trim()] = $Matches[2].Trim() } } $result = @() foreach ($name in $sections.Keys) { if ($name -notlike 'Profile*') { continue } $entry = $sections[$name] if (-not $entry.ContainsKey('Path')) { continue } $path = $entry['Path'] -replace '/', '\' if ($entry.ContainsKey('IsRelative') -and $entry['IsRelative'] -eq '0') { $full = $path } else { $full = Join-Path $firefoxRoot $path } if (-not (Test-Path -LiteralPath $full)) { continue } $displayName = $name if ($entry.ContainsKey('Name')) { $displayName = $entry['Name'] } $result += [pscustomobject]@{ Name = $displayName Path = $full IsDefault = ($entry.ContainsKey('Default') -and $entry['Default'] -eq '1') } } if ($result.Count -eq 0) { throw "In '$profilesIni' ist kein existierendes Profil eingetragen." } return $result } # ------------------------------------------------------------------ XPI pruefen function Get-XpiInfo { param([string]$Path) Add-Type -AssemblyName System.IO.Compression.FileSystem | Out-Null $archive = [System.IO.Compression.ZipFile]::OpenRead($Path) try { $signed = $false foreach ($entry in $archive.Entries) { if ($entry.FullName -like 'META-INF/*.rsa' -or $entry.FullName -like 'META-INF/*.RSA') { $signed = $true } } $manifestEntry = $archive.Entries | Where-Object { $_.FullName -eq 'manifest.json' } | Select-Object -First 1 if (-not $manifestEntry) { throw "'$Path' enthaelt keine manifest.json auf oberster Ebene. Beim Packen muss der *Inhalt* des Ordners gezippt werden, nicht der Ordner selbst." } $reader = New-Object System.IO.StreamReader($manifestEntry.Open()) try { $manifestJson = $reader.ReadToEnd() } finally { $reader.Dispose() } $manifest = $manifestJson | ConvertFrom-Json $id = $null if ($manifest.PSObject.Properties.Name -contains 'browser_specific_settings') { $id = $manifest.browser_specific_settings.gecko.id } elseif ($manifest.PSObject.Properties.Name -contains 'applications') { $id = $manifest.applications.gecko.id } return [pscustomobject]@{ Id = $id Name = $manifest.name Version = $manifest.version Signed = $signed } } finally { $archive.Dispose() } } # ------------------------------------------------------------------ Policy function Set-ExtensionPolicy { param( [string]$InstallDirectory, [string]$Id, [string]$InstallUrl ) $distribution = Join-Path $InstallDirectory 'distribution' $policyFile = Join-Path $distribution 'policies.json' if (-not (Test-Path -LiteralPath $distribution)) { New-Item -ItemType Directory -Path $distribution -Force | Out-Null } $document = @{} if (Test-Path -LiteralPath $policyFile) { Backup-File -Path $policyFile $raw = Get-Content -LiteralPath $policyFile -Raw if ($raw.Trim()) { $document = ConvertTo-HashtableDeep ($raw | ConvertFrom-Json) } } if (-not ($document -is [hashtable])) { $document = @{} } if (-not $document.ContainsKey('policies') -or -not ($document['policies'] -is [hashtable])) { $document['policies'] = @{} } $policies = $document['policies'] if (-not $policies.ContainsKey('ExtensionSettings') -or -not ($policies['ExtensionSettings'] -is [hashtable])) { $policies['ExtensionSettings'] = @{} } $policies['ExtensionSettings'][$Id] = @{ installation_mode = 'force_installed' install_url = $InstallUrl updates_disabled = $true } if ($PSCmdlet.ShouldProcess($policyFile, "ExtensionSettings fuer $Id schreiben")) { Write-JsonFile -Path $policyFile -Data $document Write-Good "Policy geschrieben: $policyFile" } } function Remove-ExtensionPolicy { param([string]$InstallDirectory, [string]$Id) $policyFile = Join-Path (Join-Path $InstallDirectory 'distribution') 'policies.json' if (-not (Test-Path -LiteralPath $policyFile)) { Write-Info "Keine policies.json vorhanden - nichts zu entfernen." return } $raw = Get-Content -LiteralPath $policyFile -Raw if (-not $raw.Trim()) { return } $document = ConvertTo-HashtableDeep ($raw | ConvertFrom-Json) if (-not ($document -is [hashtable]) -or -not $document.ContainsKey('policies')) { return } $policies = $document['policies'] if (-not ($policies -is [hashtable]) -or -not $policies.ContainsKey('ExtensionSettings')) { Write-Info "Kein ExtensionSettings-Block - nichts zu entfernen." return } $settings = $policies['ExtensionSettings'] if (-not $settings.ContainsKey($Id)) { Write-Info "Kein Eintrag fuer $Id - nichts zu entfernen." return } Backup-File -Path $policyFile $settings.Remove($Id) # Leere Container nicht stehen lassen. if ($settings.Count -eq 0) { $policies.Remove('ExtensionSettings') } if ($PSCmdlet.ShouldProcess($policyFile, "Eintrag $Id entfernen")) { if ($policies.Count -eq 0) { Remove-Item -LiteralPath $policyFile -Force Write-Good "policies.json enthielt nur diesen Eintrag und wurde geloescht." } else { Write-JsonFile -Path $policyFile -Data $document Write-Good "Eintrag aus $policyFile entfernt." } } } # ------------------------------------------------------------------ Signaturpflicht function Set-SignatureEnforcement { param([string]$InstallDirectory, [bool]$Required) $prefDirectory = Join-Path $InstallDirectory 'defaults\pref' $autoconfigJs = Join-Path $prefDirectory 'autoconfig.js' $configCfg = Join-Path $InstallDirectory 'swyx-tab-bridge.cfg' if ($Required) { foreach ($file in @($autoconfigJs, $configCfg)) { if (Test-Path -LiteralPath $file) { Remove-Item -LiteralPath $file -Force Write-Good "Entfernt: $file" } } return } if (-not (Test-Path -LiteralPath $prefDirectory)) { New-Item -ItemType Directory -Path $prefDirectory -Force | Out-Null } $autoconfigContent = @' // Swyx Tab Bridge - autoconfig aktivieren pref("general.config.filename", "swyx-tab-bridge.cfg"); pref("general.config.obscure_value", 0); pref("general.config.sandbox_enabled", false); '@ # Die erste Zeile einer .cfg wird von Firefox immer ignoriert - deshalb der Kommentar. $configContent = @' // Swyx Tab Bridge lockPref("xpinstall.signatures.required", false); '@ if ($PSCmdlet.ShouldProcess($InstallDirectory, "Signaturpruefung per autoconfig abschalten")) { $ascii = New-Object System.Text.ASCIIEncoding [System.IO.File]::WriteAllText($autoconfigJs, $autoconfigContent, $ascii) [System.IO.File]::WriteAllText($configCfg, $configContent, $ascii) Write-Good "Signaturpruefung abgeschaltet ($configCfg)" } } # ------------------------------------------------------------------ Firefox-Prozess # Stehen mehrere Firefox-Installationen nebeneinander, laufen sie alle als # firefox.exe. Beruecksichtigt wird deshalb nur, was aus dem Zielverzeichnis # gestartet wurde - eine daneben laufende zweite Installation bleibt in Ruhe. function Get-FirefoxProcess { param([string]$InstallDirectory) $all = @(Get-Process -Name 'firefox' -ErrorAction SilentlyContinue) if (-not $InstallDirectory) { return $all } $matching = @() foreach ($process in $all) { $path = $null try { $path = $process.Path } catch { $path = $null } # Ohne lesbaren Pfad (fremder Benutzer) im Zweifel nicht anfassen. if ($path -and $path.StartsWith($InstallDirectory, [StringComparison]::OrdinalIgnoreCase)) { $matching += $process } } return $matching } function Stop-FirefoxProcess { param([string]$InstallDirectory) $running = @(Get-FirefoxProcess -InstallDirectory $InstallDirectory) if ($running.Count -eq 0) { Write-Info "Aus diesem Verzeichnis laeuft kein Firefox." return $false } if ($PSCmdlet.ShouldProcess("firefox.exe ($($running.Count) Prozesse)", "beenden")) { $running | Stop-Process -Force Start-Sleep -Seconds 2 Write-Good "Firefox beendet ($($running.Count) Prozesse) - die Sitzung wird beim naechsten Start wiederhergestellt." } return $true } function Test-FirefoxRunning { param([string]$InstallDirectory) return @(Get-FirefoxProcess -InstallDirectory $InstallDirectory).Count -gt 0 } # ------------------------------------------------------------------ Ergebnis pruefen function Get-InstalledAddonState { param([string]$ProfilePath, [string]$Id) $extensionsJson = Join-Path $ProfilePath 'extensions.json' if (-not (Test-Path -LiteralPath $extensionsJson)) { return $null } try { $data = Get-Content -LiteralPath $extensionsJson -Raw | ConvertFrom-Json } catch { return $null } if (-not ($data.PSObject.Properties.Name -contains 'addons')) { return $null } $addon = $data.addons | Where-Object { $_.id -eq $Id } | Select-Object -First 1 if (-not $addon) { return $null } return [pscustomobject]@{ Version = $addon.version Active = [bool]$addon.active Location = $addon.location } } # ------------------------------------------------------------------ Ablauf function Invoke-Install { param([string]$Xpi) Write-Step "Paket pruefen" if (-not (Test-Path -LiteralPath $Xpi)) { throw "Datei nicht gefunden: $Xpi" } $Xpi = (Resolve-Path -LiteralPath $Xpi).ProviderPath $xpiInfo = Get-XpiInfo -Path $Xpi Write-Info "Paket: $($xpiInfo.Name) $($xpiInfo.Version)" Write-Info "ID: $($xpiInfo.Id)" Write-Info "Signiert: $(if ($xpiInfo.Signed) { 'ja' } else { 'nein' })" if ($xpiInfo.Id -and $xpiInfo.Id -ne $ExtensionId) { throw "ID im Paket ('$($xpiInfo.Id)') passt nicht zu -ExtensionId ('$ExtensionId')." } if (-not $xpiInfo.Id) { throw "Im Manifest fehlt browser_specific_settings.gecko.id - ohne feste ID laesst sich das Add-on nicht per Policy verwalten." } Write-Step "Firefox suchen" $firefox = Select-FirefoxInstallation -Explicit $FirefoxDir Write-Info "$($firefox.Product) $($firefox.Version) [$($firefox.Channel)]" Write-Info "$($firefox.Directory)" if (-not $xpiInfo.Signed -and -not $AllowUnsigned) { throw "Das Paket ist nicht signiert. Firefox installiert es so nicht. Entweder mit 'web-ext sign --channel=unlisted' signieren, oder auf ESR/Developer Edition -AllowUnsigned verwenden." } # Nur ESR, Developer Edition und Nightly werten xpinstall.signatures.required # ueberhaupt aus; Release und Beta erzwingen die Signatur fest im Build. if ($AllowUnsigned -and @('release', 'beta') -contains $firefox.Channel) { Write-Note "Achtung: Diese Installation ist Firefox $($firefox.Channel). Dort wird xpinstall.signatures.required ignoriert - ein unsigniertes Add-on laeuft trotz -AllowUnsigned nicht." } if ($AllowUnsigned) { Write-Step "Signaturpruefung abschalten" if (-not (Test-DirectoryWritable $firefox.Directory)) { throw "Kein Schreibrecht auf '$($firefox.Directory)'. -AllowUnsigned erfordert Administratorrechte, wenn Firefox systemweit installiert ist." } Set-SignatureEnforcement -InstallDirectory $firefox.Directory -Required $false $Script:RestartNeeded = $true } if ($Mode -eq 'Policy') { Write-Step "Installation per Enterprise-Policy" if (-not (Test-DirectoryWritable $firefox.Directory)) { throw "Kein Schreibrecht auf '$($firefox.Directory)\distribution'. Bei systemweiter Installation Administratorrechte verwenden, sonst -Mode Profile." } # Bei einer Benutzerinstallation ist %ProgramData% nicht beschreibbar - # dann liegt das Paket im Benutzerprofil. if (-not (Test-DirectoryWritable $Script:XpiStore)) { $Script:XpiStore = Join-Path $env:LOCALAPPDATA 'SwyxTabBridge' Write-Info "Kein Schreibrecht in ProgramData - Ablage im Benutzerprofil." } if (-not (Test-Path -LiteralPath $Script:XpiStore)) { New-Item -ItemType Directory -Path $Script:XpiStore -Force | Out-Null } $target = Join-Path $Script:XpiStore "$ExtensionId.xpi" Copy-Item -LiteralPath $Xpi -Destination $target -Force Write-Info "Paket abgelegt: $target" Set-ExtensionPolicy -InstallDirectory $firefox.Directory -Id $ExtensionId -InstallUrl (ConvertTo-FileUrl $target) $Script:RestartNeeded = $true } else { Write-Step "Installation in die Profile von '$TargetUser'" $profiles = Get-FirefoxProfile -User $TargetUser foreach ($profileEntry in $profiles) { $extensionsDir = Join-Path $profileEntry.Path 'extensions' if (-not (Test-Path -LiteralPath $extensionsDir)) { New-Item -ItemType Directory -Path $extensionsDir -Force | Out-Null } $target = Join-Path $extensionsDir "$ExtensionId.xpi" if ($PSCmdlet.ShouldProcess($target, 'Add-on kopieren')) { Copy-Item -LiteralPath $Xpi -Destination $target -Force Write-Good "$($profileEntry.Name): $target" } } $Script:RestartNeeded = $true Write-Note "Firefox fragt beim naechsten Start einmal, ob das Add-on aktiviert werden soll." } Write-Step "Firefox" if ($StopFirefox) { Stop-FirefoxProcess -InstallDirectory $firefox.Directory | Out-Null $Script:RestartNeeded = $false } elseif (Test-FirefoxRunning -InstallDirectory $firefox.Directory) { Write-Note "Firefox laeuft. Das Add-on wird erst nach einem Neustart des Browsers aktiv (oder Skript mit -StopFirefox aufrufen)." } else { Write-Info "Firefox laeuft nicht - das Add-on wird beim naechsten Start aktiv." } Write-Step "Ergebnis" Write-Info "Ein Nachweis der aktiven Installation steht erst nach dem naechsten Firefox-Start in extensions.json." try { foreach ($profileEntry in (Get-FirefoxProfile -User $TargetUser)) { $state = Get-InstalledAddonState -ProfilePath $profileEntry.Path -Id $ExtensionId if ($state) { Write-Good "$($profileEntry.Name): Version $($state.Version), aktiv=$($state.Active), Quelle=$($state.Location)" } else { Write-Info "$($profileEntry.Name): noch nicht eingetragen (erwartet vor dem ersten Start)" } } } catch { Write-Info "Profilstatus nicht lesbar: $($_.Exception.Message)" } Write-Line Write-Good "Fertig. Danach im Firefox pruefen: about:addons, und about:policies#active fuer die Policy." Write-Info "Server-Adresse (Default ws://127.0.0.1:17655) ggf. ueber das Symbolleisten-Icon -> Einstellungen anpassen." } function Invoke-Uninstall { Write-Step "Firefox suchen" $firefox = Select-FirefoxInstallation -Explicit $FirefoxDir Write-Info "$($firefox.Product) $($firefox.Version) - $($firefox.Directory)" Write-Step "Policy entfernen" if (Test-DirectoryWritable $firefox.Directory) { Remove-ExtensionPolicy -InstallDirectory $firefox.Directory -Id $ExtensionId Set-SignatureEnforcement -InstallDirectory $firefox.Directory -Required $true } else { Write-Note "Kein Schreibrecht auf '$($firefox.Directory)' - Policy bleibt unveraendert." } Write-Step "Hinterlegtes Paket entfernen" $found = $false foreach ($store in @($Script:XpiStore, (Join-Path $env:LOCALAPPDATA 'SwyxTabBridge'))) { $stored = Join-Path $store "$ExtensionId.xpi" if (Test-Path -LiteralPath $stored) { Remove-Item -LiteralPath $stored -Force Write-Good "Geloescht: $stored" $found = $true } } if (-not $found) { Write-Info "Kein hinterlegtes Paket gefunden." } Write-Step "Profil-Kopien entfernen" try { foreach ($profileEntry in (Get-FirefoxProfile -User $TargetUser)) { $target = Join-Path (Join-Path $profileEntry.Path 'extensions') "$ExtensionId.xpi" if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target -Force Write-Good "$($profileEntry.Name): geloescht" } else { Write-Info "$($profileEntry.Name): nichts vorhanden" } } } catch { Write-Info "Profile nicht lesbar: $($_.Exception.Message)" } if ($StopFirefox) { Stop-FirefoxProcess -InstallDirectory $firefox.Directory | Out-Null } else { $Script:RestartNeeded = Test-FirefoxRunning -InstallDirectory $firefox.Directory } Write-Line Write-Good "Deinstallation abgeschlossen." } # ------------------------------------------------------------------ Einstieg try { Write-Line "Swyx Tab Bridge - Windows-Installation" Write-Info "Rechner: $env:COMPUTERNAME Benutzer: $env:USERNAME Admin: $(if (Test-Administrator) { 'ja' } else { 'nein' })" if ($Uninstall) { Invoke-Uninstall } else { if (-not $XpiPath) { throw "-XpiPath fehlt (Pfad zum .xpi auf diesem Rechner)." } Invoke-Install -Xpi $XpiPath } # Der Exitcode allein traegt nicht weit genug: laeuft das Skript ueber SSH, # reicht die Login-Shell auf dem Windows-Host alles ausser 0 als 1 durch. # Deshalb steht das Ergebnis zusaetzlich als Markerzeile in der Ausgabe. if ($Script:RestartNeeded) { Write-Note "Firefox muss noch neu gestartet werden." Write-Line "SWYX-STATUS: RESTART" exit 2 } Write-Line "SWYX-STATUS: OK" exit 0 } catch { Write-Line Write-Line "FEHLER: $($_.Exception.Message)" if ($_.ScriptStackTrace) { Write-Verbose $_.ScriptStackTrace } Write-Line "SWYX-STATUS: ERROR" exit 1 }