v1.16 — Remove PowerShell edition, restore flat project structure
PowerShell version removed — will use OV code signing certificate instead. Files moved back from src/ to project root. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
185
Services/LicenseManager.cs
Normal file
185
Services/LicenseManager.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace InstaSoftOfficeTool.Services
|
||||
{
|
||||
public class LicenseEntry
|
||||
{
|
||||
public string LicenseName { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public string Status { get; set; } = "";
|
||||
public string ErrorDescription { get; set; } = "";
|
||||
public string Last5 { get; set; } = "";
|
||||
}
|
||||
|
||||
public class LicenseManager
|
||||
{
|
||||
public string OsppPath { get; private set; }
|
||||
|
||||
public bool FindOspp()
|
||||
{
|
||||
var searchPaths = new[]
|
||||
{
|
||||
// Click-to-Run (leggyakoribb — Office 365, 2019, 2021, 2024)
|
||||
@"C:\Program Files\Microsoft Office\root\Office16\OSPP.VBS",
|
||||
@"C:\Program Files (x86)\Microsoft Office\root\Office16\OSPP.VBS",
|
||||
// Hagyományos MSI
|
||||
@"C:\Program Files\Microsoft Office\Office16\ospp.vbs",
|
||||
@"C:\Program Files (x86)\Microsoft Office\Office16\ospp.vbs",
|
||||
@"C:\Program Files\Microsoft Office\Office15\ospp.vbs",
|
||||
@"C:\Program Files (x86)\Microsoft Office\Office15\ospp.vbs",
|
||||
@"C:\Program Files\Microsoft Office\Office14\ospp.vbs",
|
||||
@"C:\Program Files (x86)\Microsoft Office\Office14\ospp.vbs",
|
||||
};
|
||||
|
||||
foreach (var path in searchPaths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
OsppPath = path;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var c2rPath = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(
|
||||
@"SOFTWARE\Microsoft\Office\ClickToRun\Configuration")
|
||||
?.GetValue("InstallationPath") as string;
|
||||
|
||||
if (!string.IsNullOrEmpty(c2rPath))
|
||||
{
|
||||
// C2R: root\Office16 vagy sima Office16
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(c2rPath, "root", "Office16", "OSPP.VBS"),
|
||||
Path.Combine(c2rPath, "Office16", "OSPP.VBS"),
|
||||
};
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
OsppPath = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<string> GetStatusAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(OsppPath))
|
||||
return "Az ospp.vbs nem tal\u00e1lhat\u00f3.";
|
||||
|
||||
var runner = new ProcessRunner();
|
||||
return await runner.RunAndCaptureAsync("cscript",
|
||||
"//Nologo \"" + OsppPath + "\" /dstatus");
|
||||
}
|
||||
|
||||
public List<string> ParseLicenseKeys(string dstatusOutput)
|
||||
{
|
||||
var keys = new List<string>();
|
||||
var regex = new Regex(@"Last 5 characters of installed product key:\s*(\S+)",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
foreach (Match match in regex.Matches(dstatusOutput))
|
||||
{
|
||||
keys.Add(match.Groups[1].Value);
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
public List<LicenseEntry> ParseLicenseEntries(string dstatusOutput)
|
||||
{
|
||||
var entries = new List<LicenseEntry>();
|
||||
|
||||
// Split by "-------" separator blocks
|
||||
var blocks = Regex.Split(dstatusOutput, @"-{10,}");
|
||||
|
||||
foreach (var block in blocks)
|
||||
{
|
||||
var trimmed = block.Trim();
|
||||
if (string.IsNullOrEmpty(trimmed)) continue;
|
||||
if (trimmed.StartsWith("---Processing") || trimmed.StartsWith("---Exiting")) continue;
|
||||
|
||||
var entry = new LicenseEntry();
|
||||
|
||||
var nameMatch = Regex.Match(trimmed, @"LICENSE NAME:\s*(.+)", RegexOptions.IgnoreCase);
|
||||
if (nameMatch.Success) entry.LicenseName = nameMatch.Groups[1].Value.Trim();
|
||||
|
||||
var descMatch = Regex.Match(trimmed, @"LICENSE DESCRIPTION:\s*(.+)", RegexOptions.IgnoreCase);
|
||||
if (descMatch.Success) entry.Description = descMatch.Groups[1].Value.Trim();
|
||||
|
||||
var statusMatch = Regex.Match(trimmed, @"LICENSE STATUS:\s*(.+)", RegexOptions.IgnoreCase);
|
||||
if (statusMatch.Success) entry.Status = statusMatch.Groups[1].Value.Trim();
|
||||
|
||||
var errorMatch = Regex.Match(trimmed, @"ERROR DESCRIPTION:\s*(.+)", RegexOptions.IgnoreCase);
|
||||
if (errorMatch.Success) entry.ErrorDescription = errorMatch.Groups[1].Value.Trim();
|
||||
|
||||
var keyMatch = Regex.Match(trimmed, @"Last 5 characters of installed product key:\s*(\S+)", RegexOptions.IgnoreCase);
|
||||
if (keyMatch.Success) entry.Last5 = keyMatch.Groups[1].Value.Trim();
|
||||
|
||||
if (!string.IsNullOrEmpty(entry.Last5))
|
||||
entries.Add(entry);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public async Task<string> RemoveKeyAsync(string last5Chars)
|
||||
{
|
||||
if (string.IsNullOrEmpty(OsppPath))
|
||||
return "Az ospp.vbs nem tal\u00e1lhat\u00f3.";
|
||||
|
||||
var runner = new ProcessRunner();
|
||||
return await runner.RunAndCaptureAsync("cscript",
|
||||
"//Nologo \"" + OsppPath + "\" /unpkey:" + last5Chars);
|
||||
}
|
||||
|
||||
public async Task<string> RemoveAllKeysAsync()
|
||||
{
|
||||
var status = await GetStatusAsync();
|
||||
var keys = ParseLicenseKeys(status);
|
||||
|
||||
if (keys.Count == 0)
|
||||
return "Nem tal\u00e1lhat\u00f3 telep\u00edtett term\u00e9kkulcs.";
|
||||
|
||||
var results = new List<string>();
|
||||
foreach (var key in keys)
|
||||
{
|
||||
var result = await RemoveKeyAsync(key);
|
||||
results.Add(key + ": " + result.Trim());
|
||||
}
|
||||
|
||||
return string.Join("\n", results);
|
||||
}
|
||||
|
||||
public async Task<string> InstallKeyAsync(string productKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(OsppPath))
|
||||
return "Az ospp.vbs nem tal\u00e1lhat\u00f3.";
|
||||
|
||||
var runner = new ProcessRunner();
|
||||
return await runner.RunAndCaptureAsync("cscript",
|
||||
"//Nologo \"" + OsppPath + "\" /inpkey:" + productKey);
|
||||
}
|
||||
|
||||
public async Task<string> ActivateAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(OsppPath))
|
||||
return "Az ospp.vbs nem tal\u00e1lhat\u00f3.";
|
||||
|
||||
var runner = new ProcessRunner();
|
||||
return await runner.RunAndCaptureAsync("cscript",
|
||||
"//Nologo \"" + OsppPath + "\" /act");
|
||||
}
|
||||
}
|
||||
}
|
||||
113
Services/OdtDownloader.cs
Normal file
113
Services/OdtDownloader.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace InstaSoftOfficeTool.Services
|
||||
{
|
||||
public class OdtDownloader
|
||||
{
|
||||
// Direct CDN link to ODT setup.exe (no self-extractor needed)
|
||||
private const string OdtDownloadUrl = "https://officecdn.microsoft.com/pr/wsus/setup.exe";
|
||||
|
||||
public event Action<string> StatusChanged;
|
||||
|
||||
public string OdtFolder { get; private set; }
|
||||
public string SetupExePath { get; private set; }
|
||||
|
||||
public OdtDownloader()
|
||||
{
|
||||
OdtFolder = Path.Combine(Path.GetTempPath(), "InstaSoftODT");
|
||||
}
|
||||
|
||||
public async Task<bool> DownloadAndExtractAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
SetupExePath = Path.Combine(OdtFolder, "setup.exe");
|
||||
|
||||
// Check if valid setup.exe already exists
|
||||
if (File.Exists(SetupExePath))
|
||||
{
|
||||
var existingSize = new FileInfo(SetupExePath).Length;
|
||||
if (existingSize > 1000000) // valid setup.exe is ~7MB
|
||||
{
|
||||
StatusChanged?.Invoke("Az ODT setup.exe m\u00e1r el\u00e9rhet\u0151 (" +
|
||||
(existingSize / 1024 / 1024) + " MB).");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Delete corrupted file
|
||||
StatusChanged?.Invoke("S\u00e9r\u00fclt setup.exe t\u00f6rl\u00e9se...");
|
||||
File.Delete(SetupExePath);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(OdtFolder);
|
||||
|
||||
// Download setup.exe directly from CDN
|
||||
StatusChanged?.Invoke("Office Deployment Tool let\u00f6lt\u00e9se...");
|
||||
StatusChanged?.Invoke("URL: " + OdtDownloadUrl);
|
||||
|
||||
using (var handler = new HttpClientHandler { AllowAutoRedirect = true })
|
||||
using (var client = new HttpClient(handler))
|
||||
{
|
||||
client.DefaultRequestHeaders.Add("User-Agent", "InstaSoftOfficeTool/1.0");
|
||||
client.Timeout = TimeSpan.FromMinutes(5);
|
||||
|
||||
var response = await client.GetAsync(OdtDownloadUrl);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
StatusChanged?.Invoke("Hiba: HTTP " + (int)response.StatusCode + " " + response.StatusCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
var contentType = response.Content.Headers.ContentType?.MediaType ?? "";
|
||||
if (contentType.Contains("text/html"))
|
||||
{
|
||||
StatusChanged?.Invoke("Hiba: HTML oldal \u00e9rkezett exe helyett.");
|
||||
StatusChanged?.Invoke("T\u00f6ltse le manu\u00e1lisan: https://www.microsoft.com/en-us/download/details.aspx?id=49117");
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||
File.WriteAllBytes(SetupExePath, bytes);
|
||||
|
||||
StatusChanged?.Invoke("Let\u00f6ltve: " + (bytes.Length / 1024) + " KB");
|
||||
}
|
||||
|
||||
if (!File.Exists(SetupExePath) || new FileInfo(SetupExePath).Length < 1000000)
|
||||
{
|
||||
StatusChanged?.Invoke("Hiba: A let\u00f6lt\u00f6tt f\u00e1jl s\u00e9r\u00fclt vagy t\u00fal kicsi.");
|
||||
return false;
|
||||
}
|
||||
|
||||
StatusChanged?.Invoke("ODT setup.exe k\u00e9sz.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusChanged?.Invoke("Hiba: " + ex.GetType().Name + ": " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> RunSetupAsync(string configXmlPath, Action<string> outputCallback)
|
||||
{
|
||||
var runner = new ProcessRunner();
|
||||
runner.OutputReceived += line => outputCallback?.Invoke(line);
|
||||
|
||||
StatusChanged?.Invoke("Office telep\u00edt\u00e9s ind\u00edt\u00e1sa...");
|
||||
return await runner.RunAsync(SetupExePath, "/configure \"" + configXmlPath + "\"");
|
||||
}
|
||||
|
||||
public async Task<int> RunRemoveAsync(string configXmlPath, Action<string> outputCallback)
|
||||
{
|
||||
var runner = new ProcessRunner();
|
||||
runner.OutputReceived += line => outputCallback?.Invoke(line);
|
||||
|
||||
StatusChanged?.Invoke("Office elt\u00e1vol\u00edt\u00e1s ind\u00edt\u00e1sa...");
|
||||
return await runner.RunAsync(SetupExePath, "/configure \"" + configXmlPath + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
79
Services/OdtXmlGenerator.cs
Normal file
79
Services/OdtXmlGenerator.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System.Xml.Linq;
|
||||
using InstaSoftOfficeTool.Models;
|
||||
|
||||
namespace InstaSoftOfficeTool.Services
|
||||
{
|
||||
public static class OdtXmlGenerator
|
||||
{
|
||||
public static string Generate(InstallConfig config)
|
||||
{
|
||||
var product = new XElement("Product",
|
||||
new XAttribute("ID", config.Edition.ProductId));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(config.ProductKey))
|
||||
{
|
||||
product.Add(new XAttribute("PIDKEY", config.ProductKey.Replace("-", "").Trim()));
|
||||
}
|
||||
|
||||
product.Add(new XElement("Language", new XAttribute("ID", config.Language)));
|
||||
|
||||
foreach (var app in config.ExcludedApps)
|
||||
{
|
||||
product.Add(new XElement("ExcludeApp", new XAttribute("ID", app)));
|
||||
}
|
||||
|
||||
var add = new XElement("Add",
|
||||
new XAttribute("OfficeClientEdition", config.Architecture),
|
||||
new XAttribute("Channel", config.Edition.Channel),
|
||||
product);
|
||||
|
||||
var configuration = new XElement("Configuration",
|
||||
add,
|
||||
new XElement("Display",
|
||||
new XAttribute("Level", "Full"),
|
||||
new XAttribute("AcceptEULA", "TRUE")),
|
||||
new XElement("Property",
|
||||
new XAttribute("Name", "FORCEAPPSHUTDOWN"),
|
||||
new XAttribute("Value", "TRUE")));
|
||||
|
||||
var doc = new XDocument(new XDeclaration("1.0", "utf-8", null), configuration);
|
||||
return doc.Declaration + "\n" + doc.Root;
|
||||
}
|
||||
|
||||
public static string GenerateRemoveAll()
|
||||
{
|
||||
var configuration = new XElement("Configuration",
|
||||
new XElement("Remove", new XAttribute("All", "TRUE")),
|
||||
new XElement("Display",
|
||||
new XAttribute("Level", "Full"),
|
||||
new XAttribute("AcceptEULA", "TRUE")),
|
||||
new XElement("Property",
|
||||
new XAttribute("Name", "FORCEAPPSHUTDOWN"),
|
||||
new XAttribute("Value", "TRUE")));
|
||||
|
||||
var doc = new XDocument(new XDeclaration("1.0", "utf-8", null), configuration);
|
||||
return doc.Declaration + "\n" + doc.Root;
|
||||
}
|
||||
|
||||
public static string GenerateRemoveProducts(string[] productIds)
|
||||
{
|
||||
var remove = new XElement("Remove");
|
||||
foreach (var id in productIds)
|
||||
{
|
||||
remove.Add(new XElement("Product", new XAttribute("ID", id)));
|
||||
}
|
||||
|
||||
var configuration = new XElement("Configuration",
|
||||
remove,
|
||||
new XElement("Display",
|
||||
new XAttribute("Level", "Full"),
|
||||
new XAttribute("AcceptEULA", "TRUE")),
|
||||
new XElement("Property",
|
||||
new XAttribute("Name", "FORCEAPPSHUTDOWN"),
|
||||
new XAttribute("Value", "TRUE")));
|
||||
|
||||
var doc = new XDocument(new XDeclaration("1.0", "utf-8", null), configuration);
|
||||
return doc.Declaration + "\n" + doc.Root;
|
||||
}
|
||||
}
|
||||
}
|
||||
116
Services/OfficeDetector.cs
Normal file
116
Services/OfficeDetector.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using InstaSoftOfficeTool.Models;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace InstaSoftOfficeTool.Services
|
||||
{
|
||||
public static class OfficeDetector
|
||||
{
|
||||
public static List<InstalledOffice> Detect()
|
||||
{
|
||||
var results = new List<InstalledOffice>();
|
||||
|
||||
// Check Click-to-Run
|
||||
DetectClickToRun(results);
|
||||
|
||||
// Check MSI-based installs
|
||||
DetectMsi(results);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static void DetectClickToRun(List<InstalledOffice> results)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var key = Registry.LocalMachine.OpenSubKey(
|
||||
@"SOFTWARE\Microsoft\Office\ClickToRun\Configuration"))
|
||||
{
|
||||
if (key == null) return;
|
||||
|
||||
var productIds = key.GetValue("ProductReleaseIds") as string;
|
||||
var versionToReport = key.GetValue("VersionToReport") as string;
|
||||
|
||||
if (string.IsNullOrEmpty(productIds)) return;
|
||||
|
||||
// Split into individual products (e.g. "O365BusinessRetail,VisioProRetail")
|
||||
var ids = productIds.Split(',');
|
||||
foreach (var id in ids)
|
||||
{
|
||||
var trimmedId = id.Trim();
|
||||
if (string.IsNullOrEmpty(trimmedId)) continue;
|
||||
|
||||
results.Add(new InstalledOffice
|
||||
{
|
||||
DisplayName = "Microsoft Office Click-to-Run (" + trimmedId + ")",
|
||||
Version = versionToReport ?? "",
|
||||
ProductCode = trimmedId,
|
||||
IsClickToRun = true,
|
||||
IsSelected = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static void DetectMsi(List<InstalledOffice> results)
|
||||
{
|
||||
var uninstallPaths = new[]
|
||||
{
|
||||
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
|
||||
@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
|
||||
};
|
||||
|
||||
foreach (var path in uninstallPaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var key = Registry.LocalMachine.OpenSubKey(path))
|
||||
{
|
||||
if (key == null) continue;
|
||||
|
||||
foreach (var subKeyName in key.GetSubKeyNames())
|
||||
{
|
||||
using (var subKey = key.OpenSubKey(subKeyName))
|
||||
{
|
||||
if (subKey == null) continue;
|
||||
|
||||
var displayName = subKey.GetValue("DisplayName") as string;
|
||||
var publisher = subKey.GetValue("Publisher") as string;
|
||||
|
||||
if (displayName != null &&
|
||||
publisher != null &&
|
||||
publisher.Contains("Microsoft") &&
|
||||
(displayName.Contains("Microsoft Office") ||
|
||||
displayName.Contains("Microsoft 365")))
|
||||
{
|
||||
var version = subKey.GetValue("DisplayVersion") as string ?? "";
|
||||
|
||||
// Skip Click-to-Run updater entries
|
||||
if (displayName.Contains("Click-to-Run") &&
|
||||
!displayName.Contains("Microsoft Office"))
|
||||
continue;
|
||||
|
||||
// Only accept {GUID} product codes for MSI uninstall
|
||||
bool isGuid = Regex.IsMatch(subKeyName, @"^\{[0-9A-Fa-f\-]+\}$");
|
||||
|
||||
results.Add(new InstalledOffice
|
||||
{
|
||||
DisplayName = displayName,
|
||||
Version = version,
|
||||
ProductCode = isGuid ? subKeyName : null,
|
||||
IsClickToRun = false,
|
||||
IsSelected = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
Services/ProcessRunner.cs
Normal file
63
Services/ProcessRunner.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace InstaSoftOfficeTool.Services
|
||||
{
|
||||
public class ProcessRunner
|
||||
{
|
||||
public event Action<string> OutputReceived;
|
||||
|
||||
public async Task<int> RunAsync(string fileName, string arguments, bool redirectOutput = true)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = redirectOutput,
|
||||
RedirectStandardError = redirectOutput,
|
||||
StandardOutputEncoding = redirectOutput ? Encoding.UTF8 : null,
|
||||
StandardErrorEncoding = redirectOutput ? Encoding.UTF8 : null
|
||||
};
|
||||
|
||||
using (var process = new Process { StartInfo = psi })
|
||||
{
|
||||
if (redirectOutput)
|
||||
{
|
||||
process.OutputDataReceived += (s, e) =>
|
||||
{
|
||||
if (e.Data != null)
|
||||
OutputReceived?.Invoke(e.Data);
|
||||
};
|
||||
process.ErrorDataReceived += (s, e) =>
|
||||
{
|
||||
if (e.Data != null)
|
||||
OutputReceived?.Invoke("[HIBA] " + e.Data);
|
||||
};
|
||||
}
|
||||
|
||||
process.Start();
|
||||
|
||||
if (redirectOutput)
|
||||
{
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
}
|
||||
|
||||
await Task.Run(() => process.WaitForExit());
|
||||
return process.ExitCode;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> RunAndCaptureAsync(string fileName, string arguments)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
OutputReceived += line => sb.AppendLine(line);
|
||||
await RunAsync(fileName, arguments);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user