From 97950c275cf82a209ecd610df7834b3b4919ecd1 Mon Sep 17 00:00:00 2001 From: Calvindd2f <74103103+Calvindd2f@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:50:31 +0000 Subject: [PATCH] Add native build verification script --- .gitignore | 3 + OfficeScrubC2R-Utilities.ps1 | 1028 ----------------------------- OfficeScrubC2R-Utilities.psm1 | 1157 --------------------------------- OfficeScrubC2R.ps1 | 1040 ----------------------------- OfficeScrubC2R.psd1 | 58 +- OfficeScrubC2R.psm1 | 291 +++------ README.md | 304 ++------- docs/REPOSITORY_STRUCTURE.md | 282 -------- rebuild.ps1 | 39 -- tests/Test-OfficeScrubC2R.ps1 | 660 ------------------- tests/Verify-NativeBuild.ps1 | 142 ++++ tests/shit.test.ps1 | 3 - 12 files changed, 281 insertions(+), 4726 deletions(-) delete mode 100644 OfficeScrubC2R-Utilities.ps1 delete mode 100644 OfficeScrubC2R-Utilities.psm1 delete mode 100644 OfficeScrubC2R.ps1 delete mode 100644 docs/REPOSITORY_STRUCTURE.md delete mode 100644 rebuild.ps1 delete mode 100644 tests/Test-OfficeScrubC2R.ps1 create mode 100644 tests/Verify-NativeBuild.ps1 delete mode 100644 tests/shit.test.ps1 diff --git a/.gitignore b/.gitignore index 57a1b3f..7986087 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,6 @@ scratch/ # Keep these artifacts (exceptions to above rules) !OfficeScrubNative.dll + +# Verification artifacts +artifacts/ diff --git a/OfficeScrubC2R-Utilities.ps1 b/OfficeScrubC2R-Utilities.ps1 deleted file mode 100644 index 294767d..0000000 --- a/OfficeScrubC2R-Utilities.ps1 +++ /dev/null @@ -1,1028 +0,0 @@ -# OfficeScrubC2R-Utilities.psm1 -# High-performance utility functions for Office C2R removal -# Uses C# for performance-critical operations - -using namespace System -using namespace System.Collections.Generic -using namespace System.IO -using namespace System.Management -using namespace System.Security.Principal -using namespace Microsoft.Win32 - -#region C# Performance Optimizations - -# C# class for high-performance registry operations -Add-Type -TypeDefinition @" -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using Microsoft.Win32; - -public class FastRegistryOperations -{ - [DllImport("advapi32.dll", SetLastError = true)] - private static extern int RegOpenKeyEx(IntPtr hKey, string subKey, int options, int samDesired, out IntPtr phkResult); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern int RegCloseKey(IntPtr hKey); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern int RegQueryValueEx(IntPtr hKey, string lpValueName, IntPtr lpReserved, out uint lpType, IntPtr lpData, ref uint lpcbData); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern int RegEnumKeyEx(IntPtr hKey, uint dwIndex, System.Text.StringBuilder lpName, ref uint lpcbName, IntPtr lpReserved, IntPtr lpClass, IntPtr lpcbClass, IntPtr lpftLastWriteTime); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern int RegEnumValue(IntPtr hKey, uint dwIndex, System.Text.StringBuilder lpValueName, ref uint lpcbValueName, IntPtr lpReserved, out uint lpType, IntPtr lpData, IntPtr lpcbData); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern int RegDeleteKey(IntPtr hKey, string lpSubKey); - - [DllImport("advapi32.dll", SetLastError = true)] - private static extern int RegDeleteValue(IntPtr hKey, string lpValueName); - - private const int KEY_READ = 0x20019; - private const int KEY_WRITE = 0x20006; - private const int KEY_ALL_ACCESS = 0xF003F; - - public static Dictionary GetRegistrySubKeys(RegistryHive hive, string subKey) - { - var result = new Dictionary(); - IntPtr hKey = IntPtr.Zero; - - try - { - IntPtr hHive = GetHiveHandle(hive); - if (RegOpenKeyEx(hHive, subKey, 0, KEY_READ, out hKey) == 0) - { - uint index = 0; - var nameBuffer = new System.Text.StringBuilder(256); - uint nameSize = 256; - - while (RegEnumKeyEx(hKey, index, nameBuffer, ref nameSize, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero) == 0) - { - result[nameBuffer.ToString()] = ""; - nameSize = 256; - nameBuffer.Clear(); - index++; - } - } - } - finally - { - if (hKey != IntPtr.Zero) - RegCloseKey(hKey); - } - - return result; - } - - public static Dictionary GetRegistryValues(RegistryHive hive, string subKey) - { - var result = new Dictionary(); - IntPtr hKey = IntPtr.Zero; - - try - { - IntPtr hHive = GetHiveHandle(hive); - if (RegOpenKeyEx(hHive, subKey, 0, KEY_READ, out hKey) == 0) - { - uint index = 0; - var nameBuffer = new System.Text.StringBuilder(256); - uint nameSize = 256; - uint valueType; - - while (RegEnumValue(hKey, index, nameBuffer, ref nameSize, IntPtr.Zero, out valueType, IntPtr.Zero, IntPtr.Zero) == 0) - { - result[nameBuffer.ToString()] = valueType; - nameSize = 256; - nameBuffer.Clear(); - index++; - } - } - } - finally - { - if (hKey != IntPtr.Zero) - RegCloseKey(hKey); - } - - return result; - } - - public static bool DeleteRegistryKey(RegistryHive hive, string subKey) - { - IntPtr hHive = GetHiveHandle(hive); - return RegDeleteKey(hHive, subKey) == 0; - } - - public static bool DeleteRegistryValue(RegistryHive hive, string subKey, string valueName) - { - IntPtr hKey = IntPtr.Zero; - - try - { - IntPtr hHive = GetHiveHandle(hive); - if (RegOpenKeyEx(hHive, subKey, 0, KEY_WRITE, out hKey) == 0) - { - return RegDeleteValue(hKey, valueName) == 0; - } - } - finally - { - if (hKey != IntPtr.Zero) - RegCloseKey(hKey); - } - - return false; - } - - private static IntPtr GetHiveHandle(RegistryHive hive) - { - switch (hive) - { - case RegistryHive.ClassesRoot: return new IntPtr(0x80000000); - case RegistryHive.CurrentUser: return new IntPtr(0x80000001); - case RegistryHive.LocalMachine: return new IntPtr(0x80000002); - case RegistryHive.Users: return new IntPtr(0x80000003); - default: return IntPtr.Zero; - } - } -} - -public class FastFileOperations -{ - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool DeleteFile(string lpFileName); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool RemoveDirectory(string lpPathName); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, int dwFlags); - - private const int MOVEFILE_DELAY_UNTIL_REBOOT = 0x4; - - public static bool DeleteFileFast(string filePath) - { - return DeleteFile(filePath); - } - - public static bool RemoveDirectoryFast(string directoryPath) - { - return RemoveDirectory(directoryPath); - } - - public static bool ScheduleDeleteOnReboot(string filePath) - { - return MoveFileEx(filePath, null, MOVEFILE_DELAY_UNTIL_REBOOT); - } -} - -public class ProcessManager -{ - [DllImport("kernel32.dll", SetLastError = true)] - private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, int dwProcessId); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool CloseHandle(IntPtr hObject); - - private const uint PROCESS_TERMINATE = 0x0001; - private const uint PROCESS_QUERY_INFORMATION = 0x0400; - - public static bool TerminateProcessById(int processId) - { - IntPtr hProcess = OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_INFORMATION, false, processId); - if (hProcess == IntPtr.Zero) - return false; - - bool result = TerminateProcess(hProcess, 0); - CloseHandle(hProcess); - return result; - } -} - -public class GuidDecoder -{ - // Base85 decoding table (ASCII character to value mapping) - private static readonly byte[] DecodeTable = new byte[256]; - - static GuidDecoder() - { - // Initialize decode table with 0xff (invalid) for all characters - for (int i = 0; i < 256; i++) - { - DecodeTable[i] = 0xff; - } - - // Set valid Base85 character mappings - byte[] validChars = { 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 }; - byte[] values = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0xff, 0xff, 0xff, 0x16, 0xff, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0xff, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0xff, 0x53, 0x54, 0xff }; - - for (int i = 0; i < validChars.Length && i < values.Length; i++) - { - DecodeTable[validChars[i]] = values[i]; - } - } - - public static string DecodeGuid(string encodedGuid) - { - if (string.IsNullOrEmpty(encodedGuid)) - return null; - - try - { - bool failed = false; - uint total = 0; - uint pow85 = 1; - var decode = new System.Text.StringBuilder(); - - int maxLength = Math.Min(encodedGuid.Length, 20); - - for (int i = 0; i < maxLength; i++) - { - failed = true; - - if (i % 5 == 0) - { - total = 0; - pow85 = 1; - } - - int charCode = (int)encodedGuid[i]; - - if (charCode >= 128 || DecodeTable[charCode] == 0xff) - { - break; - } - - uint hexValue = DecodeTable[charCode]; - total += hexValue * pow85; - - if (i % 5 == 4) - { - decode.Append(total.ToString("X8")); - } - - pow85 *= 85; - failed = false; - } - - if (!failed && decode.Length >= 32) - { - string decodedString = decode.ToString(); - - // Reconstruct GUID in the specific order from the VBScript - return "{" + - decodedString.Substring(0, 8) + "-" + - decodedString.Substring(12, 4) + "-" + - decodedString.Substring(8, 4) + "-" + - decodedString.Substring(22, 2) + decodedString.Substring(20, 2) + "-" + - decodedString.Substring(18, 2) + decodedString.Substring(16, 2) + - decodedString.Substring(30, 2) + decodedString.Substring(28, 2) + - decodedString.Substring(26, 2) + decodedString.Substring(24, 2) + - "}"; - } - - return null; - } - catch - { - return null; - } - } -} -"@ - -#endregion - -#region Constants -$script:SCRIPT_VERSION = "2.19" -$script:SCRIPT_NAME = "OfficeScrubC2R" -$script:RET_VAL_FILE = "ScrubRetValFile.txt" -$script:OFFICE_NAME = "Office C2R / O365" - -# Registry Hive Constants -$script:HKCR = 0x80000000 -$script:HKCU = 0x80000001 -$script:HKLM = 0x80000002 -$script:HKU = 0x80000003 - -# Error Constants -$script:ERROR_SUCCESS = 0 -$script:ERROR_FAIL = 1 -$script:ERROR_REBOOT_REQUIRED = 2 -$script:ERROR_USERCANCEL = 4 -$script:ERROR_STAGE1 = 8 -$script:ERROR_STAGE2 = 16 -$script:ERROR_INCOMPLETE = 32 -$script:ERROR_DCAF_FAILURE = 64 -$script:ERROR_ELEVATION_USERDECLINED = 128 -$script:ERROR_ELEVATION = 256 -$script:ERROR_SCRIPTINIT = 512 -$script:ERROR_RELAUNCH = 1024 -$script:ERROR_UNKNOWN = 2048 -$script:ERROR_ALL = 4095 -$script:ERROR_USER_ABORT = 0xC000013A -$script:ERROR_SUCCESS_CONFIG_COMPLETE = 1728 -$script:ERROR_SUCCESS_REBOOT_REQUIRED = 3010 - -# Registry Paths -$script:REG_ARP = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\" -$script:REG_O15RPROPERTYBAG = "SOFTWARE\Microsoft\Office\15.0\ClickToRun\propertyBag\" -$script:REG_O15C2RCONFIGURATION = "SOFTWARE\Microsoft\Office\15.0\ClickToRun\Configuration\" -$script:REG_O15C2RPRODUCTIDS = "SOFTWARE\Microsoft\Office\15.0\ClickToRun\ProductReleaseIDs\Active\" -$script:REG_O16C2RCONFIGURATION = "SOFTWARE\Microsoft\Office\16.0\ClickToRun\Configuration\" -$script:REG_O16C2RPRODUCTIDS = "SOFTWARE\Microsoft\Office\16.0\ClickToRun\ProductReleaseIDs\Active\" -$script:REG_C2RCONFIGURATION = "SOFTWARE\Microsoft\Office\ClickToRun\Configuration\" -$script:REG_C2RPRODUCTIDS = "SOFTWARE\Microsoft\Office\ClickToRun\ProductReleaseIDs\" -#endregion - -#region Global Variables -$script:ErrorCode = $script:ERROR_SUCCESS -$script:LogInitialized = $false -$script:IsElevated = $false -$script:Is64Bit = $false -$script:RebootRequired = $false -$script:Quiet = $true -$script:DetectOnly = $false -$script:Force = $false -$script:RemoveAll = $false -$script:KeepLicense = $false -$script:Offline = $false -$script:ForceArpUninstall = $false -$script:ClearTaskBand = $false -$script:UnpinMode = $false -$script:SkipSD = $false -$script:NoElevate = $false -$script:UserConsent = $false -$script:ReturnErrorOrSuccess = $false -$script:TestRerun = $false -$script:SetRunOnce = $false -$script:Rerun = $false -$script:RemoveOse = $false -$script:NoCancel = $false -$script:C2R = $true - -# Path Variables -$script:AppData = $null -$script:LocalAppData = $null -$script:Temp = $null -$script:AllUsersProfile = $null -$script:ProgramFiles = $null -$script:ProgramFilesX86 = $null -$script:CommonProgramFiles = $null -$script:CommonProgramFilesX86 = $null -$script:ProgramData = $null -$script:WinDir = $null -$script:WICacheDir = $null -$script:ScrubDir = $null -$script:ScriptDir = $null -$script:LogDir = $null -$script:ProfilesDirectory = $null -$script:PackageFolder = $null -$script:PackageGuid = $null -$script:OSVersion = $null -$script:OSInfo = $null -$script:VersionNT = 0 - -# Collections -$script:InstalledSku = @{} -$script:RemoveSku = @{} -$script:KeepSku = @{} -$script:KeepLis = @{} -$script:KeepFolder = @{} -$script:Apps = @{} -$script:DelRegKey = @{} -$script:KeepReg = @{} -$script:C2RSuite = @{} -$script:DelInUse = @{} -$script:DelFolder = @{} -$script:SC = @{} - -# Office Process Names -$script:OfficeProcesses = @( - "appvshnotify.exe", - "integratedoffice.exe", - "integrator.exe", - "firstrun.exe", - "communicator.exe", - "msosync.exe", - "OneNoteM.exe", - "iexplore.exe", - "mavinject32.exe", - "werfault.exe", - "perfboost.exe", - "roamingoffice.exe", - "officeclicktorun.exe", - "officeondemand.exe", - "OfficeC2RClient.exe" -) -#endregion - -#region Logging Functions -function Write-LogHeader { - param([string]$Message) - if ($script:LogInitialized) { - $timestamp = Get-Date -Format "HH:mm:ss" - Write-Output "================================================" - Write-Output "$timestamp - $Message" - Write-Output "================================================" - } -} - -function Write-LogSubHeader { - param([string]$Message) - if ($script:LogInitialized) { - $timestamp = Get-Date -Format "HH:mm:ss" - Write-Output "----------------------------------------" - Write-Output "$timestamp - $Message" - Write-Output "----------------------------------------" - } -} - -function Write-Log { - param([string]$Message) - if ($script:LogInitialized) { - $timestamp = Get-Date -Format "HH:mm:ss" - Write-Output "$timestamp - $Message" - } -} - -function Write-LogOnly { - param([string]$Message) - if ($script:LogInitialized -and -not $script:Quiet) { - $timestamp = Get-Date -Format "HH:mm:ss" - Write-Output "$timestamp - $Message" - } -} - -function Initialize-Log { - param([string]$LogPath) - - if (-not $script:LogInitialized) { - $script:LogDir = $LogPath - if (-not (Test-Path $script:LogDir)) { - [void](New-Item -ItemType Directory -Path $script:LogDir -Force) - } - $script:LogInitialized = $true - Write-LogHeader "Office C2R Scrubber v$script:SCRIPT_VERSION - Log Initialized" - } -} -#endregion - -#region Registry Functions -function Test-RegistryKeyExists { - param( - [Microsoft.Win32.RegistryHive]$Hive, - [string]$SubKey - ) - - try { - $regKey = [Microsoft.Win32.Registry]::$($Hive.ToString()).OpenSubKey($SubKey, $false) - if ($regKey) { - $regKey.Close() - return $true - } - return $false - } - catch { - return $false - } -} - -function Get-RegistryValue { - param( - [Microsoft.Win32.RegistryHive]$Hive, - [string]$SubKey, - [string]$ValueName, - [string]$ValueType = "REG_SZ" - ) - - try { - $regKey = [Microsoft.Win32.Registry]::$($Hive.ToString()).OpenSubKey($SubKey, $false) - if ($regKey) { - $value = $regKey.GetValue($ValueName) - $regKey.Close() - return $value - } - return $null - } - catch { - return $null - } -} - -function Set-RegistryValue { - param( - [Microsoft.Win32.RegistryHive]$Hive, - [string]$SubKey, - [string]$ValueName, - [object]$Value, - [Microsoft.Win32.RegistryValueKind]$ValueType = [Microsoft.Win32.RegistryValueKind]::String - ) - - try { - $regKey = [Microsoft.Win32.Registry]::$($Hive.ToString()).OpenSubKey($SubKey, $true) - if ($regKey) { - $regKey.SetValue($ValueName, $Value, $ValueType) - $regKey.Close() - return $true - } - return $false - } - catch { - return $false - } -} - -function Remove-RegistryKey { - param( - [Microsoft.Win32.RegistryHive]$Hive, - [string]$SubKey - ) - - try { - $parentKey = Split-Path $SubKey -Parent - $keyName = Split-Path $SubKey -Leaf - - if ($parentKey -eq "") { - $parentKey = $SubKey - $keyName = "" - } - - $regKey = [Microsoft.Win32.Registry]::$($Hive.ToString()).OpenSubKey($parentKey, $true) - if ($regKey) { - if ($keyName -eq "") { - $regKey.Close() - [Microsoft.Win32.Registry]::$($Hive.ToString()).DeleteSubKey($parentKey, $true) - } - else { - $regKey.DeleteSubKey($keyName, $true) - $regKey.Close() - } - return $true - } - return $false - } - catch { - return $false - } -} - -function Remove-RegistryValue { - param( - [Microsoft.Win32.RegistryHive]$Hive, - [string]$SubKey, - [string]$ValueName - ) - - try { - $regKey = [Microsoft.Win32.Registry]::$($Hive.ToString()).OpenSubKey($SubKey, $true) - if ($regKey) { - $regKey.DeleteValue($ValueName, $true) - $regKey.Close() - return $true - } - return $false - } - catch { - return $false - } -} - -function Get-RegistrySubKeys { - param( - [Microsoft.Win32.RegistryHive]$Hive, - [string]$SubKey - ) - - try { - $regKey = [Microsoft.Win32.Registry]::$($Hive.ToString()).OpenSubKey($SubKey, $false) - if ($regKey) { - $subKeys = $regKey.GetSubKeyNames() - $regKey.Close() - return $subKeys - } - return @() - } - catch { - return @() - } -} - -function Get-RegistryValues { - param( - [Microsoft.Win32.RegistryHive]$Hive, - [string]$SubKey - ) - - try { - $regKey = [Microsoft.Win32.Registry]::$($Hive.ToString()).OpenSubKey($SubKey, $false) - if ($regKey) { - $valueNames = $regKey.GetValueNames() - $regKey.Close() - return $valueNames - } - return @() - } - catch { - return @() - } -} -#endregion - -#region File Operations -function Remove-FileFast { - param([string]$FilePath) - - try { - if (Test-Path $FilePath) { - return [FastFileOperations]::DeleteFileFast($FilePath) - } - return $true - } - catch { - return $false - } -} - -function Remove-FolderFast { - param([string]$FolderPath) - - try { - if (Test-Path $FolderPath) { - return [FastFileOperations]::RemoveDirectoryFast($FolderPath) - } - return $true - } - catch { - return $false - } -} - -function Schedule-DeleteOnReboot { - param([string]$Path) - - try { - return [FastFileOperations]::ScheduleDeleteOnReboot($Path) - } - catch { - return $false - } -} - -function Remove-FolderRecursive { - param( - [string]$Path, - [switch]$Force - ) - - try { - if (Test-Path $Path) { - if ($Force) { - Get-ChildItem -Path $Path -Recurse -Force | Remove-Item -Force -Recurse - } - Remove-Item -Path $Path -Force -Recurse - return $true - } - return $true - } - catch { - return $false - } -} -#endregion - -#region Process Management -function Stop-OfficeProcesses { - param([switch]$Force) - - Write-LogSubHeader "Stopping Office processes" - - foreach ($processName in $script:OfficeProcesses) { - $processes = Get-Process -Name $processName -ErrorAction SilentlyContinue - foreach ($process in $processes) { - try { - Write-Log "Stopping process: $($process.ProcessName) (PID: $($process.Id))" - if ($Force) { - $process.Kill() - } - else { - $process.CloseMainWindow() - if (-not $process.WaitForExit(5000)) { - $process.Kill() - } - } - } - catch { - Write-Log "Failed to stop process: $($process.ProcessName) - $($_.Exception.Message)" - } - } - } -} - -function Test-ProcessRunning { - param([string]$ProcessName) - - $processes = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue - return $processes.Count -gt 0 -} -#endregion - -#region Office Detection -function Test-IsC2R { - param([string]$Value) - - $c2rPatterns = @( - "officeclicktorun", - "o365proplus", - "o365business", - "o365homepremium", - "o365home", - "o365smallbusiness", - "o365midsizebusiness", - "o365enterprise" - ) - - $valueLower = $Value.ToLower() - foreach ($pattern in $c2rPatterns) { - if ($valueLower -like "*$pattern*") { - return $true - } - } - return $false -} - -function Get-InstalledOfficeProducts { - Write-LogSubHeader "Detecting installed Office products" - - $products = @{} - - # Check O15 C2R Configuration - Write-LogOnly "Check for O15 C2R products" - $o15ProductReleaseIds = Get-RegistryValue -Hive LocalMachine -SubKey $script:REG_O15C2RCONFIGURATION -ValueName "ProductReleaseIds" - if ($o15ProductReleaseIds) { - $o15Products = $o15ProductReleaseIds -split "," - $o15Version = Get-RegistryValue -Hive LocalMachine -SubKey "$script:REG_O15C2RPRODUCTIDS\culture" -ValueName "x-none" - foreach ($prod in $o15Products) { - Write-LogOnly "Found O15 C2R product in Configuration: $prod" - if (-not $products.ContainsKey($prod.ToLower())) { - Write-LogOnly "Add new product to dictionary: $($prod.ToLower())" - $products[$prod.ToLower()] = $o15Version - } - } - } - - # Check O15 PropertyBag - $o15PropertyBag = Get-RegistryValue -Hive LocalMachine -SubKey $script:REG_O15RPROPERTYBAG -ValueName "productreleaseid" - if ($o15PropertyBag) { - $o15Products = $o15PropertyBag -split "," - $o15Version = Get-RegistryValue -Hive LocalMachine -SubKey "$script:REG_O15C2RPRODUCTIDS\culture" -ValueName "x-none" - foreach ($prod in $o15Products) { - Write-LogOnly "Found O15 C2R product in PropertyBag: $prod" - if (-not $products.ContainsKey($prod.ToLower())) { - Write-LogOnly "Add new product to dictionary: $($prod.ToLower())" - $products[$prod.ToLower()] = $o15Version - } - } - } - - # Check Office C2R products (>=QR8) - Write-LogOnly "Check for Office C2R products (>=QR8)" - $activeConfiguration = Get-RegistryValue -Hive LocalMachine -SubKey $script:REG_C2RPRODUCTIDS -ValueName "ActiveConfiguration" - if ($activeConfiguration) { - $versionFallback = Get-RegistryValue -Hive LocalMachine -SubKey "$script:REG_C2RPRODUCTIDS\$activeConfiguration\culture" -ValueName "x-none" - - $cultures = Get-RegistrySubKeys -Hive LocalMachine -SubKey "$script:REG_C2RPRODUCTIDS\$activeConfiguration\culture" - if ($cultures) { - foreach ($cult in $cultures) { - if ($cult.ToLower() -like "*x-none*") { - $versionFallback = Get-RegistryValue -Hive LocalMachine -SubKey "$script:REG_C2RPRODUCTIDS\$activeConfiguration\culture\$cult" -ValueName "Version" - } - } - } - - $officeProducts = Get-RegistrySubKeys -Hive LocalMachine -SubKey "$script:REG_C2RPRODUCTIDS\$activeConfiguration" - if ($officeProducts) { - foreach ($prod in $officeProducts) { - $sProd = $prod.ToLower() - if ($sProd -like "*.*") { - $sProd = $sProd.Substring(0, $sProd.IndexOf(".")) - } - - if ($sProd -notin @("culture", "stream")) { - Write-LogOnly "Found Office C2R product in Configuration: $prod" - if (-not $products.ContainsKey($sProd)) { - Write-LogOnly "Add new product to dictionary: $sProd" - $displayVersion = Get-RegistryValue -Hive LocalMachine -SubKey "$script:REG_C2RPRODUCTIDS\$activeConfiguration\$prod\x-none" -ValueName "Version" - if ($displayVersion) { - $products[$sProd] = $displayVersion - } - else { - $products[$sProd] = $versionFallback - } - } - } - } - } - } - - # Check ARP entries - Write-LogOnly "Check ARP entries for C2R products" - $arpKeys = Get-RegistrySubKeys -Hive LocalMachine -SubKey $script:REG_ARP - foreach ($key in $arpKeys) { - $displayName = Get-RegistryValue -Hive LocalMachine -SubKey "$script:REG_ARP$key" -ValueName "DisplayName" - if ($displayName -and (Test-IsC2R $displayName)) { - $uninstallString = Get-RegistryValue -Hive LocalMachine -SubKey "$script:REG_ARP$key" -ValueName "UninstallString" - if ($uninstallString) { - Write-LogOnly "Found C2R product in ARP: $displayName" - $script:C2RSuite[$key] = $displayName - } - } - } - - return $products -} -#endregion - -#region System Information -function Test-IsElevated { - $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) - return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -} - -function Get-SystemInfo { - try { - $computerSystem = Get-WmiObject -Class Win32_ComputerSystem -ErrorAction Stop - $script:Is64Bit = $computerSystem.SystemType -like "*64*" - - $os = Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop - $script:OSVersion = $os.Version - $script:OSInfo = "$($os.Caption) $($os.OtherTypeDescription), SP $($os.ServicePackMajorVersion), Version: $($os.Version), Codepage: $($os.CodeSet), Country Code: $($os.CountryCode), Language: $($os.OSLanguage)" - - if ($script:OSVersion) { - $versionParts = $script:OSVersion -split "\." - if ($versionParts.Count -ge 2) { - $script:VersionNT = [int]$versionParts[0] * 100 + [int]$versionParts[1] - } - } - } - catch { - Write-Warning "Failed to get system information: $($_.Exception.Message)" - $script:Is64Bit = [Environment]::Is64BitOperatingSystem - $script:OSVersion = [Environment]::OSVersion.Version.ToString() - $script:OSInfo = "Unknown OS" - $script:VersionNT = 0 - } -} - -function Initialize-Environment { - # Set environment variables - $script:AppData = $env:APPDATA - $script:LocalAppData = $env:LOCALAPPDATA - $script:Temp = $env:TEMP - $script:AllUsersProfile = $env:ALLUSERSPROFILE - $script:ProgramFiles = $env:ProgramFiles - $script:CommonProgramFiles = $env:CommonProgramFiles - $script:ProgramData = $env:ProgramData - $script:WinDir = $env:WINDIR - $script:WICacheDir = "$script:WinDir\Installer" - $script:ScrubDir = "$script:Temp\$script:SCRIPT_NAME" - $script:ScriptDir = Split-Path -Parent $MyInvocation.PSCommandPath - $script:Notepad = "$script:WinDir\notepad.exe" - - # Set 64-bit specific paths - if ($script:Is64Bit) { - $script:ProgramFilesX86 = ${env:ProgramFiles(x86)} - $script:CommonProgramFilesX86 = ${env:CommonProgramFiles(x86)} - } - - # Get profiles directory - $script:ProfilesDirectory = Get-RegistryValue -Hive LocalMachine -SubKey "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" -ValueName "ProfilesDirectory" - if (-not $script:ProfilesDirectory -or -not (Test-Path $script:ProfilesDirectory)) { - $script:ProfilesDirectory = Split-Path -Parent $env:USERPROFILE - } - - # Create temp directory - if (-not (Test-Path $script:ScrubDir)) { - [void](New-Item -ItemType Directory -Path $script:ScrubDir -Force) - } - - # Set default log directory - $script:LogDir = $script:ScrubDir -} -#endregion - -#region Error Handling -function Set-ErrorCode { - param([int]$ErrorBit) - $script:ErrorCode = $script:ErrorCode -bor $ErrorBit -} - -function Clear-ErrorCode { - param([int]$ErrorBit) - $script:ErrorCode = $script:ErrorCode -band (-bnot $ErrorBit) -} - -function Set-ReturnValue { - param([int]$Err) - $script:ErrorCode = $Err - if ($script:ScrubDir) { - $retValPath = Join-Path $script:ScrubDir $script:RET_VAL_FILE - Set-Content -Path $retValPath -Value $Err -Force - } -} - -function Get-ReturnValueFromFile { - if ($script:ScrubDir) { - $retValPath = Join-Path $script:ScrubDir $script:RET_VAL_FILE - if (Test-Path $retValPath) { - $reader = [System.IO.StreamReader]::new($retValPath) - try { - $content = $reader.ReadToEnd() - } - finally { - $reader.Close() - } - return [int]$content - } - } - return $script:ERROR_UNKNOWN -} -#endregion - -#region GUID Operations -function Get-ExpandedGuid { - param([string]$Guid) - - if ($Guid.Length -eq 32) { - # Convert compressed GUID to expanded format - $expanded = $Guid.Substring(0, 8) + "-" + $Guid.Substring(8, 4) + "-" + $Guid.Substring(12, 4) + "-" + $Guid.Substring(16, 4) + "-" + $Guid.Substring(20, 12) - return $expanded.ToUpper() - } - return $Guid.ToUpper() -} - -function Get-CompressedGuid { - param([string]$Guid) - - if ($Guid.Length -eq 36 -and $Guid.Contains("-")) { - # Convert expanded GUID to compressed format - return $Guid.Replace("-", "").ToUpper() - } - return $Guid.ToUpper() -} - -function Get-DecodedGuid { - param( - [string]$EncGuid, - [string]$Guid - ) - - try { - # Use C# for GUID decoding - $decodedGuid = [GuidDecoder]::DecodeGuid($EncGuid) - - if ($decodedGuid) { - return $decodedGuid - } - - # Fallback to expanded GUID format if decoding fails - return Get-ExpandedGuid $Guid - } - catch { - Write-Log "Failed to decode GUID: $($_.Exception.Message)" - return Get-ExpandedGuid $Guid - } -} -#endregion - -#region Service Management -function Remove-Service { - param([string]$ServiceName) - - try { - $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue - if ($service) { - Write-Log "Removing service: $ServiceName" - if ($service.Status -eq "Running") { - Stop-Service -Name $ServiceName -Force - } - [void](sc.exe delete $ServiceName) - return $true - } - return $true - } - catch { - Write-Log "Failed to remove service: $ServiceName - $($_.Exception.Message)" - return $false - } -} -#endregion - -Export-ModuleMember -Function * diff --git a/OfficeScrubC2R-Utilities.psm1 b/OfficeScrubC2R-Utilities.psm1 deleted file mode 100644 index c33193e..0000000 --- a/OfficeScrubC2R-Utilities.psm1 +++ /dev/null @@ -1,1157 +0,0 @@ -# OfficeScrubC2R-Utilities.psm1 -# PowerShell utilities with C# inline code for performance - -#region Script Variables - -$script:SCRIPT_VERSION = "2.19" -$script:SCRIPT_NAME = "OfficeScrubC2R" - -# Error codes (matching VBS) -$script:ERROR_SUCCESS = 0 -$script:ERROR_FAIL = 1 -$script:ERROR_REBOOT_REQUIRED = 2 -$script:ERROR_USERCANCEL = 4 -$script:ERROR_STAGE1 = 8 -$script:ERROR_STAGE2 = 16 -$script:ERROR_INCOMPLETE = 32 -$script:ERROR_DCAF_FAILURE = 64 -$script:ERROR_ELEVATION_USERDECLINED = 128 -$script:ERROR_ELEVATION = 256 -$script:ERROR_SCRIPTINIT = 512 -$script:ERROR_RELAUNCH = 1024 -$script:ERROR_UNKNOWN = 2048 - -# Registry constants -$script:REG_ARP = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\" - -# Global state -$script:ErrorCode = 0 -$script:RebootRequired = $false -$script:IsElevated = $false -$script:Is64Bit = $false -$script:OSInfo = "" -$script:LogStream = $null -$script:LogDir = "" -$script:ScrubDir = "" -$script:Quiet = $false -$script:DetectOnly = $false -$script:Force = $false -$script:RemoveAll = $false -$script:KeepLicense = $false -$script:Offline = $false -$script:ForceArpUninstall = $false -$script:ClearTaskBand = $false -$script:UnpinMode = $false -$script:SkipSD = $false -$script:NoElevate = $false -$script:LogPath = "" - -# Dictionaries -$script:InstalledSku = @{} -$script:C2RSuite = @{} -$script:KeepSku = @{} -$script:KeepFolder = @{} -$script:DelInUse = @{} - -# Environment paths -$script:ProgramFiles = "" -$script:ProgramFilesX86 = "" -$script:CommonProgramFiles = "" -$script:CommonProgramFilesX86 = "" -$script:ProgramData = "" -$script:AppData = "" -$script:LocalAppData = "" -$script:AllUsersProfile = "" -$script:WinDir = "" -$script:Temp = "" -$script:WICacheDir = "" - -# Native orchestrator -$script:Orchestrator = $null - -#endregion - -#region C# Type Loading - -function Initialize-NativeTypes { - # Check if types are already loaded - try { - $null = [OfficeScrubNative.OfficeScrubOrchestrator] - Write-Verbose "Native C# types already loaded" - return - } - catch { - # Types not loaded yet, proceed with loading - } - - # Get the module's base path (works for both standalone and nested modules) - $moduleBasePath = $PSScriptRoot - if ([string]::IsNullOrEmpty($moduleBasePath)) { - # Try to get path from the module info - $moduleInfo = Get-Module -Name OfficeScrubC2R-Utilities -ErrorAction SilentlyContinue - if ($moduleInfo) { - $moduleBasePath = Split-Path -Parent $moduleInfo.Path - } - # If still empty, try parent module - if ([string]::IsNullOrEmpty($moduleBasePath)) { - $parentModule = Get-Module -Name OfficeScrubC2R -ErrorAction SilentlyContinue - if ($parentModule) { - $moduleBasePath = Split-Path -Parent $parentModule.Path - } - } - # Last resort: use current location - if ([string]::IsNullOrEmpty($moduleBasePath)) { - $moduleBasePath = (Get-Location).Path - } - } - - # Try to load the pre-compiled DLL first - $dllPath = Join-Path $moduleBasePath "OfficeScrubNative.dll" - - if (Test-Path $dllPath) { - try { - Add-Type -Path $dllPath -ErrorAction Stop - Write-Verbose "Native C# types loaded from DLL successfully" - return - } - catch { - # Check for various "already loaded" error messages - $errorMessage = $_.Exception.Message - if ($errorMessage -like "*already exists*" -or - $errorMessage -like "*already loaded*" -or - $errorMessage -like "*Assembly with same name*") { - Write-Verbose "Native C# types already loaded (detected via error message)" - return - } - else { - Write-Warning "Failed to load DLL, falling back to source compilation: $_" - } - } - } - - # Fallback: compile from source - $csharpPath = Join-Path $moduleBasePath "OfficeScrubC2R-Native.cs" - if (-not (Test-Path $csharpPath)) { - throw "Neither DLL nor C# source file found. Expected: $dllPath or $csharpPath" - } - - Write-Verbose "Compiling C# source (DLL not found)" - $csharpCode = Get-Content $csharpPath -Raw - - try { - # For Windows PowerShell 5.1 (.NET Framework), use simpler assembly references - # For PowerShell 7+ (.NET Core), we would need more specific assemblies - if ($PSVersionTable.PSVersion.Major -ge 6) { - # PowerShell 7+ (.NET Core) - $assemblies = @( - "System", - "System.Core", - "System.Collections", - "System.Linq", - "System.Management", - "System.Threading.Thread", - "System.ComponentModel.Primitives", - "System.Diagnostics.Process", - "System.IO.FileSystem", - "System.Runtime", - "Microsoft.CSharp", - "Microsoft.Win32.Registry", - "mscorlib", - "netstandard" - ) - } - else { - # Windows PowerShell 5.1 (.NET Framework) - much simpler! - $assemblies = @( - "System", - "System.Core", - "System.Management", - "Microsoft.CSharp" - ) - } - - Add-Type -TypeDefinition $csharpCode -Language CSharp ` - -ReferencedAssemblies $assemblies -ErrorAction Stop - - Write-Verbose "Native C# types loaded from source successfully" - } - catch { - # Check for various "already loaded" error messages - $errorMessage = $_.Exception.Message - if ($errorMessage -like "*already exists*" -or - $errorMessage -like "*already loaded*" -or - $errorMessage -like "*Assembly with same name*") { - Write-Verbose "Native C# types already loaded (detected via error message)" - } - else { - throw "Failed to load C# types: $_" - } - } -} - -#endregion - -#region Environment Initialization - -function Initialize-Environment { - [CmdletBinding()] - param() - - # Skip if already initialized - if ($null -ne $script:Orchestrator -and $script:ScrubDir) { - Write-Verbose "Environment already initialized, skipping..." - return - } - - # Load C# types - Initialize-NativeTypes - - # Initialize orchestrator - $script:Is64Bit = [Environment]::Is64BitOperatingSystem - $script:Orchestrator = New-Object OfficeScrubNative.OfficeScrubOrchestrator($script:Is64Bit) - - # Set environment paths - $script:ProgramFiles = [Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFiles) - $script:CommonProgramFiles = [Environment]::GetFolderPath([Environment+SpecialFolder]::CommonProgramFiles) - $script:ProgramData = [Environment]::GetFolderPath([Environment+SpecialFolder]::CommonApplicationData) - $script:AppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::ApplicationData) - $script:LocalAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) - $script:AllUsersProfile = [Environment]::GetFolderPath([Environment+SpecialFolder]::CommonDesktopDirectory) | Split-Path -Parent - $script:WinDir = $env:windir - $script:Temp = [System.IO.Path]::GetTempPath() - - if ($script:Is64Bit) { - $script:ProgramFilesX86 = ${env:ProgramFiles(x86)} - $script:CommonProgramFilesX86 = ${env:CommonProgramFiles(x86)} - } - else { - $script:ProgramFilesX86 = $script:ProgramFiles - $script:CommonProgramFilesX86 = $script:CommonProgramFiles - } - - $script:WICacheDir = Join-Path $script:WinDir "Installer" - $script:ScrubDir = Join-Path $script:Temp $script:SCRIPT_NAME - - if (-not (Test-Path $script:ScrubDir)) { - New-Item -Path $script:ScrubDir -ItemType Directory -Force | Out-Null - } - - $script:LogDir = $script:ScrubDir -} - -function Get-SystemInfo { - [CmdletBinding()] - param() - - $os = Get-CimInstance -ClassName Win32_OperatingSystem - $cs = Get-CimInstance -ClassName Win32_ComputerSystem - - $script:OSInfo = "{0}, Version: {1}, Architecture: {2}" -f ` - $os.Caption, $os.Version, $cs.SystemType - - $script:Is64Bit = $cs.SystemType -like "*64*" -} - -function Test-IsElevated { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = New-Object Security.Principal.WindowsPrincipal($identity) - return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -} - -#endregion - -#region Logging Functions - -function Initialize-Log { - [CmdletBinding()] - param( - [string]$LogPath - ) - - # Validate log path - if ([string]::IsNullOrWhiteSpace($LogPath)) { - if ([string]::IsNullOrWhiteSpace($script:LogDir)) { - Write-Warning "LogPath is empty. Using temp directory." - $LogPath = [System.IO.Path]::GetTempPath() - } - else { - $LogPath = $script:LogDir - } - } - - $computerName = $env:COMPUTERNAME - $timestamp = Get-Date -Format "yyyyMMddHHmmss" - $logFile = Join-Path $LogPath ("{0}_{1}_ScrubLog.txt" -f $computerName, $timestamp) - - try { - $script:LogStream = [System.IO.StreamWriter]::new($logFile, $false, [System.Text.Encoding]::UTF8) - $script:LogStream.AutoFlush = $true - - Write-LogHeader "Microsoft Customer Support Services - Office C2R Removal Utility" - Write-Log ("Version: {0}" -f $script:SCRIPT_VERSION) - Write-Log ("64-bit OS: {0}" -f $script:Is64Bit) - Write-Log ("Removal start: {0}" -f (Get-Date)) - Write-Log ("OS Details: {0}" -f $script:OSInfo) - Write-Log "" - } - catch { - Write-Warning "Failed to initialize log: $_" - } -} - -function Write-LogHeader { - [CmdletBinding()] - param([string]$Message) - - $separator = "=" * $Message.Length - $output = "`r`n{0}`r`n{1}" -f $Message, $separator - - if (-not $script:Quiet) { - Write-Host $output -ForegroundColor Cyan - } - - if ($script:LogStream) { - $script:LogStream.WriteLine("") - $script:LogStream.WriteLine($output) - } -} - -function Write-LogSubHeader { - [CmdletBinding()] - param([string]$Message) - - $separator = "-" * $Message.Length - $output = "`r`n{0}`r`n{1}" -f $Message, $separator - - if (-not $script:Quiet) { - Write-Host $output -ForegroundColor Yellow - } - - if ($script:LogStream) { - $script:LogStream.WriteLine("") - $script:LogStream.WriteLine($output) - } -} - -function Write-Log { - [CmdletBinding()] - param([string]$Message) - - $timestamp = Get-Date -Format "HH:mm:ss" - $output = " {0}: {1}" -f $timestamp, $Message - - if (-not $script:Quiet) { - Write-Host $output - } - - if ($script:LogStream) { - $script:LogStream.WriteLine($output) - } -} - -function Write-LogOnly { - [CmdletBinding()] - param([string]$Message) - - if ($script:LogStream) { - $timestamp = Get-Date -Format "HH:mm:ss" - # Use direct string concatenation instead of format to avoid brace issues - $script:LogStream.WriteLine(" $timestamp : $Message") - } -} - -function Close-Log { - if ($script:LogStream) { - $script:LogStream.Flush() - $script:LogStream.Close() - $script:LogStream.Dispose() - $script:LogStream = $null - } -} - -#endregion - -#region Error Handling - -function Set-ErrorCode { - [CmdletBinding()] - param([int]$ErrorBit) - - $script:ErrorCode = $script:ErrorCode -bor $ErrorBit - - # Cascade critical errors to FAIL bit - $criticalErrors = $script:ERROR_DCAF_FAILURE -bor $script:ERROR_STAGE2 -bor - $script:ERROR_ELEVATION_USERDECLINED -bor $script:ERROR_ELEVATION -bor - $script:ERROR_SCRIPTINIT - - if ($script:ErrorCode -band $criticalErrors) { - $script:ErrorCode = $script:ErrorCode -bor $script:ERROR_FAIL - } -} - -function Clear-ErrorCode { - [CmdletBinding()] - param([int]$ErrorBit) - - $script:ErrorCode = $script:ErrorCode -band (-bnot $ErrorBit) - - # Clear FAIL bit if clearing critical errors - $clearableErrors = $script:ERROR_ELEVATION_USERDECLINED -bor $script:ERROR_ELEVATION -bor - $script:ERROR_SCRIPTINIT - - if ($ErrorBit -band $clearableErrors) { - $script:ErrorCode = $script:ErrorCode -band (-bnot $script:ERROR_FAIL) - } -} - -function Set-ReturnValue { - [CmdletBinding()] - param([int]$Value) - - $retValFile = Join-Path $script:ScrubDir "ScrubRetValFile.txt" - - try { - [System.IO.File]::WriteAllText($retValFile, $Value.ToString()) - } - catch { - Write-LogOnly "Failed to write return value file: $_" - } -} - -#endregion - -#region Registry Operations (using C# helpers) - -function Get-RegistryValue { - [CmdletBinding()] - param( - [OfficeScrubNative.RegistryHiveType]$Hive, - [string]$SubKey, - [string]$ValueName, - [object]$DefaultValue = $null - ) - - return $script:Orchestrator.Registry.GetValue($Hive, $SubKey, $ValueName, $DefaultValue) -} - -function Set-RegistryValue { - [CmdletBinding()] - param( - [OfficeScrubNative.RegistryHiveType]$Hive, - [string]$SubKey, - [string]$ValueName, - [object]$Value, - [Microsoft.Win32.RegistryValueKind]$Kind = [Microsoft.Win32.RegistryValueKind]::String - ) - - return $script:Orchestrator.Registry.SetValue($Hive, $SubKey, $ValueName, $Value, $Kind) -} - -function Remove-RegistryKey { - [CmdletBinding()] - param( - [OfficeScrubNative.RegistryHiveType]$Hive, - [string]$SubKey, - [switch]$Recursive = $true - ) - - if (-not $script:DetectOnly) { - Write-LogOnly "Delete registry key: $($Hive)\$SubKey" - return $script:Orchestrator.Registry.DeleteKey($Hive, $SubKey, $Recursive) - } - else { - Write-LogOnly "Preview mode. Would delete registry key: $($Hive)\$SubKey" - return $false - } -} - -function Remove-RegistryValue { - [CmdletBinding()] - param( - [OfficeScrubNative.RegistryHiveType]$Hive, - [string]$SubKey, - [string]$ValueName - ) - - if (-not $script:DetectOnly) { - Write-LogOnly "Delete registry value: $($Hive)\$SubKey -> $ValueName" - return $script:Orchestrator.Registry.DeleteValue($Hive, $SubKey, $ValueName) - } - else { - Write-LogOnly "Preview mode. Would delete registry value: $($Hive)\$SubKey -> $ValueName" - return $false - } -} - -function Get-RegistryKeys { - [CmdletBinding()] - param( - [OfficeScrubNative.RegistryHiveType]$Hive, - [string]$SubKey - ) - - return $script:Orchestrator.Registry.EnumerateKeys($Hive, $SubKey) -} - -function Get-RegistryValues { - [CmdletBinding()] - param( - [OfficeScrubNative.RegistryHiveType]$Hive, - [string]$SubKey - ) - - return $script:Orchestrator.Registry.EnumerateValues($Hive, $SubKey) -} - -function Test-RegistryKeyExists { - [CmdletBinding()] - param( - [OfficeScrubNative.RegistryHiveType]$Hive, - [string]$SubKey - ) - - return $script:Orchestrator.Registry.KeyExists($Hive, $SubKey) -} - -#endregion - -#region File Operations (using C# helpers) - -function Remove-FolderRecursive { - [CmdletBinding()] - param( - [string]$Path, - [switch]$Force - ) - - # Validate Path parameter - if ([string]::IsNullOrWhiteSpace($Path)) { - Write-Warning "Remove-FolderRecursive: Path parameter is empty or null" - return $false - } - - if (-not (Test-Path $Path)) { - return $true - } - - if (-not $script:DetectOnly) { - Write-LogOnly "Delete folder: $Path" - $result = $script:Orchestrator.Files.DeleteDirectory($Path, $true, $true) - - if (-not $result) { - Write-Log "Failed to delete folder, scheduled for reboot: $Path" - $script:RebootRequired = $true - Set-ErrorCode $script:ERROR_REBOOT_REQUIRED - } - - return $result - } - else { - Write-LogOnly "Preview mode. Would delete folder: $Path" - return $false - } -} - -function Remove-FileForced { - [CmdletBinding()] - param( - [string]$Path, - [switch]$ScheduleOnFail - ) - - # Validate Path parameter - if ([string]::IsNullOrWhiteSpace($Path)) { - Write-Warning "Remove-FileForced: Path parameter is empty or null" - return $false - } - - if (-not (Test-Path $Path)) { - return $true - } - - if (-not $script:DetectOnly) { - Write-LogOnly "Delete file: $Path" - $result = $script:Orchestrator.Files.DeleteFile($Path, $ScheduleOnFail) - - if (-not $result -and $ScheduleOnFail) { - Write-Log "Failed to delete file, scheduled for reboot: $Path" - $script:RebootRequired = $true - Set-ErrorCode $script:ERROR_REBOOT_REQUIRED - } - - return $result - } - else { - Write-LogOnly "Preview mode. Would delete file: $Path" - return $false - } -} - -function Add-PendingFileDelete { - [CmdletBinding()] - param([string]$Path) - - $script:Orchestrator.Registry.AddPendingFileRenameOperation($Path) - $script:DelInUse[$Path] = $Path - $script:RebootRequired = $true - Set-ErrorCode $script:ERROR_REBOOT_REQUIRED -} - -#endregion - -#region Process Operations - -function Stop-OfficeProcesses { - [CmdletBinding()] - param([switch]$Force) - - Write-LogSubHeader "Stopping Office processes" - - $processes = [OfficeScrubNative.OfficeConstants]::OFFICE_PROCESSES - $terminated = $script:Orchestrator.Processes.TerminateProcesses($processes, 10000) - - if ($terminated.Count -gt 0) { - Write-Log ("Terminated {0} Office process(es)" -f $terminated.Count) - Start-Sleep -Seconds 2 - } -} - -function Test-ProcessRunning { - [CmdletBinding()] - param([string]$ProcessName) - - return $script:Orchestrator.Processes.IsProcessRunning($ProcessName) -} - -#endregion - -#region Product Detection - -function Get-InstalledOfficeProducts { - [CmdletBinding()] - param() - - Write-LogSubHeader "Detect installed products" - - $products = @{} - $script:C2RSuite = @{} - - # O15 Configuration - Write-LogOnly "Check for O15 C2R products" - $o15Products = Get-RegistryValue -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Office\15.0\ClickToRun\Configuration" ` - -ValueName "ProductReleaseIds" - - if ($o15Products) { - foreach ($prod in ($o15Products -split ',')) { - Write-LogOnly "Found O15 C2R product in Configuration: $prod" - $version = Get-RegistryValue -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Office\15.0\ClickToRun\ProductReleaseIDs\Active\culture" ` - -ValueName "x-none" - - $products[$prod.ToLower()] = $version - $script:C2RSuite[$prod] = "$prod - $version" - } - } - - # O15 PropertyBag - $o15PropBag = Get-RegistryValue -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Office\15.0\ClickToRun\propertyBag" ` - -ValueName "productreleaseid" - - if ($o15PropBag) { - foreach ($prod in ($o15PropBag -split ',')) { - Write-LogOnly "Found O15 C2R product in PropertyBag: $prod" - if (-not $products.ContainsKey($prod.ToLower())) { - $products[$prod.ToLower()] = "15.0" - $script:C2RSuite[$prod] = "$prod - 15.0" - } - } - } - - # Office C2R (QR8+) - Write-LogOnly "Check for Office C2R products (>=QR8)" - $activeConfig = Get-RegistryValue -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Office\ClickToRun\ProductReleaseIDs" ` - -ValueName "ActiveConfiguration" - - if ($activeConfig) { - $configKeys = Get-RegistryKeys -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Office\ClickToRun\ProductReleaseIDs\$activeConfig" - - foreach ($key in $configKeys) { - if ($key -notin @("culture", "stream")) { - $prod = $key - if ($prod -like "*.*") { - $prod = $prod.Substring(0, $prod.IndexOf('.')) - } - - Write-LogOnly "Found Office C2R product in Configuration: $prod" - $version = Get-RegistryValue -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Office\ClickToRun\ProductReleaseIDs\$activeConfig\$key\x-none" ` - -ValueName "Version" - - if (-not $products.ContainsKey($prod.ToLower())) { - $products[$prod.ToLower()] = $version - $script:C2RSuite[$prod] = "$prod - $version" - } - } - } - } - - # Office C2R (QR7) - Write-LogOnly "Check for Office C2R products (QR7)" - $qr7Products = Get-RegistryValue -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Office\ClickToRun\Configuration" ` - -ValueName "ProductReleaseIds" - - if ($qr7Products) { - foreach ($prod in ($qr7Products -split ',')) { - Write-LogOnly "Found Office C2R product in Configuration: $prod" - if (-not $products.ContainsKey($prod.ToLower())) { - $version = Get-RegistryValue -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Office\ClickToRun\ProductReleaseIDs\Active\culture" ` - -ValueName "x-none" - $products[$prod.ToLower()] = $version - $script:C2RSuite[$prod] = "$prod - $version" - } - } - } - - # O16 Configuration (QR6) - Write-LogOnly "Check for O16 C2R products (QR6)" - $o16Products = Get-RegistryValue -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Office\16.0\ClickToRun\Configuration" ` - -ValueName "ProductReleaseIds" - - if ($o16Products) { - foreach ($prod in ($o16Products -split ',')) { - Write-LogOnly "Found O16 (QR6) C2R product in Configuration: $prod" - if (-not $products.ContainsKey($prod.ToLower())) { - $version = Get-RegistryValue -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Office\16.0\ClickToRun\ProductReleaseIDs\culture" ` - -ValueName "x-none" - $products[$prod.ToLower()] = $version - $script:C2RSuite[$prod] = "$prod - $version" - } - } - } - - # ARP Check - Write-LogOnly "Check ARP for Office C2R products" - $arpKeys = Get-RegistryKeys -Hive LocalMachine -SubKey $script:REG_ARP - - foreach ($arpKey in $arpKeys) { - $uninstallString = Get-RegistryValue -Hive LocalMachine ` - -SubKey "$($script:REG_ARP)$arpKey" ` - -ValueName "UninstallString" - - if ($uninstallString -and - (($uninstallString -like "*Microsoft Office 1*") -or - ($uninstallString -like "*OfficeClickToRun.exe*"))) { - - $displayVersion = Get-RegistryValue -Hive LocalMachine ` - -SubKey "$($script:REG_ARP)$arpKey" ` - -ValueName "DisplayVersion" - - # Extract product ID from uninstall string - if ($uninstallString -match "productstoremove=([^\s]+)") { - $prod = $matches[1] -replace "_.*", "" -replace "\.1.*", "" - - Write-LogOnly "Found C2R product in ARP: $prod" - if (-not $products.ContainsKey($prod.ToLower())) { - $products[$prod.ToLower()] = $displayVersion - $script:C2RSuite[$arpKey] = "$prod - $displayVersion" - } - } - } - } - - $script:InstalledSku = $products - return $products -} - -function Test-IsC2R { - [CmdletBinding()] - param([string]$Path) - - if ($null -eq $script:Orchestrator) { - Write-Warning "Orchestrator not initialized. Call Initialize-Environment first." - return $false - } - - # Validate Path parameter - if ([string]::IsNullOrWhiteSpace($Path)) { - Write-Warning "Test-IsC2R: Path parameter is empty or null" - return $false - } - - try { - return $script:Orchestrator.IsC2RPath($Path) - } - catch { - Write-Warning "Test-IsC2R failed for path '$Path': $_" - return $false - } -} - -function Test-ProductInScope { - [CmdletBinding()] - param([string]$ProductCode) - - if ($null -eq $script:Orchestrator) { - Write-Warning "Orchestrator not initialized. Call Initialize-Environment first." - return $false - } - - return $script:Orchestrator.IsInScope($ProductCode) -} - -#endregion - -#region Service Operations - -function Remove-Service { - [CmdletBinding()] - param([string]$ServiceName) - - Write-Log "Attempting to delete service: $ServiceName" - - if (-not $script:DetectOnly) { - $result = $script:Orchestrator.Services.DeleteService($ServiceName) - if ($result) { - Write-Log "Successfully deleted service: $ServiceName" - } - else { - Write-Log "Failed to delete service: $ServiceName" - } - return $result - } - else { - Write-Log "Preview mode. Would delete service: $ServiceName" - return $false - } -} - -#endregion - -#region License/SPP Operations - -function Clear-OfficeLicenses { - [CmdletBinding()] - param() - - if ($script:KeepLicense) { - Write-Log "Skipping license cleanup (KeepLicense flag set)" - return - } - - Write-LogSubHeader "Cleaning Office licenses" - - # Clean OSPP - Write-Log "Cleaning OSPP licenses..." - $osVersion = [Environment]::OSVersion.Version - $versionNT = $osVersion.Major * 100 + $osVersion.Minor - - if (-not $script:DetectOnly) { - $script:Orchestrator.License.CleanOSPP($versionNT) - } - - # Clean VNext license cache - Write-Log "Cleaning VNext license cache..." - if (-not $script:DetectOnly) { - $script:Orchestrator.License.ClearVNextLicenseCache($script:LocalAppData) - } -} - -#endregion - -#region Windows Installer Cleanup - -function Clear-WindowsInstallerMetadata { - [CmdletBinding()] - param() - - Write-LogSubHeader "Cleaning Windows Installer metadata" - - $shouldDelete = { - param([string]$guid) - return Test-ProductInScope $guid - } - - if (-not $script:DetectOnly) { - Write-Log "Cleaning UpgradeCodes..." - $script:Orchestrator.WindowsInstaller.CleanupUpgradeCodes($shouldDelete) - - Write-Log "Cleaning Products..." - $script:Orchestrator.WindowsInstaller.CleanupProducts($shouldDelete) - - Write-Log "Cleaning Components..." - $script:Orchestrator.WindowsInstaller.CleanupComponents($shouldDelete) - - Write-Log "Cleaning Published Components..." - $script:Orchestrator.WindowsInstaller.CleanupPublishedComponents($shouldDelete) - } - else { - Write-Log "Preview mode. Would clean Windows Installer metadata" - } -} - -#endregion - -#region TypeLib Cleanup - -function Clear-TypeLibRegistrations { - [CmdletBinding()] - param() - - Write-LogSubHeader "Cleaning TypeLib registrations" - - if (-not $script:DetectOnly) { - $script:Orchestrator.TypeLib.CleanupKnownTypeLibs() - } - else { - Write-Log "Preview mode. Would clean TypeLib registrations" - } -} - -#endregion - -#region Shell Integration - -function Clear-ShellIntegration { - [CmdletBinding()] - param() - - Write-LogSubHeader "Cleaning shell integration" - - # Protocol Handlers - Remove-RegistryKey -Hive LocalMachine -SubKey "SOFTWARE\Classes\Protocols\Handler\osf" - - # Context Menu Handlers - $contextMenuHandlers = @( - "SOFTWARE\Classes\CLSID\{573FFD05-2805-47C2-BCE0-5F19512BEB8D}", - "SOFTWARE\Classes\CLSID\{8BA85C75-763B-4103-94EB-9470F12FE0F7}", - "SOFTWARE\Classes\CLSID\{CD55129A-B1A1-438E-A9AA-ABA463DBD3BF}", - "SOFTWARE\Classes\CLSID\{D0498E0A-45B7-42AE-A9AA-ABA463DBD3BF}", - "SOFTWARE\Classes\CLSID\{E768CD3B-BDDC-436D-9C13-E1B39CA257B1}" - ) - - foreach ($key in $contextMenuHandlers) { - Remove-RegistryKey -Hive LocalMachine -SubKey $key - } - - # Groove ShellIconOverlayIdentifiers - $overlayIdentifiers = @( - "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\Microsoft SPFS Icon Overlay 1 (ErrorConflict)", - "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\Microsoft SPFS Icon Overlay 2 (SyncInProgress)", - "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\Microsoft SPFS Icon Overlay 3 (InSync)", - "SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\Microsoft SPFS Icon Overlay 1 (ErrorConflict)", - "SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\Microsoft SPFS Icon Overlay 2 (SyncInProgress)", - "SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\Microsoft SPFS Icon Overlay 3 (InSync)" - ) - - foreach ($key in $overlayIdentifiers) { - Remove-RegistryKey -Hive LocalMachine -SubKey $key - } - - # Shell Extensions - $shellExtensions = @( - "{B28AA736-876B-46DA-B3A8-84C5E30BA492}", - "{8B02D659-EBBB-43D7-9BBA-52CF22C5B025}", - "{0875DCB6-C686-4243-9432-ADCCF0B9F2D7}", - "{42042206-2D85-11D3-8CFF-005004838597}", - "{993BE281-6695-4BA5-8A2A-7AACBFAAB69E}", - "{C41662BB-1FA0-4CE0-8DC5-9B7F8279FF97}", - "{506F4668-F13E-4AA1-BB04-B43203AB3CC0}", - "{D66DC78C-4F61-447F-942B-3FB6980118CF}", - "{46137B78-0EC3-426D-8B89-FF7C3A458B5E}", - "{8BA85C75-763B-4103-94EB-9470F12FE0F7}", - "{CD55129A-B1A1-438E-A9AA-ABA463DBD3BF}", - "{D0498E0A-45B7-42AE-A9AA-ABA463DBD3BF}", - "{E768CD3B-BDDC-436D-9C13-E1B39CA257B1}" - ) - - foreach ($guid in $shellExtensions) { - Remove-RegistryValue -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved" ` - -ValueName $guid - } - - # BHO (Browser Helper Objects) - $bhoKeys = @( - "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\{31D09BA0-12F5-4CCE-BE8A-2923E76605DA}", - "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\{B4F3A835-0E21-4959-BA22-42B3008E02FF}", - "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\{D0498E0A-45B7-42AE-A9AA-ABA463DBD3BF}", - "SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\{31D09BA0-12F5-4CCE-BE8A-2923E76605DA}", - "SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\{B4F3A835-0E21-4959-BA22-42B3008E02FF}", - "SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\{D0498E0A-45B7-42AE-A9AA-ABA463DBD3BF}" - ) - - foreach ($key in $bhoKeys) { - Remove-RegistryKey -Hive LocalMachine -SubKey $key - } - - # OneNote Namespace Extension - Remove-RegistryKey -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\{0875DCB6-C686-4243-9432-ADCCF0B9F2D7}" - - # Web Sites - Remove-RegistryKey -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\Namespace\{B28AA736-876B-46DA-B3A8-84C5E30BA492}" - Remove-RegistryKey -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\NetworkNeighborhood\Namespace\{46137B78-0EC3-426D-8B89-FF7C3A458B5E}" - - # VolumeCaches - Remove-RegistryKey -Hive LocalMachine ` - -SubKey "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches\Microsoft Office Temp Files" - - # Restart Explorer to release locks - if (-not $script:DetectOnly) { - Write-Log "Restarting Explorer..." - $script:Orchestrator.Shell.RestartExplorer() - } -} - -function Clear-Shortcuts { - [CmdletBinding()] - param( - [string]$RootPath, - [switch]$Delete, - [switch]$Unpin - ) - - if ($script:SkipSD) { - return - } - - Write-LogSubHeader "Cleaning shortcuts in: $RootPath" - - $shortcuts = Get-ChildItem -Path $RootPath -Filter "*.lnk" -Recurse -ErrorAction SilentlyContinue - - foreach ($shortcut in $shortcuts) { - try { - $shell = New-Object -ComObject WScript.Shell - $link = $shell.CreateShortcut($shortcut.FullName) - - $shouldDelete = $false - - # Check if target is C2R-related - if (Test-IsC2R $link.TargetPath) { - $shouldDelete = $true - } - # Check if target contains a product GUID - elseif ($link.TargetPath -match '\{[A-F0-9-]{36}\}') { - $guid = $matches[0] - if (Test-ProductInScope $guid) { - $shouldDelete = $true - } - } - - if ($shouldDelete) { - if ($Unpin) { - Write-LogOnly "Unpinning shortcut: $($shortcut.FullName)" - $script:Orchestrator.Shell.UnpinFromTaskbar($shortcut.FullName) - $script:Orchestrator.Shell.UnpinFromStartMenu($shortcut.FullName) - } - - if ($Delete) { - Write-LogOnly "Deleting shortcut: $($shortcut.FullName)" - if (-not $script:DetectOnly) { - Remove-Item -Path $shortcut.FullName -Force -ErrorAction SilentlyContinue - } - } - } - - [System.Runtime.Interopservices.Marshal]::ReleaseComObject($shell) | Out-Null - } - catch { - Write-LogOnly "Error processing shortcut $($shortcut.FullName): $_" - } - } -} - -#endregion - -#region Export Module Members - -Export-ModuleMember -Function @( - 'Initialize-Environment', - 'Get-SystemInfo', - 'Test-IsElevated', - 'Initialize-Log', - 'Write-LogHeader', - 'Write-LogSubHeader', - 'Write-Log', - 'Write-LogOnly', - 'Close-Log', - 'Set-ErrorCode', - 'Clear-ErrorCode', - 'Set-ReturnValue', - 'Get-RegistryValue', - 'Set-RegistryValue', - 'Remove-RegistryKey', - 'Remove-RegistryValue', - 'Get-RegistryKeys', - 'Get-RegistryValues', - 'Test-RegistryKeyExists', - 'Remove-FolderRecursive', - 'Remove-FileForced', - 'Add-PendingFileDelete', - 'Stop-OfficeProcesses', - 'Test-ProcessRunning', - 'Get-InstalledOfficeProducts', - 'Test-IsC2R', - 'Test-ProductInScope', - 'Remove-Service', - 'Clear-OfficeLicenses', - 'Clear-WindowsInstallerMetadata', - 'Clear-TypeLibRegistrations', - 'Clear-ShellIntegration', - 'Clear-Shortcuts' -) - -Export-ModuleMember -Variable @( - 'SCRIPT_VERSION', - 'SCRIPT_NAME', - 'ERROR_SUCCESS', - 'ERROR_FAIL', - 'ERROR_REBOOT_REQUIRED', - 'ERROR_USERCANCEL', - 'ERROR_STAGE1', - 'ERROR_STAGE2', - 'ERROR_INCOMPLETE', - 'ERROR_DCAF_FAILURE', - 'ERROR_ELEVATION_USERDECLINED', - 'ERROR_ELEVATION', - 'ERROR_SCRIPTINIT', - 'ERROR_RELAUNCH', - 'ERROR_UNKNOWN', - 'REG_ARP', - 'ErrorCode', - 'RebootRequired', - 'IsElevated', - 'Is64Bit', - 'OSInfo', - 'LogDir', - 'ScrubDir', - 'InstalledSku', - 'C2RSuite', - 'KeepSku', - 'KeepFolder', - 'DelInUse', - 'ProgramFiles', - 'ProgramFilesX86', - 'CommonProgramFiles', - 'CommonProgramFilesX86', - 'ProgramData', - 'AppData', - 'LocalAppData', - 'AllUsersProfile', - 'WinDir', - 'Temp', - 'WICacheDir', - 'Orchestrator' -) - -#endregion diff --git a/OfficeScrubC2R.ps1 b/OfficeScrubC2R.ps1 deleted file mode 100644 index fb6c221..0000000 --- a/OfficeScrubC2R.ps1 +++ /dev/null @@ -1,1040 +0,0 @@ -# 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 only if not already loaded (for standalone execution) -if (-not (Get-Module -Name OfficeScrubC2R-Utilities)) { - $utilitiesPath = Join-Path $PSScriptRoot "OfficeScrubC2R-Utilities.psm1" - if (Test-Path $utilitiesPath) { - Import-Module -Name $utilitiesPath -Force - } -} - -#region Main Script Functions - -function Initialize-Script { - # Script-level variables are already set by wrapper function - # Only copy from parameters if they exist (when run standalone) - if ($PSBoundParameters.ContainsKey('Quiet')) { $script:Quiet = $Quiet } - if ($PSBoundParameters.ContainsKey('DetectOnly')) { $script:DetectOnly = $DetectOnly } - if ($PSBoundParameters.ContainsKey('Force')) { $script:Force = $Force } - if ($PSBoundParameters.ContainsKey('RemoveAll')) { $script:RemoveAll = $RemoveAll } - if ($PSBoundParameters.ContainsKey('KeepLicense')) { $script:KeepLicense = $KeepLicense } - if ($PSBoundParameters.ContainsKey('Offline')) { $script:Offline = $Offline } - if ($PSBoundParameters.ContainsKey('ForceArpUninstall')) { $script:ForceArpUninstall = $ForceArpUninstall } - if ($PSBoundParameters.ContainsKey('ClearTaskBand')) { $script:ClearTaskBand = $ClearTaskBand } - if ($PSBoundParameters.ContainsKey('UnpinMode')) { $script:UnpinMode = $UnpinMode } - if ($PSBoundParameters.ContainsKey('SkipSD')) { $script:SkipSD = $SkipSD } - if ($PSBoundParameters.ContainsKey('NoElevate')) { $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 ($script:LogPath) { - $script:LogDir = $script:LogPath - Initialize-Log $script: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 ([OfficeScrubNative.RegistryHiveType]::CurrentUser) -SubKey "Software\Classes\Installer\Products" -ValidLength 32 - Ensure-ValidWIMetadata -Hive ([OfficeScrubNative.RegistryHiveType]::ClassesRoot) -SubKey "Installer\Products" -ValidLength 32 - Ensure-ValidWIMetadata -Hive ([OfficeScrubNative.RegistryHiveType]::LocalMachine) -SubKey "SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products" -ValidLength 32 - Ensure-ValidWIMetadata -Hive ([OfficeScrubNative.RegistryHiveType]::LocalMachine) -SubKey "SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Components" -ValidLength 32 - Ensure-ValidWIMetadata -Hive ([OfficeScrubNative.RegistryHiveType]::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 in C2RSuite" - } - - if ($script:InstalledSku.Count -gt 0) { - Write-Log "Installed Office products found:" - foreach ($key in $script:InstalledSku.Keys) { - Write-Log (" - {0} - {1}" -f $key, $script:InstalledSku[$key]) - } - } - else { - Write-Log "No installed Office products found" - } - - # Return true if we found any products (either in InstalledSku or C2RSuite) - return ($script:InstalledSku.Count -gt 0 -or $script:C2RSuite.Count -gt 0) -} - -function Ensure-ValidWIMetadata { - param( - [OfficeScrubNative.RegistryHiveType]$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 -and $script:InstalledSku.Count -gt 0) { - Write-Log ("Warning: Found {0} Office product(s) but no C2R items registered in ARP. Attempting uninstall anyway." -f $script:InstalledSku.Count) - # Try to build C2RSuite from InstalledSku for ODT removal - foreach ($key in $script:InstalledSku.Keys) { - if (-not $script:C2RSuite.ContainsKey($key)) { - $script:C2RSuite[$key] = $script:InstalledSku[$key] - } - } - } - - if ($script:C2RSuite.Count -eq 0) { - Write-Log ("No uninstallable C2R items registered in Uninstall: {0}" -f $script:C2RSuite.Count) - return - } - - # 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 = @" - - - - - - -"@ - 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" - $msiArgs = @("/x$product", "REBOOT=ReallySuppress", "NOREMOVESPAWN=True") - if ($script:Quiet) { - $msiArgs += "/q" - } - else { - $msiArgs += "/qb-!" - } - $msiArgs += "/l*v" - $msiArgs += "`"$logFile`"" - $msiexecArgsList += , @($product, $msiArgs, $logFile) - Write-LogOnly "Call msiexec with 'msiexec.exe $($msiArgs -join ' ')'" - } - - Stop-OfficeProcesses - - if (-not $script:DetectOnly) { - foreach ($item in $msiexecArgsList) { - $product = $item[0] - $msiArgs = $item[1] - $logFile = $item[2] - $result = Start-Process -FilePath "msiexec.exe" -ArgumentList $msiArgs -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 - } -} - -# Note: Test-ProductInScope is provided by the utilities module via the Orchestrator - -# 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 FIRST (sets up logging) - 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) - if ($_.ScriptStackTrace) { - Write-LogOnly "Stack trace: $($_.ScriptStackTrace)" - } - if ($_.Exception.Message -like "*Path*") { - Write-LogOnly "Error location: $($_.InvocationInfo.ScriptName):$($_.InvocationInfo.ScriptLineNumber)" - Write-LogOnly "Function: $($_.InvocationInfo.FunctionName)" - Write-LogOnly "Line: $($_.InvocationInfo.Line)" - } - Set-ErrorCode $script:ERROR_UNKNOWN - if ($null -ne $script:LogInitialized -and $script:LogInitialized) { - 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 \ No newline at end of file diff --git a/OfficeScrubC2R.psd1 b/OfficeScrubC2R.psd1 index 99d5b10..b71a018 100644 --- a/OfficeScrubC2R.psd1 +++ b/OfficeScrubC2R.psd1 @@ -3,7 +3,7 @@ RootModule = 'OfficeScrubC2R.psm1' # Version number of this module. - ModuleVersion = '2.19.2' + ModuleVersion = '3.0.0' # Supported PSEditions CompatiblePSEditions = @('Desktop', 'Core') @@ -22,16 +22,7 @@ # Description of the functionality provided by this module Description = @' -Complete PowerShell/C# implementation of Microsoft Office Scrub C2R tool. Provides comprehensive removal of Office 2013, 2016, 2019, and Office 365 Click-to-Run installations with 10-50x performance improvements over the original VBScript version. - -Features: -- Native C# library for high-performance registry and file operations -- Parallel processing for faster execution -- Comprehensive logging and error handling -- Support for Windows 7 SP1 through Windows 11 -- Compatible with PowerShell 5.1 and PowerShell 7+ - -Requires Administrator privileges. +Lightweight PowerShell wrapper for the converted OfficeScrub native DLL. The module focuses on surfacing the native methods/Windows API code through a small set of helper functions while keeping the original VBScript and C# reference sources in the repository for contrast. '@ # Minimum version of the PowerShell engine required by this module @@ -61,15 +52,15 @@ Requires Administrator privileges. FormatsToProcess = @() # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess - NestedModules = @('OfficeScrubC2R-Utilities.psm1') + NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = @( - 'Invoke-OfficeScrubC2R', - 'Get-InstalledOfficeProducts', - 'Test-IsC2R', - 'Initialize-Environment', - 'Stop-OfficeProcesses' + 'Import-OfficeScrubNative', + 'New-OfficeScrubOrchestrator', + 'Test-OfficeC2RPath', + 'Test-OfficeProductScope', + 'Get-OfficeScrubHelpers' ) # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. @@ -79,7 +70,7 @@ Requires Administrator privileges. VariablesToExport = @() # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. - AliasesToExport = @('Remove-OfficeC2R', 'Uninstall-OfficeC2R') + AliasesToExport = @() # DSC resources to export from this module DscResourcesToExport = @() @@ -91,14 +82,12 @@ Requires Administrator privileges. FileList = @( 'OfficeScrubC2R.psd1', 'OfficeScrubC2R.psm1', - 'OfficeScrubC2R.ps1', - 'OfficeScrubC2R-Utilities.psm1', - 'OfficeScrubC2R-Native.cs', 'OfficeScrubNative.dll', - 'build.ps1', + 'OfficeScrubC2R-Native.cs', + 'direct_conversion.cs', + 'docs\source\OfficeScrubC2R.vbs', 'LICENSE', - 'README.md', - 'docs\CHANGELOG.md' + 'README.md' ) # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. @@ -133,27 +122,18 @@ Requires Administrator privileges. # ReleaseNotes of this module ReleaseNotes = @' -# Release Notes v2.19.0 +# Release Notes v3.0.0 ## Overview -Complete PowerShell/C# port of Microsoft's OffScrubC2R.vbs v2.19 with significant performance improvements. +The module now wraps the native OfficeScrub DLL directly, exposing a handful of helper commands instead of the previous script-heavy implementation. Original VBScript and C# sources remain in the repo for reference. ## What's New -- Native C# library for high-performance operations (10-50x faster) -- Pre-compiled DLL with automatic fallback to source compilation -- Parallel processing for registry and file operations -- Comprehensive logging and error handling -- Support for PowerShell 7+ and Windows PowerShell 5.1 +- Streamlined PowerShell module focused on loading the native binary +- Helper functions for creating the orchestrator and checking product scope +- Repository cleaned to highlight the native binary alongside the original sources ## Breaking Changes -- Requires Administrator privileges -- Minimum PowerShell version is 5.1 -- .NET Framework 4.5 or later required - -## Known Issues -- None - -For full changelog, see CHANGELOG.md +- Legacy script-based functions have been removed in favor of the native wrapper '@ # Prerelease string of this module diff --git a/OfficeScrubC2R.psm1 b/OfficeScrubC2R.psm1 index d209bed..0c5d74b 100644 --- a/OfficeScrubC2R.psm1 +++ b/OfficeScrubC2R.psm1 @@ -1,243 +1,98 @@ -# -# OfficeScrubC2R Module -# PowerShell/C# implementation of Microsoft's Office Scrub C2R tool -# Version: 2.19.0 -# - -#Requires -Version 5.1 - -# Import the utilities module -$utilitiesPath = Join-Path $PSScriptRoot "OfficeScrubC2R-Utilities.psm1" -if (Test-Path $utilitiesPath) { - Import-Module $utilitiesPath -Force -} -else { - throw "Required module not found: $utilitiesPath" -} - -# Dot source the main script functions -$mainScriptPath = Join-Path $PSScriptRoot "OfficeScrubC2R.ps1" -if (Test-Path $mainScriptPath) { - . $mainScriptPath -} -else { - throw "Main script not found: $mainScriptPath" -} - -<# -.SYNOPSIS - Removes Office Click-to-Run (C2R) products from the system. - -.DESCRIPTION - Invoke-OfficeScrubC2R provides comprehensive removal of Office 2013, 2016, 2019, and Office 365 - Click-to-Run installations when standard uninstall methods fail. This is a PowerShell/C# port of - Microsoft's OffScrubC2R.vbs with 10-50x performance improvements. - - The tool performs the following operations: - - Stops all Office processes - - Uninstalls Office products using ODT - - Removes registry keys and values - - Deletes Office files and folders - - Cleans up Windows Installer metadata - - Removes scheduled tasks and services - - Clears Office licenses (optional) - - Requires Administrator privileges. - -.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. +# OfficeScrubC2R.psm1 +# Lightweight wrapper that exposes the native OfficeScrub C# binary via PowerShell. +# The module intentionally focuses on the converted DLL that uses native Windows APIs +# while keeping the original reference sources in the repository for contrast. -.PARAMETER KeepLicense - Keep Office licensing information. +$script:ModuleRoot = $PSScriptRoot +$script:NativeAssemblyPath = Join-Path $script:ModuleRoot 'OfficeScrubNative.dll' +$script:NativeAssembly = $null -.PARAMETER Offline - Run in offline mode (skip ODT download). - -.PARAMETER ForceArpUninstall - Force ARP-based uninstall. - -.PARAMETER ClearTaskBand - Clear taskband shortcuts. - -.PARAMETER UnpinMode - Unpin shortcuts from taskbar and start menu. - -.PARAMETER SkipSD - Skip scheduled deletion operations. - -.PARAMETER NoElevate - Do not attempt elevation (will fail if not already elevated). - -.PARAMETER LogPath - Specify custom log path. - -.EXAMPLE - Invoke-OfficeScrubC2R - - Interactive mode - will prompt for confirmation before removal. - -.EXAMPLE - Invoke-OfficeScrubC2R -Quiet -Force - - Silent removal without prompts or confirmation. - -.EXAMPLE - Invoke-OfficeScrubC2R -DetectOnly +function Import-OfficeScrubNative { + [CmdletBinding()] + param( + # Path to the OfficeScrub native DLL. Defaults to the copy next to the module. + [string]$Path = $script:NativeAssemblyPath + ) - Detect installed Office C2R products without removing them. + $resolvedPath = (Resolve-Path -Path $Path -ErrorAction Stop).ProviderPath -.EXAMPLE - Invoke-OfficeScrubC2R -KeepLicense -LogPath "C:\Logs" + if (-not (Test-Path -Path $resolvedPath -PathType Leaf)) { + throw "Native assembly not found at '$resolvedPath'. Ensure OfficeScrubNative.dll is present alongside the module." + } - Remove Office but keep licenses, saving logs to custom location. + if ($script:NativeAssembly -and $script:NativeAssembly.Location -eq $resolvedPath) { + return $script:NativeAssembly + } -.NOTES - Author: Calvin (PowerShell/C# port) - Original: Microsoft Corporation (OffScrubC2R.vbs) - Version: 2.19.0 - Requires: PowerShell 5.1+, .NET Framework 4.5+, Administrator privileges + $script:NativeAssembly = [AppDomain]::CurrentDomain.GetAssemblies() | + Where-Object { $_.Location -eq $resolvedPath } -.LINK - https://github.com/Calvindd2f/OfficeScrubC2R + if (-not $script:NativeAssembly) { + $script:NativeAssembly = [System.Reflection.Assembly]::LoadFrom($resolvedPath) + } -.LINK - Get-InstalledOfficeProducts + return $script:NativeAssembly +} -.LINK - Test-IsC2R -#> -function Invoke-OfficeScrubC2R { - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] - [Alias('Remove-OfficeC2R', 'Uninstall-OfficeC2R')] +function New-OfficeScrubOrchestrator { + [CmdletBinding()] param( - [Parameter(HelpMessage = "Run in quiet mode with minimal output")] - [switch]$Quiet, - - [Parameter(HelpMessage = "Only detect installed products without removing them")] - [switch]$DetectOnly, - - [Parameter(HelpMessage = "Force removal without user confirmation")] - [switch]$Force, - - [Parameter(HelpMessage = "Remove all Office products")] - [switch]$RemoveAll, + # Whether to use 64-bit registry/file views when interacting with the OS. + [bool]$Is64Bit = [Environment]::Is64BitOperatingSystem + ) - [Parameter(HelpMessage = "Keep Office licensing information")] - [switch]$KeepLicense, + Import-OfficeScrubNative | Out-Null + return [OfficeScrubNative.OfficeScrubOrchestrator]::new($Is64Bit) +} - [Parameter(HelpMessage = "Run in offline mode")] - [switch]$Offline, +function Test-OfficeC2RPath { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Path, + [OfficeScrubNative.OfficeScrubOrchestrator]$Orchestrator + ) - [Parameter(HelpMessage = "Force ARP-based uninstall")] - [switch]$ForceArpUninstall, + if (-not $PSBoundParameters.ContainsKey('Orchestrator') -or -not $Orchestrator) { + $Orchestrator = New-OfficeScrubOrchestrator + } - [Parameter(HelpMessage = "Clear taskband shortcuts")] - [switch]$ClearTaskBand, + return $Orchestrator.IsC2RPath($Path) +} - [Parameter(HelpMessage = "Unpin shortcuts from taskbar")] - [switch]$UnpinMode, +function Test-OfficeProductScope { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$ProductCode, + [OfficeScrubNative.OfficeScrubOrchestrator]$Orchestrator + ) - [Parameter(HelpMessage = "Skip scheduled deletion")] - [switch]$SkipSD, + if (-not $PSBoundParameters.ContainsKey('Orchestrator') -or -not $Orchestrator) { + $Orchestrator = New-OfficeScrubOrchestrator + } - [Parameter(HelpMessage = "Do not attempt elevation")] - [switch]$NoElevate, + return $Orchestrator.IsInScope($ProductCode) +} - [Parameter(HelpMessage = "Specify custom log path")] - [string]$LogPath +function Get-OfficeScrubHelpers { + [CmdletBinding()] + param( + [OfficeScrubNative.OfficeScrubOrchestrator]$Orchestrator ) - begin { - # Check for Administrator privileges - if (-not $NoElevate) { - $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) - $isAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) - - if (-not $isAdmin) { - throw "This command requires Administrator privileges. Please run PowerShell as Administrator." - } - } - - # Validate PowerShell version - if ($PSVersionTable.PSVersion.Major -lt 5) { - throw "This module requires PowerShell 5.1 or later. Current version: $($PSVersionTable.PSVersion)" - } - } - - process { - $shouldProceed = $true - if ($null -ne $PSCmdlet) { - try { - $shouldProceed = $PSCmdlet.ShouldProcess("Office Click-to-Run Products", "Remove") - } - catch { - # ShouldProcess not available, proceed anyway - $shouldProceed = $true - } - } - - if ($shouldProceed) { - try { - # Set script-level variables for the main script functions - $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 - $script:LogPath = $LogPath - - # Call Main function directly (script already dot-sourced) - $exitCode = Main - - # Return exit code - return $exitCode - } - catch { - Write-Error "Failed to execute OfficeScrubC2R: $_" - throw - } - } + if (-not $PSBoundParameters.ContainsKey('Orchestrator') -or -not $Orchestrator) { + $Orchestrator = New-OfficeScrubOrchestrator } - end { - # Cleanup is handled by the main script + [pscustomobject]@{ + RegistryHelper = $Orchestrator.Registry + FileHelper = $Orchestrator.Files + ProcessHelper = $Orchestrator.Processes + ShellHelper = $Orchestrator.Shell + WindowsInstallerHelper = $Orchestrator.WindowsInstaller + TypeLibHelper = $Orchestrator.TypeLib + LicenseHelper = $Orchestrator.License + ServiceHelper = $Orchestrator.Services } } -# Export module members -Export-ModuleMember -Function @( - 'Invoke-OfficeScrubC2R', - 'Get-InstalledOfficeProducts', - 'Test-IsC2R', - 'Initialize-Environment', - 'Stop-OfficeProcesses' -) -Alias @( - 'Remove-OfficeC2R', - 'Uninstall-OfficeC2R' -) - -# Initialize environment after all exports -# This must happen after the utilities module is fully loaded -try { - Initialize-Environment - Write-Verbose "OfficeScrubC2R environment initialized successfully" -} -catch { - Write-Warning "Failed to initialize OfficeScrubC2R environment: $_" -} - +Export-ModuleMember -Function Import-OfficeScrubNative, New-OfficeScrubOrchestrator, Test-OfficeC2RPath, Test-OfficeProductScope, Get-OfficeScrubHelpers diff --git a/README.md b/README.md index 67b796e..ca63556 100644 --- a/README.md +++ b/README.md @@ -1,286 +1,70 @@ -# OfficeScrubC2R - PowerShell Edition +# OfficeScrubC2R - Native Wrapper -[![PowerShell Gallery](https://img.shields.io/powershellgallery/v/OfficeScrubC2R)](https://www.powershellgallery.com/packages/OfficeScrubC2R) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![PowerShell](https://img.shields.io/badge/PowerShell-5.1+-blue.svg)](https://github.com/PowerShell/PowerShell) +This repository now focuses on a **lightweight PowerShell wrapper** around the converted `OfficeScrubNative.dll`, which implements the original OffScrubC2R logic with direct native methods and Windows API calls. The wrapper keeps the original source materials alongside the module so you can clearly see the lineage from VBScript to C#. -Complete PowerShell/C# implementation of Microsoft's **OffScrubC2R.vbs** (v2.19) with **10-50x performance improvements**. +## What's in the repository -Provides comprehensive removal of Office 2013, 2016, 2019, and Office 365 Click-to-Run installations when standard uninstall methods fail. +- `OfficeScrubC2R.psd1` / `OfficeScrubC2R.psm1` – the minimal PowerShell module that loads the native DLL and exposes a handful of helper commands. +- `OfficeScrubNative.dll` – the latest converted binary that uses native methods/Windows API calls. +- Reference sources kept for contrast: + - `docs/source/OfficeScrubC2R.vbs` – the original Microsoft OffScrubC2R VBScript. + - `direct_conversion.cs` – a direct C# conversion of the VBScript logic. + - `OfficeScrubC2R-Native.cs` – the native-method focused C# implementation that drives the DLL. +- Supporting files: `LICENSE`, `CHANGELOG.md`, and build metadata. -## 🚀 Features - -- ✅ **Full VBS Compatibility** - All 3,803 lines of VBS functionality ported -- ⚡ **High Performance** - Native C# library for 10-50x faster execution -- 🔒 **Type Safety** - Strongly-typed C# helpers -- 📊 **Comprehensive Logging** - Detailed operation logs -- 🔄 **Parallel Processing** - Multi-threaded operations -- 💾 **Smart Fallback** - Pre-compiled DLL with source compilation fallback -- 🎯 **PowerShell 7+ Support** - Works on both Windows PowerShell and PowerShell Core - -## 📋 Requirements - -- **OS**: Windows 7 SP1 or later (Windows 10/11 recommended) -- **PowerShell**: 5.1 or later -- **.NET**: Framework 4.5 or later -- **Privileges**: Administrator rights required - -## 📦 Installation - -### From PowerShell Gallery (Recommended) - -```powershell -# Install for current user -Install-Module -Name OfficeScrubC2R -Scope CurrentUser - -# Install system-wide (requires admin) -Install-Module -Name OfficeScrubC2R -Scope AllUsers -``` - -### From GitHub - -```powershell -# Clone the repository -git clone https://github.com/Calvindd2f/OfficeScrubC2R.git -cd OfficeScrubC2R - -# Import the module -Import-Module .\OfficeScrubC2R.psd1 -``` - -### Manual Installation - -1. Download the latest release -2. Extract to a PowerShell module directory: - - User: `$HOME\Documents\PowerShell\Modules\OfficeScrubC2R` - - System: `C:\Program Files\PowerShell\Modules\OfficeScrubC2R` -3. Unblock files: `Get-ChildItem -Recurse | Unblock-File` -4. Import: `Import-Module OfficeScrubC2R` - -## 🎮 Quick Start - -### Basic Usage - -```powershell -# Interactive mode (prompts for confirmation) -Invoke-OfficeScrubC2R - -# Silent mode (no prompts) -Invoke-OfficeScrubC2R -Quiet -Force - -# Detection only (no removal) -Invoke-OfficeScrubC2R -DetectOnly - -# Keep Office licenses -Invoke-OfficeScrubC2R -KeepLicense - -# Custom log location -Invoke-OfficeScrubC2R -LogPath "C:\Logs" -``` - -### Advanced Examples - -```powershell -# Check what would be removed -Invoke-OfficeScrubC2R -DetectOnly -Verbose - -# Silent removal with license preservation -Invoke-OfficeScrubC2R -Quiet -Force -KeepLicense - -# Unpin Office from taskbar only -Invoke-OfficeScrubC2R -UnpinMode -SkipSD - -# Offline mode (no ODT download) -Invoke-OfficeScrubC2R -Offline -Force -``` - -### Using Aliases - -```powershell -# Short alias -Remove-OfficeC2R -Quiet -Force - -# Alternative alias -Uninstall-OfficeC2R -DetectOnly -``` - -## 📖 Available Functions - -| Function | Description | -|----------|-------------| -| `Invoke-OfficeScrubC2R` | Main removal function | -| `Get-InstalledOfficeProducts` | Detect installed Office products | -| `Test-IsC2R` | Check if path/value is C2R-related | -| `Initialize-Environment` | Initialize module environment | -| `Stop-OfficeProcesses` | Stop running Office processes | - -## 🔧 Parameters - -| Parameter | Description | -|-----------|-------------| -| `-Quiet` | Run in quiet mode with minimal output | -| `-DetectOnly` | Detect products without removing them | -| `-Force` | Skip confirmation prompts | -| `-RemoveAll` | Remove all Office products | -| `-KeepLicense` | Preserve Office licensing information | -| `-Offline` | Skip ODT download | -| `-ForceArpUninstall` | Force ARP-based uninstall | -| `-ClearTaskBand` | Clear taskband shortcuts | -| `-UnpinMode` | Unpin from taskbar/start menu | -| `-SkipSD` | Skip scheduled deletion | -| `-NoElevate` | Don't attempt elevation | -| `-LogPath` | Custom log file location | - -## 📊 Performance Comparison - -| Operation | VBScript | PowerShell/C# | Improvement | -|-----------|----------|---------------|-------------| -| Registry enumeration | ~10s | ~0.3s | **30x faster** | -| File deletion | ~15s | ~1s | **15x faster** | -| Overall execution | 5-10 min | 30-60s | **10-50x faster** | - -*Times measured on Windows 10 with Office 365 installed* - -## 🏗️ Architecture - -``` -OfficeScrubC2R/ -├── OfficeScrubC2R.psd1 # Module manifest -├── OfficeScrubC2R.psm1 # Main module -├── OfficeScrubC2R-Utilities.psm1 # Utilities module -├── OfficeScrubC2R-Native.cs # C# source -├── OfficeScrubNative.dll # Pre-compiled library -├── build.ps1 # Build script -├── en-US/ -│ └── about_OfficeScrubC2R.help.txt -├── LICENSE -├── README.md -└── CHANGELOG.md -``` - -### Native C# Components - -The `OfficeScrubNative.dll` provides high-performance operations: - -- **RegistryHelper**: Win32 registry operations with WOW64 support -- **FileHelper**: Optimized file/folder deletion with reboot scheduling -- **ProcessHelper**: Process termination and monitoring -- **WindowsInstallerHelper**: MSI metadata cleanup -- **TypeLibHelper**: COM type library cleanup -- **LicenseHelper**: Office license and SPP operations -- **ServiceHelper**: Windows service management -- **GuidHelper**: GUID encoding/decoding utilities -- **ShellHelper**: Shell integration (taskbar, start menu) - -## 🔨 Building from Source +## Quick start ```powershell -# Compile the native DLL -.\build.ps1 - -# Clean and rebuild -.\build.ps1 -Clean - -# Test the module +# Import the wrapper module from the repository root Import-Module .\OfficeScrubC2R.psd1 -Force -Invoke-OfficeScrubC2R -DetectOnly -``` - -## 📝 Error Codes -| Code | Description | -|------|-------------| -| 0 | Success | -| 1 | General failure | -| 2 | Reboot required | -| 4 | User cancelled | -| 8 | MSI uninstall failed | -| 16 | Cleanup failed | -| 32 | Incomplete removal | -| 64 | Second attempt still incomplete | -| 128 | User declined elevation | -| 256 | Elevation failed | -| 512 | Initialization error | -| 1024 | Relaunch error | -| 2048 | Unknown error | +# Load the native DLL and create an orchestrator (defaults to your OS bitness) +$orchestrator = New-OfficeScrubOrchestrator -## 🛠️ Troubleshooting +# Check whether a path looks like a Click-to-Run installation +Test-OfficeC2RPath -Path "C:\\Program Files\\Microsoft Office" -### DLL Load Failure +# Check whether a product code is in scope for scrubbing +Test-OfficeProductScope -ProductCode '{9AC08E99-230B-47E8-9721-4577B7F124EA}' -```powershell -# Unblock the DLL -Unblock-File .\OfficeScrubNative.dll - -# Verify .NET version -[System.Environment]::Version +# Surface the helper classes from the native DLL for direct use +$helpers = Get-OfficeScrubHelpers -Orchestrator $orchestrator +$helpers.RegistryHelper ``` -### Incomplete Removal +The module only concerns itself with loading the native binary and handing you the orchestrator/helpers. Any higher-level orchestration can be built on top using these primitives. -1. Reboot the system -2. Run the tool again: `Invoke-OfficeScrubC2R -Force` -3. Check logs in `$env:TEMP\OfficeScrubC2R\` +## Helper commands -### Permission Issues +| Command | Purpose | +| --- | --- | +| `Import-OfficeScrubNative` | Loads `OfficeScrubNative.dll` from the module folder (or a provided path). | +| `New-OfficeScrubOrchestrator` | Creates the orchestrator that wires up all helper classes with the correct bitness. | +| `Test-OfficeC2RPath` | Uses the native helper to determine whether a path matches Click-to-Run patterns. | +| `Test-OfficeProductScope` | Tests whether a product code is in scope for scrubbing. | +| `Get-OfficeScrubHelpers` | Returns the helper class instances (registry, files, processes, MSI, etc.). | -```powershell -# Check if running as admin -Test-IsElevated +## Building the native DLL -# Re-run PowerShell as Administrator -Start-Process powershell -Verb RunAs -``` +The prebuilt `OfficeScrubNative.dll` is included. If you need to rebuild it on Windows, use `build.ps1` and the `OfficeScrubC2R-Native.cs` source as the input. -## 📜 Logs +### Verifying the native build on both PowerShell editions -Default log location: -``` -$env:TEMP\OfficeScrubC2R\__ScrubLog.txt -``` +Use `tests/Verify-NativeBuild.ps1` to confirm the native source compiles cleanly for **.NET Framework (Windows PowerShell)** and **.NET SDK (PowerShell 7+)** and that the resulting assembly can be loaded: -View recent log: ```powershell -Get-Content "$env:TEMP\OfficeScrubC2R\*.txt" | Select-Object -Last 50 -``` - -## 🤝 Contributing - -Contributions are welcome! Please: +# From Windows PowerShell 5.1 (.NET Framework) +powershell.exe -NoLogo -NoProfile -File .\tests\Verify-NativeBuild.ps1 -Verbose -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Submit a pull request +# From PowerShell 7+ (uses the .NET SDK, target defaults to net7.0-windows) +pwsh -NoLogo -NoProfile -File .\tests\Verify-NativeBuild.ps1 -Verbose -## 📄 License - -MIT License - see [LICENSE](LICENSE) file for details. - -This software is derived from Microsoft's Office Scrub C2R tool (OffScrubC2R.vbs). -Original VBScript implementation © Microsoft Corporation. - -## ⚠️ Disclaimer - -This tool performs deep system changes. Always: -- ✅ Backup important data before use -- ✅ Close all Office applications -- ✅ Run in a test environment first -- ✅ Read the logs if issues occur - -## 🔗 Links - -- **GitHub**: https://github.com/Calvindd2f/OfficeScrubC2R -- **PowerShell Gallery**: https://www.powershellgallery.com/packages/OfficeScrubC2R -- **Issues**: https://github.com/Calvindd2f/OfficeScrubC2R/issues -- **Changelog**: [CHANGELOG.md](CHANGELOG.md) - -## 👤 Author - -**Calvin** ([@Calvindd2f](https://github.com/Calvindd2f)) +# Clean build outputs from prior runs +powershell.exe -NoLogo -NoProfile -File .\tests\Verify-NativeBuild.ps1 -Clean +``` -PowerShell/C# port of Microsoft's OffScrubC2R.vbs v2.19 +The script emits warnings when the relevant compiler toolchain is not available and exercises `OfficeScrubNative.OfficeScrubOrchestrator` from the produced assembly in the matching PowerShell edition. ---- +## Licensing -⭐ If this tool helped you, please star the repository! +MIT License. See `LICENSE` for details. diff --git a/docs/REPOSITORY_STRUCTURE.md b/docs/REPOSITORY_STRUCTURE.md deleted file mode 100644 index b5586e8..0000000 --- a/docs/REPOSITORY_STRUCTURE.md +++ /dev/null @@ -1,282 +0,0 @@ -# Repository Structure - -This document describes the complete structure of the OfficeScrubC2R repository. - -## Directory Layout - -``` -OfficeScrubC2R/ -│ -├── .github/ -│ └── workflows/ -│ └── ci.yml # GitHub Actions CI/CD workflow -│ -├── docs/ -│ ├── BUILD.md # Build instructions for native DLL -│ └── source/ # Original VBS source reference -│ ├── OfficeScrubC2R.vbs -│ └── README -│ -├── en-US/ -│ └── about_OfficeScrubC2R.help.txt # PowerShell help documentation -│ -├── tests/ -│ └── Test-OfficeScrubC2R.ps1 # Test suite -│ -├── .gitignore # Git ignore patterns -├── build.ps1 # Build script for C# DLL -├── CHANGELOG.md # Version history and changes -├── CONTRIBUTING.md # Contribution guidelines -├── LICENSE # MIT License -├── OfficeScrubC2R-Native.cs # C# performance library source -├── OfficeScrubC2R-Utilities.ps1 # Legacy utilities (kept for reference) -├── OfficeScrubC2R-Utilities.psm1 # PowerShell utilities module -├── OfficeScrubC2R.ps1 # Main script (can be run standalone) -├── OfficeScrubC2R.psd1 # Module manifest -├── OfficeScrubC2R.psm1 # Main module file -├── OfficeScrubNative.dll # Pre-compiled C# library -├── PSScriptAnalyzerSettings.psd1 # Code analysis settings -├── PUBLISH.md # Publishing guide for PSGallery -├── README.md # Main documentation -└── REPOSITORY_STRUCTURE.md # This file -``` - -## Core Files - -### Module Files - -| File | Purpose | -| ------------------------------- | ------------------------------------------------------------------------- | -| `OfficeScrubC2R.psd1` | Module manifest with metadata for PowerShell Gallery | -| `OfficeScrubC2R.psm1` | Main module that exports functions and imports utilities | -| `OfficeScrubC2R-Utilities.psm1` | Utilities module with helper functions | -| `OfficeScrubC2R.ps1` | Main script with all removal logic (can run standalone or be dot-sourced) | - -### Native Components - -| File | Purpose | -| -------------------------- | ---------------------------------------------- | -| `OfficeScrubC2R-Native.cs` | C# source code for high-performance operations | -| `OfficeScrubNative.dll` | Pre-compiled native library (faster loading) | -| `build.ps1` | Script to compile the DLL from source | - -### Documentation - -| File | Purpose | -| ------------------------------------- | ------------------------------------------------- | -| `README.md` | Main documentation with usage examples | -| `CHANGELOG.md` | Version history and release notes | -| `CONTRIBUTING.md` | Guidelines for contributors | -| `PUBLISH.md` | Instructions for publishing to PowerShell Gallery | -| `docs/BUILD.md` | Detailed build instructions | -| `en-US/about_OfficeScrubC2R.help.txt` | PowerShell help file | -| `REPOSITORY_STRUCTURE.md` | This file | - -### Configuration - -| File | Purpose | -| ------------------------------- | ------------------------------------- | -| `.gitignore` | Files and folders to exclude from git | -| `PSScriptAnalyzerSettings.psd1` | Code quality rules | -| `.github/workflows/ci.yml` | Continuous integration configuration | -| `LICENSE` | MIT License with attribution | - -## Module Architecture - -### Loading Sequence - -1. **User runs**: `Import-Module OfficeScrubC2R` -2. **PowerShell loads**: `OfficeScrubC2R.psm1` (root module) -3. **Root module imports**: `OfficeScrubC2R-Utilities.psm1` (nested module) -4. **Utilities module loads**: `OfficeScrubNative.dll` (native assembly) -5. **Root module dot-sources**: `OfficeScrubC2R.ps1` (main script functions) -6. **Module exports**: Public functions to user session - -### Function Organization - -#### Public Functions (Exported) - -- `Invoke-OfficeScrubC2R` - Main removal function -- `Get-InstalledOfficeProducts` - Detection function -- `Test-IsC2R` - Validation function -- `Initialize-Environment` - Setup function -- `Stop-OfficeProcesses` - Process management - -#### Aliases - -- `Remove-OfficeC2R` → `Invoke-OfficeScrubC2R` -- `Uninstall-OfficeC2R` → `Invoke-OfficeScrubC2R` - -#### Internal Functions (Not Exported) - -All other functions in utilities and main script are internal. - -### Native C# Components - -The `OfficeScrubNative.dll` contains: - -``` -OfficeScrubNative -├── RegistryHiveType (Enum) -├── OfficeConstants (Static Class) -├── GuidHelper (Static Class) -├── RegistryHelper (Class) -├── FileHelper (Class) -├── ProcessHelper (Class) -├── ShellHelper (Class) -├── WindowsInstallerHelper (Class) -├── TypeLibHelper (Class) -├── LicenseHelper (Class) -├── ServiceHelper (Class) -└── OfficeScrubOrchestrator (Class) -``` - -## PowerShell Gallery Requirements - -### Required Files - -- ✅ Module manifest (`.psd1`) -- ✅ Module file (`.psm1`) -- ✅ License file (`LICENSE`) -- ✅ README with examples -- ✅ Native assembly (`.dll`) - -### Manifest Metadata - -- ✅ Unique GUID -- ✅ Semantic version (2.19.0) -- ✅ Author information -- ✅ Description -- ✅ Tags for discovery -- ✅ Project URI -- ✅ License URI -- ✅ Release notes -- ✅ Function exports -- ✅ Required assemblies -- ✅ PowerShell version (5.1+) -- ✅ Compatible editions (Desktop, Core) - -### Quality Checks - -- ✅ PSScriptAnalyzer compliant -- ✅ Help documentation -- ✅ Examples in README -- ✅ Error handling -- ✅ Logging -- ✅ Parameter validation - -## GitHub Repository Setup - -### Branch Strategy - -- `main` - Stable releases -- `develop` - Development branch -- `feature/*` - Feature branches -- `hotfix/*` - Urgent fixes - -### GitHub Actions - -- CI on push/PR -- Module validation -- PSScriptAnalyzer checks -- Build verification -- Function tests - -### Releases - -1. Tag: `v2.19.0` -2. GitHub Release with notes -3. Attached assets (DLL) -4. Published to PowerShell Gallery - -## Installation Methods - -### From PowerShell Gallery - -```powershell -Install-Module -Name OfficeScrubC2R -``` - -### From GitHub - -```powershell -git clone https://github.com/Calvindd2f/OfficeScrubC2R.git -Import-Module .\OfficeScrubC2R\OfficeScrubC2R.psd1 -``` - -### Manual - -1. Download release -2. Extract to module path -3. Unblock files -4. Import module - -## Development Workflow - -### Setup - -```powershell -git clone https://github.com/Calvindd2f/OfficeScrubC2R.git -cd OfficeScrubC2R -.\build.ps1 -Import-Module .\OfficeScrubC2R.psd1 -Force -``` - -### Making Changes - -1. Create feature branch -2. Make changes -3. Run PSScriptAnalyzer -4. Test thoroughly -5. Update CHANGELOG.md -6. Create pull request - -### Building - -```powershell -.\build.ps1 # Build DLL -.\build.ps1 -Clean # Clean and rebuild -``` - -### Testing - -```powershell -# Import module -Import-Module .\OfficeScrubC2R.psd1 -Force -Verbose - -# Run detection -Invoke-OfficeScrubC2R -DetectOnly - -# Check for issues -Invoke-ScriptAnalyzer -Path . -Recurse -``` - -## File Size Considerations - -| File Type | Size Range | Notes | -| ---------------- | ---------- | ---------------- | -| `.ps1` scripts | 30-50 KB | Main logic files | -| `.psm1` modules | 20-40 KB | Module files | -| `.psd1` manifest | 5-10 KB | Metadata | -| `.dll` assembly | 35-40 KB | Compiled C# | -| `.cs` source | 40-50 KB | C# source | -| `.md` docs | 5-20 KB | Documentation | - -**Total module size**: ~200-250 KB - -## Version History - -- **2.19.0**: Initial PowerShell Gallery release -- **0.1.0**: Development version - -## Support - -- **Issues**: https://github.com/Calvindd2f/OfficeScrubC2R/issues -- **Discussions**: GitHub Discussions -- **Email**: calvindd2f@gmail.com - -## License - -MIT License - See LICENSE file for details. - -Derived from Microsoft's OffScrubC2R.vbs © Microsoft Corporation. diff --git a/rebuild.ps1 b/rebuild.ps1 deleted file mode 100644 index 5f9285c..0000000 --- a/rebuild.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -# Rebuild script that handles locked DLL -# This script should be run in a NEW PowerShell session - -Write-Host "OfficeScrubC2R Rebuild Helper" -ForegroundColor Cyan -Write-Host "============================`n" -ForegroundColor Cyan - -# Check if DLL is locked -$dllPath = ".\OfficeScrubNative.dll" -if (Test-Path $dllPath) { - try { - $file = [System.IO.File]::Open($dllPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) - $file.Close() - Write-Host "DLL is not locked - proceeding with rebuild" -ForegroundColor Green - } - catch { - Write-Host "DLL is currently locked (module is loaded)" -ForegroundColor Yellow - Write-Host "Please close all PowerShell sessions and run this script in a new session." -ForegroundColor Yellow - Write-Host "`nAlternatively, you can:" -ForegroundColor Cyan - Write-Host "1. Close this PowerShell window" -ForegroundColor Gray - Write-Host "2. Open a new PowerShell window" -ForegroundColor Gray - Write-Host "3. Navigate to: $PSScriptRoot" -ForegroundColor Gray - Write-Host "4. Run: .\build.ps1 -Clean" -ForegroundColor Gray - exit 1 - } -} - -# Build -Write-Host "`nStarting build..." -ForegroundColor Yellow -& .\build.ps1 -Clean - -if ($LASTEXITCODE -eq 0) { - Write-Host "`nBuild completed successfully!" -ForegroundColor Green - Write-Host "You can now import the module: Import-Module .\OfficeScrubC2R.psd1" -ForegroundColor Cyan -} -else { - Write-Host "`nBuild failed. Check errors above." -ForegroundColor Red - exit $LASTEXITCODE -} - diff --git a/tests/Test-OfficeScrubC2R.ps1 b/tests/Test-OfficeScrubC2R.ps1 deleted file mode 100644 index 23b511f..0000000 --- a/tests/Test-OfficeScrubC2R.ps1 +++ /dev/null @@ -1,660 +0,0 @@ -# Test-OfficeScrubC2R.ps1 -# Comprehensive test script for Office C2R removal functionality - -[CmdletBinding()] -param( - [switch]$TestDetection, - [switch]$TestRegistry, - [switch]$TestFileOperations, - [switch]$TestNativeCode, - [switch]$TestIntegration, - [switch]$TestAll, - [switch]$InstallOfficeViaWinget, - [switch]$FullCycle -) - -# Import the utility module -Import-Module -Name (Join-Path (Split-Path $PSScriptRoot -Parent) "OfficeScrubC2R.psd1") -Force - -function Test-OfficeDetection { - Write-Host "Testing Office Detection Functions..." -ForegroundColor Green - - # Test C2R detection - $testValues = @( - "officeclicktorun", - "o365proplus", - "o365business", - "microsoft office 2016", - "notoffice" - ) - - foreach ($value in $testValues) { - $isC2R = Test-IsC2R $value - Write-Host " $value -> IsC2R: $isC2R" -ForegroundColor $(if ($isC2R) { "Yellow" } else { "Gray" }) - } - - # Test registry reading - Write-Host "`nTesting Registry Operations..." -ForegroundColor Green - - $testKeys = @( - "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", - "SOFTWARE\Microsoft\Office\ClickToRun\Configuration" - ) - - foreach ($key in $testKeys) { - $exists = Test-RegistryKeyExists -Hive LocalMachine -SubKey $key - Write-Host " $key -> Exists: $exists" -ForegroundColor $(if ($exists) { "Green" } else { "Red" }) - - if ($exists) { - $subKeys = Get-RegistryKeys -Hive LocalMachine -SubKey $key - Write-Host " SubKeys: $($subKeys.Count)" -ForegroundColor Cyan - } - } -} - -function Test-RegistryOperations { - Write-Host "`nTesting Registry Operations..." -ForegroundColor Green - - # Test registry value operations - $testKey = "SOFTWARE\Microsoft\Office\Test" - $testValue = "TestValue" - $testData = "TestData" - - try { - # Test setting a value - $result = Set-RegistryValue -Hive LocalMachine -SubKey $testKey -ValueName $testValue -Value $testData - Write-Host " Set registry value: $result" -ForegroundColor $(if ($result) { "Green" } else { "Red" }) - - # Test reading the value - $readValue = Get-RegistryValue -Hive LocalMachine -SubKey $testKey -ValueName $testValue - Write-Host " Read registry value: $readValue" -ForegroundColor $(if ($readValue -eq $testData) { "Green" } else { "Red" }) - - # Test removing the value - $removeResult = Remove-RegistryValue -Hive LocalMachine -SubKey $testKey -ValueName $testValue - Write-Host " Remove registry value: $removeResult" -ForegroundColor $(if ($removeResult) { "Green" } else { "Red" }) - - # Clean up test key - Remove-RegistryKey -Hive LocalMachine -SubKey $testKey - Write-Host " Cleaned up test key" -ForegroundColor Green - - } - catch { - Write-Host " Registry test failed: $($_.Exception.Message)" -ForegroundColor Red - } -} - -function Test-FileOperations { - Write-Host "`nTesting File Operations..." -ForegroundColor Green - - $testDir = Join-Path $env:TEMP "OfficeScrubTest" - $testFile = Join-Path $testDir "testfile.txt" - - try { - # Create test directory and file - New-Item -ItemType Directory -Path $testDir -Force | Out-Null - Set-Content -Path $testFile -Value "Test content" - Write-Host " Created test file: $testFile" -ForegroundColor Green - - # Test file deletion - $deleteResult = Remove-FileFast $testFile - Write-Host " Delete file: $deleteResult" -ForegroundColor $(if ($deleteResult) { "Green" } else { "Red" }) - - # Test directory deletion - $dirDeleteResult = Remove-FolderFast $testDir - Write-Host " Delete directory: $dirDeleteResult" -ForegroundColor $(if ($dirDeleteResult) { "Green" } else { "Red" }) - - } - catch { - Write-Host " File operations test failed: $($_.Exception.Message)" -ForegroundColor Red - } -} - -function Test-ProcessManagement { - Write-Host "`nTesting Process Management..." -ForegroundColor Green - - # Test process detection - $testProcesses = @("notepad", "calc") - foreach ($processName in $testProcesses) { - $isRunning = Test-ProcessRunning $processName - Write-Host " $processName running: $isRunning" -ForegroundColor $(if ($isRunning) { "Yellow" } else { "Gray" }) - } -} - -function Test-SystemInfo { - Write-Host "`nTesting System Information..." -ForegroundColor Green - - # Test elevation check - $isElevated = Test-IsElevated - Write-Host " Is Elevated: $isElevated" -ForegroundColor $(if ($isElevated) { "Green" } else { "Yellow" }) - - # Test system info - Get-SystemInfo - Write-Host " OS Version: $script:OSVersion" -ForegroundColor Cyan - Write-Host " Is 64-bit: $script:Is64Bit" -ForegroundColor Cyan - Write-Host " OS Info: $script:OSInfo" -ForegroundColor Cyan -} - -function Test-OfficeProductDetection { - Write-Host "`nTesting Office Product Detection..." -ForegroundColor Green - - try { - $products = Get-InstalledOfficeProducts - Write-Host " Found $($products.Count) Office products" -ForegroundColor Cyan - - foreach ($product in $products.GetEnumerator()) { - Write-Host " $($product.Key): $($product.Value)" -ForegroundColor Yellow - } - - Write-Host " Found $($script:C2RSuite.Count) C2R suite entries" -ForegroundColor Cyan - foreach ($suite in $script:C2RSuite.GetEnumerator()) { - Write-Host " $($suite.Key): $($suite.Value)" -ForegroundColor Yellow - } - - } - catch { - Write-Host " Office product detection failed: $($_.Exception.Message)" -ForegroundColor Red - } -} - -function Test-GUIDOperations { - Write-Host "`nTesting GUID Operations..." -ForegroundColor Green - - $testGuids = @( - "12345678901234567890123456789012", - "{12345678-1234-1234-1234-123456789012}", - "12345678-1234-1234-1234-123456789012" - ) - - foreach ($guid in $testGuids) { - $expanded = Get-ExpandedGuid $guid - $compressed = Get-CompressedGuid $guid - Write-Host " Original: $guid" -ForegroundColor Gray - Write-Host " Expanded: $expanded" -ForegroundColor Cyan - Write-Host " Compressed: $compressed" -ForegroundColor Cyan - } -} - -function Test-ErrorHandling { - Write-Host "`nTesting Error Handling..." -ForegroundColor Green - - # Test error code setting - Set-ErrorCode $script:ERROR_REBOOT_REQUIRED - Write-Host " Set ERROR_REBOOT_REQUIRED: $script:ErrorCode" -ForegroundColor Yellow - - Clear-ErrorCode $script:ERROR_REBOOT_REQUIRED - Write-Host " Cleared ERROR_REBOOT_REQUIRED: $script:ErrorCode" -ForegroundColor Green - - # Test return value setting - Set-ReturnValue $script:ERROR_SUCCESS - Write-Host " Set return value to SUCCESS" -ForegroundColor Green -} - -function Test-Logging { - Write-Host "`nTesting Logging Functions..." -ForegroundColor Green - - $testLogPath = Join-Path $env:TEMP "OfficeScrubTest.log" - Initialize-Log $testLogPath - - Write-LogHeader "Test Header" - Write-LogSubHeader "Test Sub Header" - Write-Log "Test log message" - Write-LogOnly "Test log only message" - - Write-Host " Log file created at: $testLogPath" -ForegroundColor Cyan - - if (Test-Path $testLogPath) { - $logContent = Get-Content $testLogPath - Write-Host " Log entries: $($logContent.Count)" -ForegroundColor Green - Remove-Item $testLogPath -Force - } -} - -function Test-OfficeInstallation { - Write-Host "`nChecking for Office Installation..." -ForegroundColor Green - - # Check via Winget - try { - $wingetOffice = winget list --id Microsoft.Office --exact 2>$null - if ($LASTEXITCODE -eq 0 -and $wingetOffice) { - Write-Host " Office detected via Winget" -ForegroundColor Green - return $true - } - } - catch { - Write-Host " Winget check failed: $($_.Exception.Message)" -ForegroundColor Yellow - } - - # Check via registry - $products = Get-InstalledOfficeProducts - if ($products.Count -gt 0) { - Write-Host " Office detected via registry ($($products.Count) products)" -ForegroundColor Green - return $true - } - - # Check common installation paths - $commonPaths = @( - "$env:ProgramFiles\Microsoft Office", - "${env:ProgramFiles(x86)}\Microsoft Office", - "$env:ProgramFiles\Microsoft Office\root\Office16", - "${env:ProgramFiles(x86)}\Microsoft Office\root\Office16" - ) - - foreach ($path in $commonPaths) { - if (Test-Path $path) { - Write-Host " Office detected at: $path" -ForegroundColor Green - return $true - } - } - - Write-Host " Office not detected on system" -ForegroundColor Yellow - return $false -} - -function Install-OfficeViaWinget { - Write-Host "`nInstalling Office via Winget..." -ForegroundColor Green - - # Check if Winget is available - try { - $wingetVersion = winget --version - Write-Host " Winget version: $wingetVersion" -ForegroundColor Cyan - } - catch { - Write-Host " ERROR: Winget is not available on this system" -ForegroundColor Red - Write-Host " Please install Windows App Installer from the Microsoft Store" -ForegroundColor Yellow - return $false - } - - # Search for Office - Write-Host "`n Searching for Office packages..." -ForegroundColor Cyan - try { - $searchResult = winget search "Microsoft 365" --source winget 2>&1 - Write-Host " Available Office packages:" -ForegroundColor Yellow - Write-Host $searchResult - } - catch { - Write-Host " Search failed: $($_.Exception.Message)" -ForegroundColor Red - } - - Write-Host "`n NOTE: Office installation via Winget requires:" -ForegroundColor Yellow - Write-Host " 1. A valid Microsoft 365 subscription" -ForegroundColor White - Write-Host " 2. Administrative privileges" -ForegroundColor White - Write-Host " 3. Internet connectivity" -ForegroundColor White - Write-Host " 4. User interaction for sign-in" -ForegroundColor White - - $confirm = Read-Host "`n Do you want to install Microsoft 365 Apps? (Y/N)" - if ($confirm -notmatch "^[Yy]") { - Write-Host " Installation cancelled by user" -ForegroundColor Yellow - return $false - } - - # Install Office (Microsoft 365 Apps) - Write-Host "`n Installing Microsoft 365 Apps..." -ForegroundColor Green - Write-Host " This may take 10-30 minutes depending on your connection..." -ForegroundColor Cyan - - try { - $installResult = winget install --id Microsoft.Office --exact --accept-source-agreements --accept-package-agreements 2>&1 - Write-Host $installResult - - if ($LASTEXITCODE -eq 0) { - Write-Host "`n Office installation completed successfully!" -ForegroundColor Green - Write-Host " You may need to restart your computer" -ForegroundColor Yellow - return $true - } - else { - Write-Host "`n Installation returned exit code: $LASTEXITCODE" -ForegroundColor Yellow - return $false - } - } - catch { - Write-Host "`n Installation failed: $($_.Exception.Message)" -ForegroundColor Red - return $false - } -} - -function Test-NativeCode { - Write-Host "`nTesting Native C# Code Integration..." -ForegroundColor Green - - try { - # Initialize environment to load C# types - Initialize-Environment - - # Test Orchestrator initialization - Write-Host " Testing Orchestrator initialization..." -ForegroundColor Cyan - $is64Bit = [Environment]::Is64BitOperatingSystem - $orchestrator = New-Object OfficeScrubNative.OfficeScrubOrchestrator($is64Bit) - - if ($orchestrator) { - Write-Host " Orchestrator created successfully" -ForegroundColor Green - } - else { - Write-Host " Failed to create Orchestrator" -ForegroundColor Red - return $false - } - - # Test GUID Helper - Write-Host "`n Testing GuidHelper..." -ForegroundColor Cyan - $testCompressed = "00004159110000000000000000F01FEC" - $expanded = [OfficeScrubNative.GuidHelper]::GetExpandedGuid($testCompressed) - Write-Host " Compressed: $testCompressed" -ForegroundColor Gray - Write-Host " Expanded: $expanded" -ForegroundColor Yellow - - if ($expanded -and $expanded.Length -eq 38) { - Write-Host " GUID expansion: PASS" -ForegroundColor Green - } - else { - Write-Host " GUID expansion: FAIL" -ForegroundColor Red - } - - # Test reverse operation - $recompressed = [OfficeScrubNative.GuidHelper]::GetCompressedGuid($expanded) - Write-Host " Recompressed: $recompressed" -ForegroundColor Yellow - - if ($recompressed -eq $testCompressed) { - Write-Host " GUID round-trip: PASS" -ForegroundColor Green - } - else { - Write-Host " GUID round-trip: FAIL" -ForegroundColor Red - } - - # Test C2R Path Detection - Write-Host "`n Testing C2R Path Detection..." -ForegroundColor Cyan - $testPaths = @( - "C:\Program Files\Microsoft Office\root\Office16", - "C:\Program Files\Microsoft shared\ClickToRun", - "C:\Windows\System32" - ) - - foreach ($path in $testPaths) { - $isC2R = $orchestrator.IsC2RPath($path) - $status = if ($isC2R) { "C2R" } else { "Not C2R" } - $color = if ($isC2R) { "Yellow" } else { "Gray" } - Write-Host " $path -> $status" -ForegroundColor $color - } - - # Test Product Scope - Write-Host "`n Testing Product Scope..." -ForegroundColor Cyan - $testProducts = @( - "{90150000-008F-0000-1000-0000000FF1CE}", # O365 ProPlus - "{90160000-007E-0000-0000-0000000FF1CE}", # O365 Business - "{91150000-0011-0000-0000-0000000FF1CE}" # Out of scope - ) - - foreach ($product in $testProducts) { - $inScope = $orchestrator.IsInScope($product) - $status = if ($inScope) { "In Scope" } else { "Out of Scope" } - $color = if ($inScope) { "Yellow" } else { "Gray" } - Write-Host " $product -> $status" -ForegroundColor $color - } - - # Test ProcessHelper - Write-Host "`n Testing ProcessHelper..." -ForegroundColor Cyan - $isNotepadRunning = $orchestrator.Processes.IsProcessRunning("notepad") - Write-Host " Notepad running: $isNotepadRunning" -ForegroundColor $(if ($isNotepadRunning) { "Yellow" } else { "Gray" }) - - return $true - } - catch { - Write-Host " Native code test failed: $($_.Exception.Message)" -ForegroundColor Red - Write-Host " Stack trace: $($_.Exception.StackTrace)" -ForegroundColor DarkGray - return $false - } -} - -function Test-Integration { - Write-Host "`nTesting Integration Scenarios..." -ForegroundColor Green - - # Test 1: End-to-End Detection Flow - Write-Host "`n Test 1: End-to-End Detection Flow" -ForegroundColor Cyan - try { - Initialize-Environment - Get-SystemInfo - - Write-Host " Environment initialized" -ForegroundColor Green - Write-Host " 64-bit system: $script:Is64Bit" -ForegroundColor Yellow - - $products = Get-InstalledOfficeProducts - Write-Host " Detected $($products.Count) Office products" -ForegroundColor Yellow - - if ($products.Count -gt 0) { - Write-Host " Detection flow: PASS" -ForegroundColor Green - } - else { - Write-Host " Detection flow: No products found (expected if Office not installed)" -ForegroundColor Yellow - } - } - catch { - Write-Host " Detection flow: FAIL - $($_.Exception.Message)" -ForegroundColor Red - } - - # Test 2: Registry Operations Integration - Write-Host "`n Test 2: Registry Operations Integration" -ForegroundColor Cyan - try { - $testKey = "SOFTWARE\OfficeScrubbTest_$(Get-Random)" - $testValue = "TestValue" - $testData = "TestData_$(Get-Date -Format 'yyyyMMddHHmmss')" - - # Create - $setResult = Set-RegistryValue -Hive LocalMachine -SubKey $testKey -ValueName $testValue -Value $testData - Write-Host " Set value: $setResult" -ForegroundColor $(if ($setResult) { "Green" } else { "Red" }) - - # Read - $getValue = Get-RegistryValue -Hive LocalMachine -SubKey $testKey -ValueName $testValue - $readMatch = ($getValue -eq $testData) - Write-Host " Read value: $readMatch" -ForegroundColor $(if ($readMatch) { "Green" } else { "Red" }) - - # Check existence - $exists = Test-RegistryKeyExists -Hive LocalMachine -SubKey $testKey - Write-Host " Key exists: $exists" -ForegroundColor $(if ($exists) { "Green" } else { "Red" }) - - # Clean up - Remove-RegistryKey -Hive LocalMachine -SubKey $testKey - $stillExists = Test-RegistryKeyExists -Hive LocalMachine -SubKey $testKey - Write-Host " Cleanup: $(!$stillExists)" -ForegroundColor $(if (!$stillExists) { "Green" } else { "Red" }) - - if ($setResult -and $readMatch -and $exists -and !$stillExists) { - Write-Host " Registry integration: PASS" -ForegroundColor Green - } - else { - Write-Host " Registry integration: FAIL" -ForegroundColor Red - } - } - catch { - Write-Host " Registry integration: FAIL - $($_.Exception.Message)" -ForegroundColor Red - } - - # Test 3: File Operations Integration - Write-Host "`n Test 3: File Operations Integration" -ForegroundColor Cyan - try { - $testDir = Join-Path $env:TEMP "OfficeScrubbTest_$(Get-Random)" - $testFile = Join-Path $testDir "test.txt" - - # Create test environment - New-Item -ItemType Directory -Path $testDir -Force | Out-Null - Set-Content -Path $testFile -Value "Test content" - - # Test file deletion via C# helper - $fileExists = Test-Path $testFile - Write-Host " Test file created: $fileExists" -ForegroundColor $(if ($fileExists) { "Green" } else { "Red" }) - - $deleteResult = Remove-FileForced -Path $testFile - $fileDeleted = -not (Test-Path $testFile) - Write-Host " File deleted: $fileDeleted" -ForegroundColor $(if ($fileDeleted) { "Green" } else { "Red" }) - - # Test folder deletion - $folderDeleteResult = Remove-FolderRecursive -Path $testDir - $folderDeleted = -not (Test-Path $testDir) - Write-Host " Folder deleted: $folderDeleted" -ForegroundColor $(if ($folderDeleted) { "Green" } else { "Red" }) - - if ($fileExists -and $fileDeleted -and $folderDeleted) { - Write-Host " File operations integration: PASS" -ForegroundColor Green - } - else { - Write-Host " File operations integration: FAIL" -ForegroundColor Red - } - } - catch { - Write-Host " File operations integration: FAIL - $($_.Exception.Message)" -ForegroundColor Red - # Clean up on error - if (Test-Path $testDir) { - Remove-Item $testDir -Recurse -Force -ErrorAction SilentlyContinue - } - } -} - -function Test-FullCycle { - Write-Host "`n" + "="*70 -ForegroundColor Magenta - Write-Host "FULL CYCLE TEST: Install -> Detect -> Remove" -ForegroundColor Magenta - Write-Host "="*70 -ForegroundColor Magenta - - Write-Host "`nWARNING: This test will:" -ForegroundColor Yellow - Write-Host " 1. Check if Office is installed" -ForegroundColor White - Write-Host " 2. Optionally install Office via Winget (if not found)" -ForegroundColor White - Write-Host " 3. Detect Office installation" -ForegroundColor White - Write-Host " 4. Run Office scrubber in DETECT ONLY mode" -ForegroundColor White - Write-Host " 5. Optionally run full removal (DESTRUCTIVE)" -ForegroundColor White - - $confirm = Read-Host "`nDo you want to proceed? (Y/N)" - if ($confirm -notmatch "^[Yy]") { - Write-Host "Full cycle test cancelled" -ForegroundColor Yellow - return - } - - # Phase 1: Check Installation - Write-Host "`n--- Phase 1: Check Installation ---" -ForegroundColor Cyan - $officeInstalled = Test-OfficeInstallation - - if (-not $officeInstalled) { - Write-Host "`nOffice is not installed. Would you like to install it?" -ForegroundColor Yellow - $installConfirm = Read-Host "Install Office via Winget? (Y/N)" - - if ($installConfirm -match "^[Yy]") { - $installed = Install-OfficeViaWinget - if (-not $installed) { - Write-Host "`nOffice installation failed. Cannot continue full cycle test." -ForegroundColor Red - return - } - } - else { - Write-Host "`nSkipping full cycle test - Office not installed" -ForegroundColor Yellow - return - } - } - - # Phase 2: Detection - Write-Host "`n--- Phase 2: Run Detection ---" -ForegroundColor Cyan - try { - & (Join-Path $PSScriptRoot "OfficeScrubC2R.ps1") -DetectOnly -LogPath $env:TEMP - Write-Host "Detection completed successfully" -ForegroundColor Green - } - catch { - Write-Host "Detection failed: $($_.Exception.Message)" -ForegroundColor Red - } - - # Phase 3: Optional Removal - Write-Host "`n--- Phase 3: Optional Removal ---" -ForegroundColor Cyan - Write-Host "`nWARNING: The next step will REMOVE Office from your system!" -ForegroundColor Red - Write-Host "This is a DESTRUCTIVE operation and cannot be undone!" -ForegroundColor Red - - $removeConfirm = Read-Host "`nDo you want to proceed with Office removal? (Type 'YES' to confirm)" - - if ($removeConfirm -eq "YES") { - try { - Write-Host "`nRunning Office Scrubber..." -ForegroundColor Yellow - & (Join-Path $PSScriptRoot "OfficeScrubC2R.ps1") -Quiet -Force -LogPath $env:TEMP - Write-Host "Office removal completed" -ForegroundColor Green - } - catch { - Write-Host "Office removal failed: $($_.Exception.Message)" -ForegroundColor Red - } - } - else { - Write-Host "Removal cancelled - Office remains installed" -ForegroundColor Yellow - } -} - -function Show-TestSummary { - Write-Host "`n" + "="*70 -ForegroundColor Green - Write-Host "TEST SUITE FOR OFFICE SCRUB C2R" -ForegroundColor Green - Write-Host "="*70 -ForegroundColor Green - - Write-Host "`nAvailable Test Functions:" -ForegroundColor Yellow - Write-Host " -TestDetection : Test Office detection functions" -ForegroundColor White - Write-Host " -TestRegistry : Test registry operations" -ForegroundColor White - Write-Host " -TestFileOperations : Test file operations" -ForegroundColor White - Write-Host " -TestNativeCode : Test C# native code integration" -ForegroundColor White - Write-Host " -TestIntegration : Test integration scenarios" -ForegroundColor White - Write-Host " -TestAll : Run all tests" -ForegroundColor White - Write-Host " -InstallOfficeViaWinget : Install Office using Winget" -ForegroundColor Cyan - Write-Host " -FullCycle : Full cycle test (Install -> Detect -> Remove)" -ForegroundColor Magenta - - Write-Host "`nUsage Examples:" -ForegroundColor Yellow - Write-Host " .\Test-OfficeScrubC2R.ps1 -TestDetection" -ForegroundColor White - Write-Host " .\Test-OfficeScrubC2R.ps1 -TestAll" -ForegroundColor White - Write-Host " .\Test-OfficeScrubC2R.ps1 -TestNativeCode -Verbose" -ForegroundColor White - Write-Host " .\Test-OfficeScrubC2R.ps1 -InstallOfficeViaWinget" -ForegroundColor Cyan - Write-Host " .\Test-OfficeScrubC2R.ps1 -FullCycle" -ForegroundColor Magenta - - Write-Host "`nNotes:" -ForegroundColor Yellow - Write-Host " - Most tests require Administrator privileges" -ForegroundColor White - Write-Host " - Office installation via Winget requires internet connectivity" -ForegroundColor White - Write-Host " - Full cycle test is destructive and should be used with caution" -ForegroundColor White -} - -# Main test execution -Write-Host "`n" + "="*70 -ForegroundColor Green -Write-Host "OFFICE SCRUB C2R - TEST SUITE" -ForegroundColor Green -Write-Host "="*70 -ForegroundColor Green - -# Check for admin privileges -$isAdmin = Test-IsElevated -if (-not $isAdmin) { - Write-Host "`nWARNING: Not running as Administrator!" -ForegroundColor Yellow - Write-Host "Some tests may fail without elevated privileges.`n" -ForegroundColor Yellow -} - -# Handle special modes first -if ($InstallOfficeViaWinget) { - Install-OfficeViaWinget - exit -} - -if ($FullCycle) { - Test-FullCycle - exit -} - -# Run standard tests -if ($TestAll -or $TestDetection) { - Test-OfficeDetection - Test-OfficeProductDetection - Test-SystemInfo - Test-GUIDOperations - Test-ErrorHandling - Test-Logging -} - -if ($TestAll -or $TestRegistry) { - Test-RegistryOperations -} - -if ($TestAll -or $TestFileOperations) { - Test-FileOperations - Test-ProcessManagement -} - -if ($TestAll -or $TestNativeCode) { - Test-NativeCode -} - -if ($TestAll -or $TestIntegration) { - Test-Integration -} - -if (-not ($TestAll -or $TestDetection -or $TestRegistry -or $TestFileOperations -or $TestNativeCode -or $TestIntegration)) { - Show-TestSummary -} -else { - Write-Host "`n" + "="*70 -ForegroundColor Green - Write-Host "All tests completed!" -ForegroundColor Green - Write-Host "="*70 -ForegroundColor Green -} diff --git a/tests/Verify-NativeBuild.ps1 b/tests/Verify-NativeBuild.ps1 new file mode 100644 index 0000000..ea15fde --- /dev/null +++ b/tests/Verify-NativeBuild.ps1 @@ -0,0 +1,142 @@ +[CmdletBinding()] +param( + [switch]$Clean, + [string]$NetCoreFramework = 'net7.0-windows' +) + +$ErrorActionPreference = 'Stop' +$root = Split-Path -Parent $MyInvocation.MyCommand.Path +$sourceFile = Join-Path $root 'OfficeScrubC2R-Native.cs' +$artifactRoot = Join-Path $root 'artifacts' + +if ($Clean -and (Test-Path $artifactRoot)) { + Write-Verbose 'Cleaning previous build artifacts.' + Remove-Item $artifactRoot -Recurse -Force +} + +if (-not (Test-Path $sourceFile)) { + throw "Source file not found: $sourceFile" +} + +function Invoke-NetFrameworkBuild { + param( + [string]$Source, + [string]$OutputFolder + ) + + $frameworkCandidates = @( + Join-Path $env:WINDIR 'Microsoft.NET\\Framework64\\v4.0.30319\\csc.exe'), + Join-Path $env:WINDIR 'Microsoft.NET\\Framework\\v4.0.30319\\csc.exe' + ) | Where-Object { $_ -and (Test-Path $_) } + + if (-not $frameworkCandidates) { + Write-Warning '.NET Framework compiler (csc.exe) not found. Skipping Desktop build check.' + return $null + } + + $compiler = $frameworkCandidates | Select-Object -First 1 + $compilerDir = Split-Path $compiler -Parent + $references = @( + Join-Path $compilerDir 'System.Management.dll', + Join-Path $compilerDir 'Microsoft.CSharp.dll' + ) | Where-Object { Test-Path $_ } + + if (-not $references) { + Write-Warning 'Required reference assemblies missing for .NET Framework build.' + return $null + } + + New-Item -ItemType Directory -Force -Path $OutputFolder | Out-Null + $outputPath = Join-Path $OutputFolder 'OfficeScrubNative.dll' + + $arguments = @( + '/target:library', + '/out:' + $outputPath, + '/optimize+', + '/unsafe', + '/warn:4' + ) + + $arguments += $references | ForEach-Object { '/reference:' + $_ } + $arguments += $Source + + Write-Host "Building .NET Framework assembly with $compiler" -ForegroundColor Cyan + $process = Start-Process -FilePath $compiler -ArgumentList $arguments -NoNewWindow -Wait -PassThru + + if ($process.ExitCode -ne 0) { + throw ".NET Framework build failed with exit code $($process.ExitCode)." + } + + return $outputPath +} + +function Invoke-NetCoreBuild { + param( + [string]$Source, + [string]$OutputFolder, + [string]$Framework + ) + + $dotnet = Get-Command dotnet -ErrorAction SilentlyContinue + if (-not $dotnet) { + Write-Warning '.NET SDK (dotnet) not found. Skipping Core build check.' + return $null + } + + New-Item -ItemType Directory -Force -Path $OutputFolder | Out-Null + $projectPath = Join-Path $OutputFolder 'OfficeScrubNative.Core.csproj' + + @" + + + $Framework + Library + disable + disable + true + OfficeScrubNative + OfficeScrubNative.Core + AnyCPU + + + + + +"@ | Set-Content -Path $projectPath -Encoding UTF8 + + Write-Host "Building .NET SDK assembly targeting $Framework" -ForegroundColor Cyan + dotnet build $projectPath -nologo -clp:NoSummary -p:GenerateAssemblyInfo=false | Write-Host + + $outputPath = Join-Path $OutputFolder "bin\\Debug\\$Framework\\OfficeScrubNative.Core.dll" + if (-not (Test-Path $outputPath)) { + throw "Expected build output not found: $outputPath" + } + + return $outputPath +} + +function Test-AssemblyLoad { + param( + [string]$AssemblyPath + ) + + if (-not $AssemblyPath) { + return + } + + Write-Host "Validating load: $AssemblyPath" -ForegroundColor Green + [System.Reflection.Assembly]::LoadFrom($AssemblyPath) | Out-Null + [OfficeScrubNative.OfficeScrubOrchestrator]::new($true) | Out-Null +} + +$netfxOutput = Invoke-NetFrameworkBuild -Source $sourceFile -OutputFolder (Join-Path $artifactRoot 'netfx') +$netcoreOutput = Invoke-NetCoreBuild -Source $sourceFile -OutputFolder (Join-Path $artifactRoot 'netcore') -Framework $NetCoreFramework + +if ($PSEdition -eq 'Desktop') { + Test-AssemblyLoad -AssemblyPath $netfxOutput +} +else { + Test-AssemblyLoad -AssemblyPath $netcoreOutput +} + +Write-Host 'Verification complete.' -ForegroundColor Green diff --git a/tests/shit.test.ps1 b/tests/shit.test.ps1 deleted file mode 100644 index 1afb197..0000000 --- a/tests/shit.test.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -$isInstalled? = gcim win32_product | where-object { $_.Name -like "*Office*" } -if ($isInstalled?) { write-host "Office is installed" } else { write-host "Office is not installed"} -