OfficeScrubC2R.ps1
# OfficeScrubC2R.ps1 # Office Click-to-Run removal script # Converted from VBS with C# optimizations for better performance <# .SYNOPSIS Removes Office Click-to-Run (C2R) products when regular uninstall is not possible. .DESCRIPTION This script provides comprehensive removal of Office 2013, 2016, and O365 C2R products using C# inline code for registry and file operations. .PARAMETER Quiet Run in quiet mode with minimal output. .PARAMETER DetectOnly Only detect installed products without removing them. .PARAMETER Force Force removal without user confirmation. .PARAMETER RemoveAll Remove all Office products. .PARAMETER KeepLicense Keep Office licensing information. .PARAMETER Offline Run in offline mode. .PARAMETER ForceArpUninstall Force ARP-based uninstall. .PARAMETER ClearTaskBand Clear taskband shortcuts. .PARAMETER UnpinMode Unpin shortcuts from taskbar. .PARAMETER SkipSD Skip scheduled deletion. .PARAMETER NoElevate Do not attempt elevation. .PARAMETER LogPath Specify custom log path. .EXAMPLE .\OfficeScrubC2R.ps1 -Quiet -Force .EXAMPLE .\OfficeScrubC2R.ps1 -DetectOnly -LogPath "C:\Logs" .NOTES Author: Microsoft Customer Support Services (Converted to PowerShell) Version: 2.19 Requires: PowerShell 5.1 or later, Administrator privileges #> [CmdletBinding()] param( [switch]$Quiet, [switch]$DetectOnly, [switch]$Force, [switch]$RemoveAll, [switch]$KeepLicense, [switch]$Offline, [switch]$ForceArpUninstall, [switch]$ClearTaskBand, [switch]$UnpinMode, [switch]$SkipSD, [switch]$NoElevate, [string]$LogPath ) # Import utility module Import-Module -Name (Join-Path $PSScriptRoot "OfficeScrubC2R-Utilities.psm1") -Force #region Main Script Functions function Initialize-Script { # Set script parameters FIRST (before any logging) $script:Quiet = $Quiet $script:DetectOnly = $DetectOnly $script:Force = $Force $script:RemoveAll = $RemoveAll $script:KeepLicense = $KeepLicense $script:Offline = $Offline $script:ForceArpUninstall = $ForceArpUninstall $script:ClearTaskBand = $ClearTaskBand $script:UnpinMode = $UnpinMode $script:SkipSD = $SkipSD $script:NoElevate = $NoElevate # Initialize error code $script:ErrorCode = $script:ERROR_SUCCESS # Get system information Get-SystemInfo # Initialize environment (MUST come before logging since it sets $script:LogDir) Initialize-Environment # Check elevation $script:IsElevated = Test-IsElevated if (-not $script:IsElevated -and -not $script:NoElevate) { Write-Warning "Error: Insufficient privileges - script requires Administrator rights" Set-ErrorCode $script:ERROR_ELEVATION return $false } # Initialize logging (now $script:LogDir is properly set) if ($LogPath) { $script:LogDir = $LogPath Initialize-Log $LogPath } else { Initialize-Log $script:LogDir } # NOW we can use logging functions Write-LogHeader ("Office C2R Scrubber v{0} - Initialization" -f $script:SCRIPT_VERSION) Write-Log ("System Information: {0}" -f $script:OSInfo) Write-Log ("64-bit System: {0}" -f $script:Is64Bit) Write-Log ("Elevated: {0}" -f $script:IsElevated) return $true } function Find-InstalledOfficeProducts { Write-LogSubHeader "Stage # 0 - Basic detection" # Ensure Windows Installer metadata integrity Write-LogSubHeader "Ensure Windows Installer metadata integrity" Ensure-ValidWIMetadata -Hive CurrentUser -SubKey "Software\Classes\Installer\Products" -ValidLength 32 Ensure-ValidWIMetadata -Hive ClassesRoot -SubKey "Installer\Products" -ValidLength 32 Ensure-ValidWIMetadata -Hive LocalMachine -SubKey "SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products" -ValidLength 32 Ensure-ValidWIMetadata -Hive LocalMachine -SubKey "SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Components" -ValidLength 32 Ensure-ValidWIMetadata -Hive ClassesRoot -SubKey "Installer\Components" -ValidLength 32 # Build list of installed Office products $script:InstalledSku = Get-InstalledOfficeProducts if ($script:C2RSuite.Count -gt 0) { Write-Log "Registered ARP product(s) found:" foreach ($key in $script:C2RSuite.Keys) { Write-Log (" - {0} - {1}" -f $key, $script:C2RSuite[$key]) } } else { Write-Log "No registered product(s) found" } return $script:InstalledSku.Count -gt 0 } function Ensure-ValidWIMetadata { param( [Microsoft.Win32.RegistryHive]$Hive, [string]$SubKey, [int]$ValidLength ) try { $values = Get-RegistryValues -Hive $Hive -SubKey $SubKey foreach ($valueName in $values) { $value = Get-RegistryValue -Hive $Hive -SubKey $SubKey -ValueName $valueName if ($value -and $value.Length -lt $ValidLength) { Write-LogOnly "Removing invalid WI metadata: $valueName" Remove-RegistryValue -Hive $Hive -SubKey $SubKey -ValueName $valueName } } } catch { Write-LogOnly "Error ensuring WI metadata integrity: $($_.Exception.Message)" } } function Uninstall-OfficeProducts { if ($script:ErrorCode -band $script:ERROR_USERCANCEL) { return } Write-LogSubHeader "Stage # 1 - Uninstall" # Clean licenses first Clear-OfficeLicenses # Stop Office processes Write-LogSubHeader "End running processes" if ($script:C2RSuite.Count -eq 0 -or -not $script:KeepSku) { Clear-ShellIntegration } Stop-OfficeProcesses # Remove scheduled tasks if (-not $script:DetectOnly) { Remove-ScheduledTasks } # Unpin and clean shortcuts while they're still valid Write-LogSubHeader "Clean shortcuts" Clear-Shortcuts -RootPath $script:AllUsersProfile -Delete -Unpin if (Test-Path $env:SystemDrive\Users) { Clear-Shortcuts -RootPath "$env:SystemDrive\Users" -Delete -Unpin } # Check OSE service state Write-LogSubHeader "Check state of OSE service" $oseServices = Get-CimInstance -ClassName Win32_Service -Filter "Name LIKE 'ose%'" foreach ($service in $oseServices) { if ($service.StartMode -eq "Disabled") { Write-Log ("Conflict detected: OSE service is disabled" -f $service.StartMode) [void]($service.ChangeStartMode("Manual")) } if ($service.StartName -ne "LocalSystem") { Write-Log ("Conflict detected: OSE service not running as LocalSystem" -f $service.StartName) [void]($service.Change($null, $null, $null, $null, $null, $null, "LocalSystem", "")) } } if ($script:C2RSuite.Count -eq 0) { Write-Log ("No uninstallable C2R items registered in Uninstall: {0}" -f $script:C2RSuite.Count) } # Call ODT-based uninstall Uninstall-OfficeC2R # Remove published component registration Write-LogSubHeader ("Remove published component registration for C2R packages: {0}" -f $script:C2RSuite.Count) Remove-PublishedComponents # Remove C2R and App-V registry data Write-LogSubHeader ("Remove C2R and App-V registry data: {0}" -f $script:C2RSuite.Count) Remove-C2RRegistryData # MSI-based uninstall Uninstall-MSIProducts } function Uninstall-OfficeC2R { Write-LogSubHeader ("Uninstalling Office C2R using ODT: {0}" -f $script:C2RSuite.Count) # Build removal XML $removeXml = Build-RemoveXml if ($removeXml) { $configPath = Join-Path $script:ScrubDir "RemoveAll.xml" Set-Content -Path $configPath -Value $removeXml -Encoding UTF8 # Download and run ODT $odtPath = Join-Path $script:ScrubDir "setup.exe" if (Download-ODT -Url "https://download.microsoft.com/download/2/7/A/27AF1BE6-DD18-4A8E-8E0B-75C0FEC274F4/OfficeDeploymentTool_12325-20288.exe" -LocalPath $odtPath) { $odtArgs = "/configure `"$configPath`"" if ($script:Quiet) { if (-not ($odtArgs -is [System.Collections.Generic.List[string]])) { $odtArgs = [System.Collections.Generic.List[string]]@($odtArgs) } $odtArgs.Add("/quiet") } Write-Log ("Running ODT: {0} {1}" -f $odtPath, $odtArgs) if (-not $script:DetectOnly) { $result = Start-Process -FilePath $odtPath -ArgumentList $odtArgs -Wait -PassThru Write-Log ("ODT returned: {0}" -f $result.ExitCode) if ($result.ExitCode -eq 3010) { $script:RebootRequired = $true Set-ErrorCode $script:ERROR_REBOOT_REQUIRED } } } } } function Build-RemoveXml { $xml = @" <?xml version="1.0" encoding="utf-8"?> <Configuration> <Remove All="True" /> <Display Level="None" AcceptEULA="True" /> <Property Name="FORCEAPPSHUTDOWN" Value="True" /> </Configuration> "@ return $xml } function Download-ODT { param( [string]$Url, [string]$LocalPath ) try { Write-Log ("Downloading ODT from: {0}" -f $Url) if (-not $script:DetectOnly) { Invoke-WebRequest -Uri $Url -OutFile $LocalPath -UseBasicParsing } return $true } catch { Write-Log ("Failed to download ODT: {0}" -f $_.Exception.Message) return $false } } function Remove-PublishedComponents { $packageFolders = @( @{ Version = "15.0"; Key = "SOFTWARE\Microsoft\Office\15.0\ClickToRun" }, @{ Version = "16.0"; Key = "SOFTWARE\Microsoft\Office\16.0\ClickToRun" }, @{ Version = "Current"; Key = "SOFTWARE\Microsoft\Office\ClickToRun" } ) # Optimize by collecting all manifest files in one go and using array operations $allManifestFiles = @() $integratorTasks = @() foreach ($pkg in $packageFolders) { $packageFolder = Get-RegistryValue -Hive LocalMachine -SubKey $pkg.Key -ValueName "PackageFolder" $packageGuid = Get-RegistryValue -Hive LocalMachine -SubKey $pkg.Key -ValueName "PackageGUID" $integrationPath = "$packageFolder\root\Integration" if ($packageFolder -and (Test-Path $integrationPath)) { # Collect manifest files for batch processing $manifestFiles = Get-ChildItem -Path $integrationPath -Filter "C2RManifest*.xml" -ErrorAction SilentlyContinue if ($manifestFiles) { $allManifestFiles += $manifestFiles } # Prepare integrator tasks for later execution $integratorPath = "$integrationPath\integrator.exe" if (Test-Path $integratorPath) { $integratorArgs = "/U /Extension PackageRoot=`"$packageFolder\root`" PackageGUID=$packageGuid" $integratorTasks += [PSCustomObject]@{ Path = $integratorPath Args = $integratorArgs } } } } # Delete all manifest files in one go using .NET methods for speed if ($allManifestFiles.Count -gt 0) { $filePaths = $allManifestFiles | ForEach-Object { $_.FullName } Write-Log ("Deleting {0} manifest files..." -f $filePaths.Count) foreach ($filePath in $filePaths) { Write-Log ("Deleting manifest file: {0}" -f $filePath) } if (-not $script:DetectOnly) { # Use [System.IO.File]::Delete for performance foreach ($filePath in $filePaths) { try { [System.IO.File]::Delete($filePath) } catch { Write-Log ("Failed to delete manifest file: {0} - {1}" -f $filePath, $_.Exception.Message) } } } } # Run all integrator tasks foreach ($task in $integratorTasks) { Write-Log ("Running integrator: {0} {1}" -f $task.Path, $task.Args) if (-not $script:DetectOnly) { $result = Start-Process -FilePath $task.Path -ArgumentList $task.Args -Wait -PassThru Write-Log ("Integrator returned: {0}" -f $result.ExitCode) } } } function Remove-C2RRegistryData { # Remove ARP entries foreach ($sku in $script:C2RSuite.Keys) { Remove-RegistryKey -Hive LocalMachine -SubKey "$script:REG_ARP$sku" } # Remove C2R registry keys $c2rKeys = @( "SOFTWARE\Microsoft\Office\15.0\ClickToRun", "SOFTWARE\Microsoft\Office\16.0\ClickToRun", "SOFTWARE\Microsoft\Office\ClickToRun" ) foreach ($key in $c2rKeys) { Remove-RegistryKey -Hive CurrentUser -SubKey $key Remove-RegistryKey -Hive LocalMachine -SubKey $key } # Remove App-V keys Remove-AppVRegistryKeys } function Remove-AppVRegistryKeys { $appVKeys = @( "SOFTWARE\Microsoft\AppV\ISV", "SOFTWARE\Microsoft\AppVISV" ) foreach ($key in $appVKeys) { foreach ($hive in @([Microsoft.Win32.RegistryHive]::CurrentUser, [Microsoft.Win32.RegistryHive]::LocalMachine)) { $values = Get-RegistryValues -Hive $hive -SubKey $key foreach ($valueName in $values) { if (Test-IsC2R $valueName) { Write-LogOnly "Removing App-V C2R value: $valueName" Remove-RegistryValue -Hive $hive -SubKey $key -ValueName $valueName } } } } } function Uninstall-MSIProducts { Write-LogSubHeader "Detect MSI-based products" try { $msi = New-Object -ComObject WindowsInstaller.Installer $products = $msi.Products # Optimize by filtering in-scope products first, then process in batch $inScopeProducts = @() $outOfScopeProducts = @() foreach ($product in $products) { if (Test-ProductInScope $product) { $inScopeProducts += $product } else { $outOfScopeProducts += $product } } if ($outOfScopeProducts.Count -gt 0) { $outOfScopeProducts | ForEach-Object { Write-LogOnly "Skip out of scope product: $_" } } if ($inScopeProducts.Count -gt 0) { # Prepare msiexec commands and log files in advance $msiexecArgsList = @() foreach ($product in $inScopeProducts) { Write-Log ("Call msiexec.exe to remove {0}" -f $product) $logFile = Join-Path $script:LogDir "Uninstall_$product.log" $args = @("/x$product", "REBOOT=ReallySuppress", "NOREMOVESPAWN=True") if ($script:Quiet) { $args += "/q" } else { $args += "/qb-!" } $args += "/l*v" $args += "`"$logFile`"" $msiexecArgsList += , @($product, $args, $logFile) Write-LogOnly "Call msiexec with 'msiexec.exe $($args -join ' ')'" } Stop-OfficeProcesses if (-not $script:DetectOnly) { foreach ($item in $msiexecArgsList) { $product = $item[0] $args = $item[1] $logFile = $item[2] $result = Start-Process -FilePath "msiexec.exe" -ArgumentList $args -Wait -PassThru Write-Log ("msiexec returned: {0}" -f $result.ExitCode) if ($result.ExitCode -eq 3010) { $script:RebootRequired = $true Set-ErrorCode $script:ERROR_REBOOT_REQUIRED } } } } # Stop MSI server if (-not $script:DetectOnly) { Start-Process -FilePath "cmd.exe" -ArgumentList "/c", "net", "stop", "msiserver" -WindowStyle Hidden } } catch { Write-Log ("Error during MSI uninstall: {0}" -f $_.Exception.Message) Set-ErrorCode $script:ERROR_STAGE1 } } function Test-ProductInScope { param([string]$ProductCode) # Simplified scope check - in real implementation, this would be more comprehensive $productCodeLower = $ProductCode.ToLower() $c2rPatterns = @("office", "o365", "clicktorun") foreach ($pattern in $c2rPatterns) { if ($productCodeLower -like "*$pattern*") { return $true } } return $false } # File removal is now handled in Complete-Cleanup to match VBS flow function Clean-OfficeRegistry { Write-LogSubHeader "Stage # 2 - CleanUp - Registry" Stop-OfficeProcesses # HKCU Registration Remove-RegistryKey -Hive CurrentUser -SubKey "Software\Microsoft\Office\15.0\Registration" Remove-RegistryKey -Hive CurrentUser -SubKey "Software\Microsoft\Office\16.0\Registration" Remove-RegistryKey -Hive CurrentUser -SubKey "Software\Microsoft\Office\Registration" # Virtual InstallRoot Remove-RegistryKey -Hive LocalMachine -SubKey "SOFTWARE\Microsoft\Office\15.0\Common\InstallRoot\Virtual" Remove-RegistryKey -Hive LocalMachine -SubKey "SOFTWARE\Microsoft\Office\16.0\Common\InstallRoot\Virtual" Remove-RegistryKey -Hive LocalMachine -SubKey "SOFTWARE\Microsoft\Office\Common\InstallRoot\Virtual" # Mapi Search reg if ($script:KeepSku.Count -eq 0) { Remove-RegistryKey -Hive LocalMachine -SubKey "SOFTWARE\Classes\CLSID\{2027FC3B-CF9D-4ec7-A823-38BA308625CC}" } # C2R keys (already removed in earlier stage, but ensure cleanup) Remove-RegistryKey -Hive CurrentUser -SubKey "SOFTWARE\Microsoft\Office\15.0\ClickToRun" Remove-RegistryKey -Hive LocalMachine -SubKey "SOFTWARE\Microsoft\Office\15.0\ClickToRun" Remove-RegistryKey -Hive LocalMachine -SubKey "SOFTWARE\Microsoft\Office\15.0\ClickToRunStore" Remove-RegistryKey -Hive CurrentUser -SubKey "SOFTWARE\Microsoft\Office\16.0\ClickToRun" Remove-RegistryKey -Hive LocalMachine -SubKey "SOFTWARE\Microsoft\Office\16.0\ClickToRun" Remove-RegistryKey -Hive LocalMachine -SubKey "SOFTWARE\Microsoft\Office\16.0\ClickToRunStore" Remove-RegistryKey -Hive CurrentUser -SubKey "SOFTWARE\Microsoft\Office\ClickToRun" Remove-RegistryKey -Hive LocalMachine -SubKey "SOFTWARE\Microsoft\Office\ClickToRun" Remove-RegistryKey -Hive LocalMachine -SubKey "SOFTWARE\Microsoft\Office\ClickToRunStore" # Office key in HKLM if ($script:KeepSku.Count -eq 0) { Remove-RegistryKey -Hive LocalMachine -SubKey "Software\Microsoft\Office\15.0" Remove-RegistryKey -Hive LocalMachine -SubKey "Software\Microsoft\Office\16.0" } Clear-OfficeHKLM "SOFTWARE\Microsoft\Office" # Run key Clear-RunKeyEntries # ARP (configuration entries already removed, clean product entries) Clear-ARPEntries # Windows Installer metadata Clear-WindowsInstallerMetadata # TypeLib cleanup Clear-TypeLibRegistrations } function Clear-OfficeHKLM { param([string]$SubKey) # Recursively clean Office HKLM key of C2R references $keys = Get-RegistryKeys -Hive LocalMachine -SubKey $SubKey foreach ($key in $keys) { Clear-OfficeHKLM "$SubKey\$key" } # Check values $values = Get-RegistryValues -Hive LocalMachine -SubKey $SubKey foreach ($value in $values) { $data = Get-RegistryValue -Hive LocalMachine -SubKey $SubKey -ValueName $value if ($data -and (Test-IsC2R $data.ToString())) { Remove-RegistryValue -Hive LocalMachine -SubKey $SubKey -ValueName $value } } # Clean empty keys if (($keys.Count -eq 0 -or -not $keys) -and ($values.Count -eq 0 -or -not $values) -and ($script:KeepSku.Count -eq 0)) { Remove-RegistryKey -Hive LocalMachine -SubKey $SubKey } } function Clear-RunKeyEntries { $runKey = "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" $values = Get-RegistryValues -Hive LocalMachine -SubKey $runKey foreach ($value in $values) { $data = Get-RegistryValue -Hive LocalMachine -SubKey $runKey -ValueName $value if ($data -and (Test-IsC2R $data.ToString())) { Remove-RegistryValue -Hive LocalMachine -SubKey $runKey -ValueName $value } } Remove-RegistryValue -Hive LocalMachine -SubKey $runKey -ValueName "Lync15" Remove-RegistryValue -Hive LocalMachine -SubKey $runKey -ValueName "Lync16" } function Clear-ARPEntries { $arpKeys = Get-RegistryKeys -Hive LocalMachine -SubKey $script:REG_ARP foreach ($key in $arpKeys) { if ($key.Length -gt 37) { $guid = $key.Substring(0, 38).ToUpper() if (Test-ProductInScope $guid) { Remove-RegistryKey -Hive LocalMachine -SubKey "$($script:REG_ARP)$key" } } } } # Shell integration, shortcuts, and services are now handled in their respective cleanup stages function Remove-ScheduledTasks { Write-LogSubHeader "Remove scheduled tasks" $officeTasks = @( "FF_INTEGRATEDstreamSchedule", "FF_INTEGRATEDUPDATEDETECTION", "C2RAppVLoggingStart", "Office 15 Subscription Heartbeat", "Microsoft Office 15 Sync Maintenance for {d068b555-9700-40b8-992c-f866287b06c1}", "\Microsoft\Office\OfficeInventoryAgentFallBack", "\Microsoft\Office\OfficeTelemetryAgentFallBack", "\Microsoft\Office\OfficeInventoryAgentLogOn", "\Microsoft\Office\OfficeTelemetryAgentLogOn", "Office Background Streaming", "\Microsoft\Office\Office Automatic Updates", "\Microsoft\Office\Office ClickToRun Service Monitor", "Office Subscription Maintenance" ) foreach ($taskName in $officeTasks) { try { Write-LogOnly "Removing scheduled task: $taskName" if (-not $script:DetectOnly) { $null = Start-Process -FilePath "schtasks.exe" -ArgumentList "/Delete", "/TN", "`"$taskName`"", "/F" ` -WindowStyle Hidden -Wait -ErrorAction SilentlyContinue Start-Sleep -Milliseconds 500 } } catch { Write-LogOnly "Error removing scheduled task $taskName : $_" } } # Also try PowerShell cmdlets for pattern matching try { $tasks = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -like "*Office*" -or $_.TaskPath -like "*\Microsoft\Office\*" } foreach ($task in $tasks) { try { Write-LogOnly "Removing scheduled task (PS): $($task.TaskName)" if (-not $script:DetectOnly) { Unregister-ScheduledTask -TaskName $task.TaskName -TaskPath $task.TaskPath -Confirm:$false -ErrorAction SilentlyContinue } } catch { Write-LogOnly "Error removing task $($task.TaskName): $_" } } } catch { Write-LogOnly "Error enumerating scheduled tasks: $_" } } # License cleanup is now handled via Clear-OfficeLicenses in utilities module function Complete-Cleanup { Write-LogSubHeader "Stage # 2 - CleanUp - Files" if ($script:ErrorCode -band $script:ERROR_USERCANCEL) { return } Stop-OfficeProcesses Remove-ScheduledTasks # Delete Services Write-LogSubHeader "Delete Services" Write-Log "Delete OfficeSvc service" Remove-Service -ServiceName "OfficeSvc" Write-Log "Delete ClickToRunSvc service" Remove-Service -ServiceName "ClickToRunSvc" # Add additional processes to termination list $additionalProcesses = @("explorer.exe", "msiexec.exe", "ose.exe") if ($script:Orchestrator) { $terminated = $script:Orchestrator.Processes.TerminateProcesses($additionalProcesses, 5000) if ($terminated.Count -gt 0) { Write-LogOnly "Terminated $($terminated.Count) additional process(es)" } } # Delete C2R package files and Office folders Write-LogSubHeader "Delete Files and Folders" $fDelFolders = $false $checkPaths = @( "$script:ProgramFiles\Microsoft Office 15", "$script:ProgramFiles\Microsoft Office 16", "$script:ProgramFiles\Microsoft Office\PackageManifests" ) if ($script:Is64Bit) { $checkPaths += "$script:ProgramFilesX86\Microsoft Office\PackageManifests" } foreach ($path in $checkPaths) { if (Test-Path $path) { $fDelFolders = $true Write-Log "Attention: Now closing Explorer.exe for file delete operations" Write-Log "Explorer will automatically restart." Start-Sleep -Seconds 2 Stop-OfficeProcesses break } } # Delete Office folders Write-LogSubHeader "Delete Office folders" $officeFolders = @( "$script:ProgramFiles\Microsoft Office 15", "$script:ProgramFiles\Microsoft Office 16" ) if ($script:Is64Bit) { $officeFolders += @( "$script:CommonProgramFilesX86\Microsoft Office 15", "$script:CommonProgramFilesX86\Microsoft Office 16" ) } foreach ($folder in $officeFolders) { if (Test-Path $folder) { Remove-FolderRecursive -Path $folder -Force } } if ($fDelFolders) { $rootFolders = @( "$script:ProgramFiles\Microsoft Office\PackageManifests", "$script:ProgramFiles\Microsoft Office\PackageSunrisePolicies", "$script:ProgramFiles\Microsoft Office\root", "$script:ProgramFiles\Microsoft Office\AppXManifest.xml", "$script:ProgramFiles\Microsoft Office\FileSystemMetadata.xml" ) if ($script:KeepSku.Count -eq 0) { $rootFolders += @( "$script:ProgramFiles\Microsoft Office\Office16", "$script:ProgramFiles\Microsoft Office\Office15" ) } if ($script:Is64Bit) { $rootFolders += @( "$script:ProgramFilesX86\Microsoft Office\PackageManifests", "$script:ProgramFilesX86\Microsoft Office\PackageSunrisePolicies", "$script:ProgramFilesX86\Microsoft Office\root", "$script:ProgramFilesX86\Microsoft Office\AppXManifest.xml", "$script:ProgramFilesX86\Microsoft Office\FileSystemMetadata.xml" ) if ($script:KeepSku.Count -eq 0) { $rootFolders += @( "$script:ProgramFilesX86\Microsoft Office\Office16", "$script:ProgramFilesX86\Microsoft Office\Office15" ) } } foreach ($item in $rootFolders) { if (Test-Path $item) { if ((Get-Item $item) -is [System.IO.DirectoryInfo]) { Remove-FolderRecursive -Path $item -Force } else { Remove-FileForced -Path $item -ScheduleOnFail } } } } # Additional cleanup paths $additionalPaths = @( "$script:ProgramData\Microsoft\ClickToRun", "$script:CommonProgramFiles\microsoft shared\ClickToRun", "$script:ProgramData\Microsoft\office\FFPackageLocker", "$script:ProgramData\Microsoft\office\ClickToRunPackageLocker" ) foreach ($path in $additionalPaths) { if (Test-Path $path) { Remove-FolderRecursive -Path $path -Force } } # Check for file-based entries that need deletion $fileEntries = @( "$script:ProgramData\Microsoft\office\FFPackageLocker", "$script:ProgramData\Microsoft\office\FFStatePBLocker" ) foreach ($file in $fileEntries) { if ((Test-Path $file) -and -not ((Get-Item $file -ErrorAction SilentlyContinue) -is [System.IO.DirectoryInfo])) { Remove-FileForced -Path $file -ScheduleOnFail } } if ($script:KeepSku.Count -eq 0) { Remove-FolderRecursive -Path "$script:ProgramData\Microsoft\office\Heartbeat" -Force } # User profile folders $userProfilePaths = @( "$env:USERPROFILE\Microsoft Office", "$env:USERPROFILE\Microsoft Office 15", "$env:USERPROFILE\Microsoft Office 16" ) foreach ($path in $userProfilePaths) { if (Test-Path $path) { Remove-FolderRecursive -Path $path -Force } } # Restore explorer if ($script:Orchestrator) { Write-Log "Restoring Explorer..." $script:Orchestrator.Shell.RestartExplorer() } # Delete shortcuts Write-LogSubHeader "Search and delete shortcuts" Clear-Shortcuts -RootPath $script:AllUsersProfile -Delete if (Test-Path "$env:SystemDrive\Users") { Clear-Shortcuts -RootPath "$env:SystemDrive\Users" -Delete } # Delete empty folders Remove-EmptyFolders # Add pending deletes to registry if any if ($script:DelInUse.Count -gt 0) { Write-LogSubHeader "Add $($script:DelInUse.Count) PendingFileRenameOperations" foreach ($path in $script:DelInUse.Keys) { Write-LogOnly " $path" $script:Orchestrator.Registry.AddPendingFileRenameOperation($path) } } } function Remove-EmptyFolders { $foldersToCheck = @( "$script:CommonProgramFiles\Microsoft Shared\Office15", "$script:CommonProgramFiles\Microsoft Shared\Office16", "$script:CommonProgramFiles\Microsoft Shared", "$script:ProgramFiles\Microsoft Office\Office15", "$script:ProgramFiles\Microsoft Office\Office16" ) foreach ($folder in $foldersToCheck) { if ((Test-Path $folder) -and (Get-ChildItem $folder -Force | Measure-Object).Count -eq 0) { Write-LogOnly "Removing empty folder: $folder" if (-not $script:DetectOnly) { Remove-Item $folder -Force -ErrorAction SilentlyContinue } } } } # Schedule-DeleteInUseFiles is handled in Complete-Cleanup via PendingFileRenameOperations function Show-Summary { Write-LogHeader "Stage # 3 - Exit" # Update return value Set-ReturnValue $script:ErrorCode # Log detailed results if ($script:ErrorCode -band $script:ERROR_INCOMPLETE) { Write-LogSubHeader ("Removal result: {0} - INCOMPLETE. Uninstall requires a system reboot to complete." -f $script:ErrorCode) } else { $status = " - SUCCESS" if ($script:ErrorCode -band $script:ERROR_USERCANCEL) { $status = " - USER CANCELED" } if ($script:ErrorCode -band $script:ERROR_FAIL) { $status = " - FAIL" } Write-LogSubHeader ("Removal result: {0}{1}" -f $script:ErrorCode, $status) } # Log individual error flags if ($script:ErrorCode -band $script:ERROR_FAIL) { if ($script:ErrorCode -band $script:ERROR_REBOOT_REQUIRED) { Write-Log " - Reboot required" } if ($script:ErrorCode -band $script:ERROR_USERCANCEL) { Write-Log " - User cancel" } if ($script:ErrorCode -band $script:ERROR_STAGE1) { Write-Log " - Msiexec failed" } if ($script:ErrorCode -band $script:ERROR_STAGE2) { Write-Log " - Cleanup failed" } if ($script:ErrorCode -band $script:ERROR_INCOMPLETE) { Write-Log " - Removal incomplete. Rerun after reboot needed" } if ($script:ErrorCode -band $script:ERROR_DCAF_FAILURE) { Write-Log " - Second attempt cleanup still incomplete" } if ($script:ErrorCode -band $script:ERROR_ELEVATION_USERDECLINED) { Write-Log " - User declined elevation" } if ($script:ErrorCode -band $script:ERROR_ELEVATION) { Write-Log " - Elevation failed" } if ($script:ErrorCode -band $script:ERROR_SCRIPTINIT) { Write-Log " - Initialization error" } if ($script:ErrorCode -band $script:ERROR_RELAUNCH) { Write-Log " - Unhandled error during relaunch attempt" } if ($script:ErrorCode -band $script:ERROR_UNKNOWN) { Write-Log " - Unknown error" } } Write-LogSubHeader "Removal end." # Reboot handling if ($script:RebootRequired) { Write-Log "" Write-Log "====================================================================" Write-Log "REBOOT REQUIRED - System restart needed to complete uninstall" Write-Log "====================================================================" if (-not $script:Quiet) { $response = Read-Host "Do you want to reboot now? (Y/N)" if ($response -match "^[Yy]") { Write-Log "Initiating system reboot..." Restart-Computer -Force } } } Write-Log ("Final exit code: {0}" -f $script:ErrorCode) } #endregion #region Main Execution function Main { try { # Initialize script Write-LogHeader "Initialization" if (-not (Initialize-Script)) { return $script:ERROR_SCRIPTINIT } # Clear init error on success Clear-ErrorCode $script:ERROR_SCRIPTINIT #----------------------------- # Stage # 0 - Basic detection #----------------------------- Write-LogHeader "Stage # 0 - Basic detection" # Find installed Office products if (-not (Find-InstalledOfficeProducts)) { Write-Log ("No Office products found to remove") Show-Summary return $script:ERROR_SUCCESS } if ($script:DetectOnly) { Write-Log ("Detection complete - no removal performed") Show-Summary return $script:ERROR_SUCCESS } # Confirm removal unless forced or quiet if (-not $script:Force -and -not $script:Quiet) { $confirmation = Read-Host ("Are you sure you want to remove all Office C2R products? (Y/N)") if ($confirmation -notmatch "^[Yy]") { Write-Log ("User cancelled removal") Set-ErrorCode $script:ERROR_USERCANCEL Show-Summary return $script:ERROR_USERCANCEL } } #----------------------- # Stage # 1 - Uninstall #----------------------- Uninstall-OfficeProducts #--------------------- # Stage # 2 - CleanUp #--------------------- # Registry cleanup Clean-OfficeRegistry # File cleanup Complete-Cleanup #------------------ # Stage # 3 - Exit #------------------ # Ensure Explorer is running if ($script:Orchestrator) { $script:Orchestrator.Shell.RestartExplorer() } # Show summary Show-Summary return $script:ErrorCode } catch { Write-Log ("Fatal error: {0}" -f $_.Exception.Message) Write-LogOnly ("Stack trace: {0}" -f $_.ScriptStackTrace) Set-ErrorCode $script:ERROR_UNKNOWN Show-Summary return $script:ERROR_UNKNOWN } finally { # Always close log Close-Log } } # Execute main function only if script is run directly (not dot-sourced) if ($MyInvocation.InvocationName -ne '.') { $exitCode = Main exit $exitCode } #endregion |