commit 4937ac4b1ced203c231c9c35caee968f182ba442 Author: hariel1985 Date: Tue Mar 31 05:40:50 2026 +0200 Initial release v1.01 — InstaSoft Office Tool Office deployment wizard for InstaSoft customers: - Install Office 2019/2021/2024 (Standard, Professional Plus, Home & Business) - Auto-download ODT from Microsoft, generate config XML, run setup - Remove existing Office installations (C2R + MSI) - License troubleshooting via ospp.vbs (dstatus, unpkey) - Fluent Design UI (WPF .NET Framework 4.8, Win7+ compatible) - Hungarian interface, multi-language Office installation - Product key input with auto-activation support Co-Authored-By: Claude Opus 4.6 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81de725 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +bin/ +obj/ +*.user +*.suo +.vs/ +*.DotSettings.user +packages/ diff --git a/App.xaml b/App.xaml new file mode 100644 index 0000000..44d89bc --- /dev/null +++ b/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/App.xaml.cs b/App.xaml.cs new file mode 100644 index 0000000..86cde79 --- /dev/null +++ b/App.xaml.cs @@ -0,0 +1,8 @@ +using System.Windows; + +namespace InstaSoftOfficeTool +{ + public partial class App : Application + { + } +} diff --git a/InstaSoftOfficeTool.csproj b/InstaSoftOfficeTool.csproj new file mode 100644 index 0000000..9d07aaf --- /dev/null +++ b/InstaSoftOfficeTool.csproj @@ -0,0 +1,26 @@ + + + WinExe + net48 + true + InstaSoftOfficeTool + InstaSoftOfficeTool + Resources\app.ico + InstaSoft Zrt. + InstaSoft Office Tool + Copyright (c) InstaSoft Zrt. 2026 + 1.0.0 + 1.0.0.0 + 1.0.0.0 + app.manifest + latest + + + + + + + + + + diff --git a/MainWindow.xaml b/MainWindow.xaml new file mode 100644 index 0000000..d7ac07c --- /dev/null +++ b/MainWindow.xaml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pages/WelcomePage.xaml.cs b/Pages/WelcomePage.xaml.cs new file mode 100644 index 0000000..2bee20d --- /dev/null +++ b/Pages/WelcomePage.xaml.cs @@ -0,0 +1,31 @@ +using System.Windows; +using System.Windows.Controls; + +namespace InstaSoftOfficeTool.Pages +{ + public partial class WelcomePage : Page + { + private readonly MainWindow _main; + + public WelcomePage(MainWindow main) + { + InitializeComponent(); + _main = main; + } + + private void InstallClick(object sender, RoutedEventArgs e) + { + _main.StartInstallFlow(); + } + + private void RemoveClick(object sender, RoutedEventArgs e) + { + _main.StartRemoveFlow(); + } + + private void LicenseClick(object sender, RoutedEventArgs e) + { + _main.StartLicenseFlow(); + } + } +} diff --git a/Resources/app.ico b/Resources/app.ico new file mode 100644 index 0000000..d5d425d Binary files /dev/null and b/Resources/app.ico differ diff --git a/Services/LicenseManager.cs b/Services/LicenseManager.cs new file mode 100644 index 0000000..e719cff --- /dev/null +++ b/Services/LicenseManager.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace InstaSoftOfficeTool.Services +{ + 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 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 ParseLicenseKeys(string dstatusOutput) + { + var keys = new List(); + 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 async Task 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 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(); + foreach (var key in keys) + { + var result = await RemoveKeyAsync(key); + results.Add(key + ": " + result.Trim()); + } + + return string.Join("\n", results); + } + } +} diff --git a/Services/OdtDownloader.cs b/Services/OdtDownloader.cs new file mode 100644 index 0000000..816337f --- /dev/null +++ b/Services/OdtDownloader.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; +using System.Net; +using System.Threading.Tasks; + +namespace InstaSoftOfficeTool.Services +{ + public class OdtDownloader + { + private const string OdtDownloadUrl = "https://go.microsoft.com/fwlink/p/?LinkID=626065"; + + public event Action StatusChanged; + public event Action ProgressChanged; + + public string OdtFolder { get; private set; } + public string SetupExePath { get; private set; } + + public OdtDownloader() + { + OdtFolder = Path.Combine(Path.GetTempPath(), "InstaSoftODT"); + } + + public async Task DownloadAndExtractAsync() + { + try + { + Directory.CreateDirectory(OdtFolder); + + var odtExePath = Path.Combine(OdtFolder, "officedeploymenttool.exe"); + SetupExePath = Path.Combine(OdtFolder, "setup.exe"); + + if (File.Exists(SetupExePath)) + { + StatusChanged?.Invoke("Az ODT setup.exe m\u00e1r el\u00e9rhet\u0151, let\u00f6lt\u00e9s kihagyva."); + return true; + } + + StatusChanged?.Invoke("Office Deployment Tool let\u00f6lt\u00e9se..."); + + using (var client = new WebClient()) + { + client.DownloadProgressChanged += (s, e) => + { + ProgressChanged?.Invoke(e.ProgressPercentage); + }; + + await client.DownloadFileTaskAsync(new Uri(OdtDownloadUrl), odtExePath); + } + + StatusChanged?.Invoke("ODT kicsomagol\u00e1sa..."); + + var runner = new ProcessRunner(); + var exitCode = await runner.RunAsync(odtExePath, + "/extract:\"" + OdtFolder + "\" /quiet"); + + if (exitCode != 0) + { + StatusChanged?.Invoke("Hiba: Az ODT kicsomagol\u00e1sa sikertelen (k\u00f3d: " + exitCode + ")"); + return false; + } + + if (!File.Exists(SetupExePath)) + { + StatusChanged?.Invoke("Hiba: A setup.exe nem tal\u00e1lhat\u00f3 a kicsomagol\u00e1s ut\u00e1n."); + return false; + } + + StatusChanged?.Invoke("ODT sikeresen let\u00f6ltve \u00e9s kicsomagolva."); + return true; + } + catch (Exception ex) + { + StatusChanged?.Invoke("Hiba a let\u00f6lt\u00e9s sor\u00e1n: " + ex.Message); + return false; + } + } + + public async Task RunSetupAsync(string configXmlPath, Action 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 RunRemoveAsync(string configXmlPath, Action 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 + "\""); + } + } +} diff --git a/Services/OdtXmlGenerator.cs b/Services/OdtXmlGenerator.cs new file mode 100644 index 0000000..5ea332c --- /dev/null +++ b/Services/OdtXmlGenerator.cs @@ -0,0 +1,55 @@ +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"))); + + var doc = new XDocument(new XDeclaration("1.0", "utf-8", null), configuration); + return doc.Declaration + "\n" + doc.Root; + } + } +} diff --git a/Services/OfficeDetector.cs b/Services/OfficeDetector.cs new file mode 100644 index 0000000..8694fe0 --- /dev/null +++ b/Services/OfficeDetector.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using InstaSoftOfficeTool.Models; +using Microsoft.Win32; + +namespace InstaSoftOfficeTool.Services +{ + public static class OfficeDetector + { + public static List Detect() + { + var results = new List(); + + // Check Click-to-Run + DetectClickToRun(results); + + // Check MSI-based installs + DetectMsi(results); + + return results; + } + + private static void DetectClickToRun(List 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)) + { + results.Add(new InstalledOffice + { + DisplayName = "Microsoft Office Click-to-Run (" + productIds + ")", + Version = versionToReport ?? "", + IsClickToRun = true, + IsSelected = true + }); + } + } + } + catch { } + } + + private static void DetectMsi(List 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; + + results.Add(new InstalledOffice + { + DisplayName = displayName, + Version = version, + ProductCode = subKeyName, + IsClickToRun = false, + IsSelected = true + }); + } + } + } + } + } + catch { } + } + } + } +} diff --git a/Services/ProcessRunner.cs b/Services/ProcessRunner.cs new file mode 100644 index 0000000..94f4be2 --- /dev/null +++ b/Services/ProcessRunner.cs @@ -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 OutputReceived; + + public async Task 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 RunAndCaptureAsync(string fileName, string arguments) + { + var sb = new StringBuilder(); + OutputReceived += line => sb.AppendLine(line); + await RunAsync(fileName, arguments); + return sb.ToString(); + } + } +} diff --git a/Styles/ButtonStyles.xaml b/Styles/ButtonStyles.xaml new file mode 100644 index 0000000..6f10a1d --- /dev/null +++ b/Styles/ButtonStyles.xaml @@ -0,0 +1,119 @@ + + + + + + + + + + + + diff --git a/Styles/ControlStyles.xaml b/Styles/ControlStyles.xaml new file mode 100644 index 0000000..34b58d9 --- /dev/null +++ b/Styles/ControlStyles.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Styles/FluentTheme.xaml b/Styles/FluentTheme.xaml new file mode 100644 index 0000000..8735882 --- /dev/null +++ b/Styles/FluentTheme.xaml @@ -0,0 +1,49 @@ + + + + #0078D4 + #106EBE + #005A9E + #F3F3F3 + #FFFFFF + #F8F8F8 + #E0E0E0 + #1A1A1A + #666666 + #FFFFFF + #107C10 + #D13438 + #CA5010 + #FAFAFA + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app.manifest b/app.manifest new file mode 100644 index 0000000..075cff8 --- /dev/null +++ b/app.manifest @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + true/pm + PerMonitorV2 + + +