795 lines
30 KiB
PowerShell
795 lines
30 KiB
PowerShell
#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 <FirefoxDir>\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 <Profil>\extensions\<ExtensionId>.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\<Produkt>\<Version>\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
|
|
}
|