src/OfficeScrubC2R.Core/OfficeDetectionService.cs

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Win32;
 
namespace OfficeScrubC2R
{
    public sealed class OfficeDetectionService
    {
        private static readonly string[] ConfigurationKeys =
        {
            @"SOFTWARE\Microsoft\Office\15.0\ClickToRun\Configuration",
            @"SOFTWARE\Microsoft\Office\16.0\ClickToRun\Configuration",
            @"SOFTWARE\Microsoft\Office\ClickToRun\Configuration"
        };
 
        private readonly RegistryAccess _registry;
 
        public OfficeDetectionService()
            : this(new RegistryAccess(Environment.Is64BitOperatingSystem))
        {
        }
 
        public OfficeDetectionService(RegistryAccess registry)
        {
            _registry = registry;
        }
 
        public IReadOnlyList<OfficeProductInfo> GetInstalledProducts()
        {
            var products = new List<OfficeProductInfo>();
 
            foreach (var view in _registry.GetCandidateViews())
            {
                ReadConfigurationProducts(products, view);
                ReadArpProducts(products, view);
            }
 
            return products
                .GroupBy(product => string.Join("|", product.ProductId, product.DisplayName, product.RegistryPath, product.RegistryView))
                .Select(group => group.First())
                .OrderBy(product => product.DisplayName, StringComparer.OrdinalIgnoreCase)
                .ThenBy(product => product.ProductId, StringComparer.OrdinalIgnoreCase)
                .ToArray();
        }
 
        public IReadOnlyList<string> GetPackagePaths()
        {
            var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (var view in _registry.GetCandidateViews())
            {
                foreach (var key in ConfigurationKeys)
                {
                    AddIfPresent(paths, _registry.GetStringValue(RegistryHive.LocalMachine, key, "PackageFolder", view));
                    AddIfPresent(paths, _registry.GetStringValue(RegistryHive.LocalMachine, key, "ClientFolder", view));
                    AddIfPresent(paths, _registry.GetStringValue(RegistryHive.LocalMachine, key, "InstallationPath", view));
                }
            }
 
            return paths.OrderBy(path => path, StringComparer.OrdinalIgnoreCase).ToArray();
        }
 
        private void ReadConfigurationProducts(List<OfficeProductInfo> products, RegistryView view)
        {
            foreach (var key in ConfigurationKeys)
            {
                var releaseIds = _registry.GetStringValue(RegistryHive.LocalMachine, key, "ProductReleaseIds", view);
                if (string.IsNullOrWhiteSpace(releaseIds))
                {
                    releaseIds = _registry.GetStringValue(RegistryHive.LocalMachine, key, "ProductReleaseIDs", view);
                }
 
                if (string.IsNullOrWhiteSpace(releaseIds))
                {
                    continue;
                }
 
                var version = FirstNonEmpty(
                    _registry.GetStringValue(RegistryHive.LocalMachine, key, "ClientVersionToReport", view),
                    _registry.GetStringValue(RegistryHive.LocalMachine, key, "VersionToReport", view),
                    _registry.GetStringValue(RegistryHive.LocalMachine, key, "Version", view));
 
                foreach (var releaseId in SplitReleaseIds(releaseIds))
                {
                    products.Add(new OfficeProductInfo
                    {
                        ProductId = releaseId,
                        DisplayName = releaseId,
                        DisplayVersion = version,
                        Architecture = view == RegistryView.Registry32 ? "x86" : "x64",
                        Source = "ClickToRunConfiguration",
                        RegistryHive = RegistryHive.LocalMachine,
                        RegistryView = view,
                        RegistryPath = key,
                        IsClickToRun = true
                    });
                }
            }
        }
 
        private void ReadArpProducts(List<OfficeProductInfo> products, RegistryView view)
        {
            const string uninstallRoot = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
            foreach (var subKeyName in _registry.GetSubKeyNames(RegistryHive.LocalMachine, uninstallRoot, view))
            {
                var path = uninstallRoot + "\\" + subKeyName;
                var displayName = _registry.GetStringValue(RegistryHive.LocalMachine, path, "DisplayName", view);
                var uninstallString = _registry.GetStringValue(RegistryHive.LocalMachine, path, "UninstallString", view);
                var displayVersion = _registry.GetStringValue(RegistryHive.LocalMachine, path, "DisplayVersion", view);
 
                if (!LooksLikeC2RProduct(displayName, uninstallString))
                {
                    continue;
                }
 
                products.Add(new OfficeProductInfo
                {
                    ProductId = subKeyName,
                    DisplayName = string.IsNullOrWhiteSpace(displayName) ? subKeyName : displayName,
                    DisplayVersion = displayVersion,
                    Architecture = view == RegistryView.Registry32 ? "x86" : "x64",
                    Source = "UninstallRegistry",
                    RegistryHive = RegistryHive.LocalMachine,
                    RegistryView = view,
                    RegistryPath = path,
                    UninstallString = uninstallString,
                    IsClickToRun = true
                });
            }
        }
 
        private static bool LooksLikeC2RProduct(string displayName, string uninstallString)
        {
            var combined = string.Concat(displayName ?? string.Empty, " ", uninstallString ?? string.Empty);
            return combined.IndexOf("OfficeClickToRun.exe", StringComparison.OrdinalIgnoreCase) >= 0 ||
                   combined.IndexOf("Microsoft Office 15", StringComparison.OrdinalIgnoreCase) >= 0 ||
                   combined.IndexOf("Microsoft Office 16", StringComparison.OrdinalIgnoreCase) >= 0 ||
                   OfficeScope.IsC2RPath(combined);
        }
 
        private static IEnumerable<string> SplitReleaseIds(string value)
        {
            return value
                .Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
                .Select(item => item.Trim())
                .Where(item => item.Length > 0);
        }
 
        private static string FirstNonEmpty(params string[] values)
        {
            return values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty;
        }
 
        private static void AddIfPresent(HashSet<string> paths, string value)
        {
            if (!string.IsNullOrWhiteSpace(value))
            {
                paths.Add(value);
            }
        }
    }
}