/*
* Copyright (c) 2023 Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonVPN. If not, see .
*/
using System.Text;
using Microsoft.Win32;
using ProtonVPN.Common.Core.Extensions;
using ProtonVPN.Configurations.Contracts;
namespace ProtonVPN.Client.Logic.Feedback.Diagnostics.Logs;
public class InstalledAppsLog : LogBase
{
private const string UNINSTALL_REGISTRY_PATH = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
private const string WOW_UNINSTALL_REGISTRY_PATH = @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall";
private const string WINDOWS_APPS_REGISTRY_PATH = @"SOFTWARE\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages";
private const string PROGRAM_TAG = "[Program]";
private const string WINDOWS_APP_TAG = "[WindowsApp]";
public InstalledAppsLog(IStaticConfiguration configuration)
: base(configuration.DiagnosticLogsFolder, "Apps.txt")
{ }
public override void Write()
{
File.WriteAllText(Path, GenerateContent());
}
private string GenerateContent()
{
List appNames = GetAppNames();
StringBuilder result = new();
appNames.Distinct()
.OrderBy(appName => appName)
.ForEach((appName) => result.AppendLine(appName));
return result.ToString();
}
private List GetAppNames()
{
List appNames = new();
using (RegistryKey? registryFolder = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64).OpenSubKey(UNINSTALL_REGISTRY_PATH))
{
appNames.AddRange(GetInstalledNames(registryFolder, ParseProgramNameIfExists));
}
using (RegistryKey? registryFolder = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(UNINSTALL_REGISTRY_PATH))
{
appNames.AddRange(GetInstalledNames(registryFolder, ParseProgramNameIfExists));
}
using (RegistryKey? registryFolder = Registry.LocalMachine.OpenSubKey(WOW_UNINSTALL_REGISTRY_PATH))
{
appNames.AddRange(GetInstalledNames(registryFolder, ParseProgramNameIfExists));
}
using (RegistryKey? registryFolder = Registry.CurrentUser.OpenSubKey(UNINSTALL_REGISTRY_PATH))
{
appNames.AddRange(GetInstalledNames(registryFolder, ParseProgramNameIfExists));
}
using (RegistryKey? registryFolder = Registry.CurrentUser.OpenSubKey(WINDOWS_APPS_REGISTRY_PATH))
{
appNames.AddRange(GetInstalledNames(registryFolder, ParseWindowsAppNameIfExists));
}
return appNames;
}
private IList GetInstalledNames(RegistryKey? registryFolder, Func installedNameParser)
{
IList names = new List();
if (registryFolder == null)
{
return names;
}
foreach (string registryFolderChild in registryFolder.GetSubKeyNames())
{
using (RegistryKey? subKey = registryFolder.OpenSubKey(registryFolderChild))
{
if (subKey != null)
{
string name = installedNameParser(subKey);
if (name != null)
{
names.Add(name);
}
}
}
}
return names;
}
private string ParseProgramNameIfExists(RegistryKey key)
{
string displayName = $"{key.GetValue("DisplayName")}";
if (string.IsNullOrEmpty(displayName))
{
return string.Empty;
}
return displayName + ParseProgramVersion(key) + $" {PROGRAM_TAG}";
}
private string ParseProgramVersion(RegistryKey key)
{
string displayVersion = $"{key.GetValue("DisplayVersion")}";
if (string.IsNullOrEmpty(displayVersion))
{
return string.Empty;
}
return $" [Version {displayVersion}]";
}
private string ParseWindowsAppNameIfExists(RegistryKey key)
{
string displayName = $"{key.GetValue("DisplayName")}";
string packageId = $"{key.GetValue("PackageID")}";
if (string.IsNullOrEmpty(displayName) && string.IsNullOrEmpty(packageId))
{
return string.Empty;
}
return ParseWindowsAppName(key, displayName, packageId) + $" {WINDOWS_APP_TAG}";
}
private string ParseWindowsAppName(RegistryKey key, string displayName, string packageId)
{
if (string.IsNullOrEmpty(packageId))
{
return $"{displayName} [Only DisplayName available]";
}
if (string.IsNullOrEmpty(displayName))
{
return $"{packageId} [Only PackageID available]";
}
return ParseWindowsAppNameWithCompleteData(key, displayName, packageId);
}
private string ParseWindowsAppNameWithCompleteData(RegistryKey key, string displayName, string packageId)
{
string packageRootFolder = $"{key.GetValue("PackageRootFolder")}";
string packageRootFolderTag = string.IsNullOrEmpty(packageRootFolder) ? string.Empty : $" [PackageRootFolder {packageRootFolder}]";
if (displayName.StartsWith("@{"))
{
try
{
string name = GetCleanWindowsAppName(displayName: displayName, packageRootFolder: packageRootFolder);
return $"{name} [DisplayName {displayName}]{packageRootFolderTag}";
}
catch
{
}
}
return $"{displayName} [PackageID {packageId}]{packageRootFolderTag}";
}
private string GetCleanWindowsAppName(string displayName, string packageRootFolder)
{
string name = GetCleanMicrosoftResourceDisplayName(displayName);
bool isValidGuid = Guid.TryParse(name, out _);
if (isValidGuid && !string.IsNullOrEmpty(packageRootFolder))
{
name = System.IO.Path.GetFileName(packageRootFolder);
}
return name;
}
private string GetCleanMicrosoftResourceDisplayName(string displayName)
{
const string msResourceString = "ms-resource://";
int nameStartIndex = displayName.IndexOf(msResourceString, StringComparison.InvariantCultureIgnoreCase) + msResourceString.Length;
string name = displayName.Substring(nameStartIndex);
int nameEndIndex = name.IndexOf("/", StringComparison.InvariantCultureIgnoreCase);
return name.Substring(0, nameEndIndex);
}
}