diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
index 8fa04498df..40ef350d30 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
@@ -1455,6 +1455,26 @@ public static string Change {
}
}
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die Change location? ähnelt.
+ ///
+ public static string ChangeLocationQuestion {
+ get {
+ return ResourceManager.GetString("ChangeLocationQuestion", resourceCulture);
+ }
+ }
+
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die The location is changed and the application is restarted afterwards.
+ ///
+ ///You can copy the “settings.json” file from "{0}" to "{1}" to migrate your previous settings, if necessary. The application must be closed for this to prevent the settings from being overwritten. ähnelt.
+ ///
+ public static string ChangeLocationSettingsMessage {
+ get {
+ return ResourceManager.GetString("ChangeLocationSettingsMessage", resourceCulture);
+ }
+ }
+
///
/// Sucht eine lokalisierte Zeichenfolge, die Changelog ähnelt.
///
@@ -3567,6 +3587,15 @@ public static string EnterValidFilePath {
}
}
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die Enter a valid folder path! ähnelt.
+ ///
+ public static string EnterValidFolderPath {
+ get {
+ return ResourceManager.GetString("EnterValidFolderPath", resourceCulture);
+ }
+ }
+
///
/// Sucht eine lokalisierte Zeichenfolge, die Enter a valid hostname! ähnelt.
///
@@ -8835,6 +8864,44 @@ public static string RestartTheApplicationToChangeTheLanguage {
}
}
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die Restore ähnelt.
+ ///
+ public static string Restore {
+ get {
+ return ResourceManager.GetString("Restore", resourceCulture);
+ }
+ }
+
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die Restore default location ähnelt.
+ ///
+ public static string RestoreDefaultLocation {
+ get {
+ return ResourceManager.GetString("RestoreDefaultLocation", resourceCulture);
+ }
+ }
+
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die Restore default location? ähnelt.
+ ///
+ public static string RestoreDefaultLocationQuestion {
+ get {
+ return ResourceManager.GetString("RestoreDefaultLocationQuestion", resourceCulture);
+ }
+ }
+
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die The default path is restored and the application is restarted afterwards.
+ ///
+ ///You can copy the “settings.json” file from "{0}" to "{1}" to migrate your previous settings, if necessary. The application must be closed for this to prevent the settings from being overwritten. ähnelt.
+ ///
+ public static string RestoreDefaultLocationSettingsMessage {
+ get {
+ return ResourceManager.GetString("RestoreDefaultLocationSettingsMessage", resourceCulture);
+ }
+ }
+
///
/// Sucht eine lokalisierte Zeichenfolge, die Restore defaults ähnelt.
///
@@ -9213,6 +9280,15 @@ public static string SetMasterPasswordDots {
}
}
+ ///
+ /// Sucht eine lokalisierte Zeichenfolge, die This setting is managed by your administrator. ähnelt.
+ ///
+ public static string SettingManagedByAdministrator {
+ get {
+ return ResourceManager.GetString("SettingManagedByAdministrator", resourceCulture);
+ }
+ }
+
///
/// Sucht eine lokalisierte Zeichenfolge, die Settings ähnelt.
///
@@ -9271,15 +9347,6 @@ public static string SettingsHaveBeenReset {
}
}
- ///
- /// Sucht eine lokalisierte Zeichenfolge, die This setting is managed by your administrator. ähnelt.
- ///
- public static string SettingManagedByAdministrator {
- get {
- return ResourceManager.GetString("SettingManagedByAdministrator", resourceCulture);
- }
- }
-
///
/// Sucht eine lokalisierte Zeichenfolge, die Appearance ähnelt.
///
diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx
index e4f0fef96e..ad2eb07c93 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.resx
+++ b/Source/NETworkManager.Localization/Resources/Strings.resx
@@ -3963,4 +3963,29 @@ If you click Cancel, the profile file will remain unencrypted.
This setting is managed by your administrator.
+
+ Restore default location
+
+
+ Restore
+
+
+ Restore default location?
+
+
+ The default path is restored and the application is restarted afterwards.
+
+You can copy the “settings.json” file from "{0}" to "{1}" to migrate your previous settings, if necessary. The application must be closed for this to prevent the settings from being overwritten.
+
+
+ Change location?
+
+
+ The location is changed and the application is restarted afterwards.
+
+You can copy the “settings.json” file from "{0}" to "{1}" to migrate your previous settings, if necessary. The application must be closed for this to prevent the settings from being overwritten.
+
+
+ Enter a valid folder path!
+
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/NETworkManager.Models.csproj b/Source/NETworkManager.Models/NETworkManager.Models.csproj
index 230de2c91a..c913dd5488 100644
--- a/Source/NETworkManager.Models/NETworkManager.Models.csproj
+++ b/Source/NETworkManager.Models/NETworkManager.Models.csproj
@@ -27,7 +27,7 @@
-
+
@@ -36,7 +36,7 @@
-
+
diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs
index d9c0d33891..bbb62fa382 100644
--- a/Source/NETworkManager.Profiles/ProfileManager.cs
+++ b/Source/NETworkManager.Profiles/ProfileManager.cs
@@ -616,10 +616,10 @@ private static void Load(ProfileFileInfo profileFileInfo)
{
var loadedProfileUpdated = false;
- Log.Info($"Load profile file: {profileFileInfo.Path}");
-
if (File.Exists(profileFileInfo.Path))
{
+ Log.Info($"Loading profile file from: {profileFileInfo.Path}");
+
// Encrypted profile file
if (profileFileInfo.IsEncrypted)
{
@@ -734,6 +734,8 @@ private static void Load(ProfileFileInfo profileFileInfo)
// Notify subscribers that profiles have been loaded/updated
ProfilesUpdated(false);
+
+ Log.Info("Profile file loaded successfully.");
}
///
diff --git a/Source/NETworkManager.Settings/LocalSettingsInfo.cs b/Source/NETworkManager.Settings/LocalSettingsInfo.cs
new file mode 100644
index 0000000000..6c7d1a692f
--- /dev/null
+++ b/Source/NETworkManager.Settings/LocalSettingsInfo.cs
@@ -0,0 +1,58 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Text.Json.Serialization;
+
+namespace NETworkManager.Settings;
+
+///
+/// Class contains local settings that are stored outside the main settings file.
+/// These settings control where the main settings file is located.
+///
+public class LocalSettingsInfo
+{
+ ///
+ /// Occurs when a property value changes.
+ ///
+ /// This event is typically used to notify subscribers that a property value has been updated. It
+ /// is commonly implemented in classes that support data binding or need to signal changes to property
+ /// values.
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ ///
+ /// Helper method to raise the event.
+ ///
+ /// Name of the property that changed.
+ private void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ SettingsChanged = true;
+
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ #region Variables
+
+ [JsonIgnore] public bool SettingsChanged { get; set; }
+
+ ///
+ /// Private field for the property."
+ ///
+ private string _settingsFolderLocation;
+
+ ///
+ /// Location of the folder where the local settings file is stored.
+ /// This can be changed by the user to move the settings file to a different location.
+ ///
+ public string SettingsFolderLocation
+ {
+ get => _settingsFolderLocation;
+ set
+ {
+ if (_settingsFolderLocation == value)
+ return;
+
+ _settingsFolderLocation = value;
+ OnPropertyChanged();
+ }
+ }
+ #endregion
+}
diff --git a/Source/NETworkManager.Settings/LocalSettingsManager.cs b/Source/NETworkManager.Settings/LocalSettingsManager.cs
new file mode 100644
index 0000000000..011a1bae1d
--- /dev/null
+++ b/Source/NETworkManager.Settings/LocalSettingsManager.cs
@@ -0,0 +1,126 @@
+using log4net;
+using Microsoft.Xaml.Behaviors.Media;
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace NETworkManager.Settings;
+
+///
+/// Manages local application settings that are stored outside the main settings file.
+/// This is used for settings that control where the main settings file is located.
+///
+public static class LocalSettingsManager
+{
+ #region Variables
+
+ ///
+ /// Logger for logging.
+ ///
+ private static readonly ILog Log = LogManager.GetLogger(typeof(LocalSettingsManager));
+
+ ///
+ /// Settings file name.
+ ///
+ private static string SettingsFileName => "Settings.json";
+
+ ///
+ /// Settings that are currently loaded.
+ ///
+ public static LocalSettingsInfo Current { get; private set; }
+
+ ///
+ /// JSON serializer options for consistent serialization/deserialization.
+ ///
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ WriteIndented = true,
+ PropertyNameCaseInsensitive = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.Never,
+ Converters = { new JsonStringEnumConverter() }
+ };
+ #endregion
+
+ #region Methods
+
+ ///
+ /// Method to get the path of the settings folder.
+ ///
+ /// Path to the settings folder.
+ private static string GetSettingsFolderLocation()
+ {
+ return Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ AssemblyManager.Current.Name);
+ }
+
+ ///
+ /// Method to get the settings file path
+ ///
+ /// Settings file path.
+ private static string GetSettingsFilePath()
+ {
+ return Path.Combine(
+ GetSettingsFolderLocation(),
+ SettingsFileName);
+ }
+
+ ///
+ /// Initialize new settings () and save them (to a file).
+ ///
+ private static void Initialize()
+ {
+ Log.Info("Initializing new local settings.");
+
+ Current = new LocalSettingsInfo();
+
+ Save();
+ }
+
+ ///
+ /// Method to load the settings from a file.
+ ///
+ public static void Load()
+ {
+ var filePath = GetSettingsFilePath();
+
+ if (File.Exists(filePath))
+ {
+ Log.Info($"Loading local settings from: {filePath}");
+
+ var jsonString = File.ReadAllText(filePath);
+ Current = JsonSerializer.Deserialize(jsonString, JsonOptions);
+
+ Log.Info("Local settings loaded successfully.");
+
+ // Reset change tracking
+ Current.SettingsChanged = false;
+
+ return;
+ }
+
+ Initialize();
+ }
+
+ ///
+ /// Method to save the current settings to a file.
+ ///
+ public static void Save()
+ {
+ // Create the directory if it does not exist
+ Directory.CreateDirectory(GetSettingsFolderLocation());
+
+ // Serialize to file
+ var filePath = GetSettingsFilePath();
+
+ var jsonString = JsonSerializer.Serialize(Current, JsonOptions);
+ File.WriteAllText(filePath, jsonString);
+
+ Log.Info($"Local settings saved to {filePath}");
+
+ // Reset change tracking
+ Current.SettingsChanged = false;
+ }
+ #endregion
+}
diff --git a/Source/NETworkManager.Settings/PolicyInfo.cs b/Source/NETworkManager.Settings/PolicyInfo.cs
index 7b9b64a6a0..2f17ee8abf 100644
--- a/Source/NETworkManager.Settings/PolicyInfo.cs
+++ b/Source/NETworkManager.Settings/PolicyInfo.cs
@@ -10,4 +10,7 @@ public class PolicyInfo
{
[JsonPropertyName("Update_CheckForUpdatesAtStartup")]
public bool? Update_CheckForUpdatesAtStartup { get; set; }
+
+ [JsonPropertyName("SettingsFolderLocation")]
+ public string? SettingsFolderLocation { get; set; }
}
diff --git a/Source/NETworkManager.Settings/PolicyManager.cs b/Source/NETworkManager.Settings/PolicyManager.cs
index 7dd762c8de..fc946620bc 100644
--- a/Source/NETworkManager.Settings/PolicyManager.cs
+++ b/Source/NETworkManager.Settings/PolicyManager.cs
@@ -83,6 +83,7 @@ public static void Load()
// Log enabled settings
Log.Info($"System-wide policy - Update_CheckForUpdatesAtStartup: {Current.Update_CheckForUpdatesAtStartup?.ToString() ?? "Not set"}");
+ Log.Info($"System-wide policy - SettingsFolderLocation: {Current.SettingsFolderLocation ?? "Not set"}");
}
}
catch (Exception ex)
diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs
index 97834d7515..5e810c8ec3 100644
--- a/Source/NETworkManager.Settings/SettingsInfo.cs
+++ b/Source/NETworkManager.Settings/SettingsInfo.cs
@@ -19,6 +19,16 @@
namespace NETworkManager.Settings;
+///
+/// Represents the application settings, user preferences, and configuration data for all supported features and
+/// modules. Supports property change notification for data binding and persistence scenarios.
+///
+/// The class provides a centralized container for storing and managing user-configurable
+/// options, operational parameters, and history collections for various application modules, such as network tools,
+/// remote access, and calculators. It implements the INotifyPropertyChanged interface to enable data binding and
+/// automatic UI updates when settings change. Most properties raise the PropertyChanged event when modified, allowing
+/// consumers to track changes and persist settings as needed. This class is typically used as the main settings model
+/// in applications that require user customization and state management across sessions.
public class SettingsInfo : INotifyPropertyChanged
{
///
diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs
index 29a140218e..302efe79a0 100644
--- a/Source/NETworkManager.Settings/SettingsManager.cs
+++ b/Source/NETworkManager.Settings/SettingsManager.cs
@@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
+using System.Security;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml.Serialization;
@@ -77,10 +78,106 @@ public static class SettingsManager
/// Path to the settings folder.
public static string GetSettingsFolderLocation()
{
- return ConfigurationManager.Current.IsPortable
- ? Path.Combine(AssemblyManager.Current.Location, SettingsFolderName)
- : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
- AssemblyManager.Current.Name, SettingsFolderName);
+ // 1. Policy override takes precedence (for IT administrators)
+ if (!string.IsNullOrWhiteSpace(PolicyManager.Current?.SettingsFolderLocation))
+ {
+ var validatedPath = ValidateSettingsFolderPath(
+ PolicyManager.Current.SettingsFolderLocation,
+ "Policy-provided",
+ "next priority");
+
+ if (validatedPath != null)
+ return validatedPath;
+ }
+
+ // 2. Custom user-configured path (not available in portable mode)
+ if (!ConfigurationManager.Current.IsPortable &&
+ !string.IsNullOrWhiteSpace(LocalSettingsManager.Current?.SettingsFolderLocation))
+ {
+ var validatedPath = ValidateSettingsFolderPath(
+ LocalSettingsManager.Current.SettingsFolderLocation,
+ "Custom",
+ "default location");
+
+ if (validatedPath != null)
+ return validatedPath;
+ }
+
+ // 3. Fall back to portable or default location
+ if (ConfigurationManager.Current.IsPortable)
+ return GetPortableSettingsFolderLocation();
+ else
+ return GetDefaultSettingsFolderLocation();
+ }
+
+ ///
+ /// Method to get the default settings folder location in the user's Documents directory.
+ ///
+ /// Path to the default settings folder location.
+ public static string GetDefaultSettingsFolderLocation()
+ {
+ return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
+ AssemblyManager.Current.Name, SettingsFolderName);
+ }
+
+ ///
+ /// Method to get the portable settings folder location (in the same directory as the application).
+ ///
+ /// Path to the portable settings folder location.
+ public static string GetPortableSettingsFolderLocation()
+ {
+ return Path.Combine(AssemblyManager.Current.Location, SettingsFolderName);
+ }
+
+ ///
+ /// Validates a settings folder path for correctness and accessibility.
+ ///
+ /// The path to validate.
+ /// Description of the path source for logging (e.g., "Policy-provided", "Custom").
+ /// Message describing what happens on validation failure (e.g., "next priority", "default location").
+ /// The validated full path if valid; otherwise, null.
+ private static string ValidateSettingsFolderPath(string path, string pathSource, string fallbackMessage)
+ {
+ // Expand environment variables first (e.g. %userprofile%\settings -> C:\Users\...\settings)
+ path = Environment.ExpandEnvironmentVariables(path);
+
+ // Validate that the path is rooted (absolute)
+ if (!Path.IsPathRooted(path))
+ {
+ Log.Error($"{pathSource} SettingsFolderLocation is not an absolute path: {path}. Falling back to {fallbackMessage}.");
+ return null;
+ }
+
+ // Validate that the path doesn't contain invalid characters
+ try
+ {
+ // This will throw ArgumentException, NotSupportedException, or SecurityException if the path is invalid
+ var fullPath = Path.GetFullPath(path);
+
+ // Check if the path is a directory (not a file)
+ if (File.Exists(fullPath))
+ {
+ Log.Error($"{pathSource} SettingsFolderLocation is a file, not a directory: {path}. Falling back to {fallbackMessage}.");
+ return null;
+ }
+
+ return Path.TrimEndingDirectorySeparator(fullPath);
+ }
+ catch (ArgumentException ex)
+ {
+ Log.Error($"{pathSource} SettingsFolderLocation contains invalid characters: {path}. Falling back to {fallbackMessage}.", ex);
+ return null;
+ }
+ catch (NotSupportedException ex)
+ {
+ Log.Error($"{pathSource} SettingsFolderLocation format is not supported: {path}. Falling back to {fallbackMessage}.", ex);
+ return null;
+ }
+ catch (SecurityException ex)
+ {
+ Log.Error($"Insufficient permissions to access {pathSource} SettingsFolderLocation: {path}. Falling back to {fallbackMessage}.", ex);
+ return null;
+ }
}
///
@@ -129,7 +226,6 @@ private static string GetLegacySettingsFilePath()
{
return Path.Combine(GetSettingsFolderLocation(), GetLegacySettingsFileName());
}
-
#endregion
#region Initialize, load and save
@@ -139,6 +235,8 @@ private static string GetLegacySettingsFilePath()
///
public static void Initialize()
{
+ Log.Info("Initializing new settings.");
+
Current = new SettingsInfo
{
Version = AssemblyManager.Current.Version.ToString()
@@ -158,8 +256,13 @@ public static void Load()
// Check if JSON file exists
if (File.Exists(filePath))
{
+ Log.Info($"Loading settings from: {filePath}");
+
Current = DeserializeFromFile(filePath);
+ Log.Info("Settings loaded successfully.");
+
+ // Reset change tracking
Current.SettingsChanged = false;
return;
@@ -235,24 +338,18 @@ public static void Save()
// Create backup before modifying
CreateDailyBackupIfNeeded();
- // Serialize the settings to a file
- SerializeToFile(GetSettingsFilePath());
-
- // Set the setting changed to false after saving them as file...
- Current.SettingsChanged = false;
- }
+ // Serialize to file
+ var filePath = GetSettingsFilePath();
- ///
- /// Method to serialize the settings to a JSON file.
- ///
- /// Path to the settings file.
- private static void SerializeToFile(string filePath)
- {
var jsonString = JsonSerializer.Serialize(Current, JsonOptions);
File.WriteAllText(filePath, jsonString);
- }
+ Log.Info($"Settings saved to {filePath}");
+
+ // Reset change tracking
+ Current.SettingsChanged = false;
+ }
#endregion
#region Backup
diff --git a/Source/NETworkManager.Settings/config.json.example b/Source/NETworkManager.Settings/config.json.example
index 8ca2bf05b3..0a47ffa700 100644
--- a/Source/NETworkManager.Settings/config.json.example
+++ b/Source/NETworkManager.Settings/config.json.example
@@ -1,3 +1,4 @@
{
- "Update_CheckForUpdatesAtStartup": false
+ "Update_CheckForUpdatesAtStartup": false,
+ "SettingsFolderLocation": "C:\\CustomPath\\NETworkManager\\Settings"
}
\ No newline at end of file
diff --git a/Source/NETworkManager.Utilities/RegexHelper.cs b/Source/NETworkManager.Utilities/RegexHelper.cs
index ec5d2385a1..4935e0ec41 100644
--- a/Source/NETworkManager.Utilities/RegexHelper.cs
+++ b/Source/NETworkManager.Utilities/RegexHelper.cs
@@ -85,6 +85,30 @@ public static partial class RegexHelper
[GeneratedRegex($@"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|{SpecialRangeRegex})\.){{3}}((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|{SpecialRangeRegex})$")]
public static partial Regex IPv4AddressSpecialRangeRegex();
+ ///
+ /// Provides a compiled regular expression that matches valid hostnames or fully qualified domain names (FQDNs) like
+ /// server-01 or server-01.example.com.
+ ///
+ /// A instance that matches valid hostnames or FQDNs.
+ [GeneratedRegex($@"^{HostnameOrDomainValues}$")]
+ public static partial Regex HostnameOrDomainRegex();
+
+ ///
+ /// Creates a regular expression that matches a local directory path or one using environment variables,
+ /// like "C:\Temp", "C:\My Settings", "%AppData%\settings".
+ ///
+ /// A instance that matches valid local directory paths.
+ [GeneratedRegex($@"^(?:%[^%]+%|[a-zA-Z]\:)(\\[a-zA-Z0-9_\-\s\.]+)+\\?$")]
+ public static partial Regex DirectoryPathWithEnvironmentVariablesRegex();
+
+ ///
+ /// Creates a regular expression that matches a UNC path like "\\server\share", "\\server\c$\settings".
+ /// The share name may end with $ for hidden shares.
+ ///
+ /// A instance that matches valid UNC paths.
+ [GeneratedRegex($@"^\\\\[a-zA-Z0-9_\-\.]+(\\[a-zA-Z0-9_\-\s\.]+\$?)(\\[a-zA-Z0-9_\-\s\.]+)*\\?$")]
+ public static partial Regex UncPathRegex();
+
// Match a MAC-Address 000000000000 00:00:00:00:00:00, 00-00-00-00-00-00-00 or 0000.0000.0000
public const string MACAddressRegex =
@"^^[A-Fa-f0-9]{12}$|^[A-Fa-f0-9]{2}(:|-){1}[A-Fa-f0-9]{2}(:|-){1}[A-Fa-f0-9]{2}(:|-){1}[A-Fa-f0-9]{2}(:|-){1}[A-Fa-f0-9]{2}(:|-){1}[A-Fa-f0-9]{2}$|^[A-Fa-f0-9]{4}.[A-Fa-f0-9]{4}.[A-Fa-f0-9]{4}$$";
@@ -107,16 +131,6 @@ public static partial class RegexHelper
public const string SpecialRangeRegex =
@"\[((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)-(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)))([,]((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)-(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))))*\]";
-
-
- ///
- /// Provides a compiled regular expression that matches valid hostnames or fully qualified domain names (FQDNs) like
- /// server-01 or server-01.example.com.
- ///
- /// A instance that matches valid hostnames or FQDNs.
- [GeneratedRegex($@"^{HostnameOrDomainValues}$")]
- public static partial Regex HostnameOrDomainRegex();
-
// Match a hostname with cidr like server-01.example.com/24
public const string HostnameOrDomainWithCidrRegex = $@"^{HostnameOrDomainValues}\/{CidrRegexValues}$";
@@ -134,9 +148,6 @@ public static partial class RegexHelper
// Match a port between 1-65535
public const string PortRegex = $@"^{PortValues}$";
- // Match any filepath (like "c:\temp\") --> https://www.codeproject.com/Tips/216238/Regular-Expression-to-Validate-File-Path-and-Exten
- public const string FilePathRegex = @"^(?:[\w]\:|\\)(\\[a-z_\-\s0-9\.]+)+$";
-
// Match any fullname (like "c:\temp\test.txt") --> https://www.codeproject.com/Tips/216238/Regular-Expression-to-Validate-File-Path-and-Exten
public const string FullNameRegex =
@"^(?:[\w]\:|\\)(\\[a-zA-Z0-9_\-\s\.()~!@#$%^&=+';,{}\[\]]+)+\.[a-zA-z0-9]{1,4}$";
diff --git a/Source/NETworkManager.Validators/DirectoryPathWithEnvironmentVariablesValidator.cs b/Source/NETworkManager.Validators/DirectoryPathWithEnvironmentVariablesValidator.cs
index 1d7ead626e..c532afb9bb 100644
--- a/Source/NETworkManager.Validators/DirectoryPathWithEnvironmentVariablesValidator.cs
+++ b/Source/NETworkManager.Validators/DirectoryPathWithEnvironmentVariablesValidator.cs
@@ -1,6 +1,4 @@
-using System;
-using System.Globalization;
-using System.Text.RegularExpressions;
+using System.Globalization;
using System.Windows.Controls;
using NETworkManager.Localization.Resources;
using NETworkManager.Utilities;
@@ -8,24 +6,27 @@
namespace NETworkManager.Validators;
///
-/// Check if the string is a valid directory path (like "C:\Temp\" or "%AppData%\Temp"). The directory path does not
-/// have to exist on the local system.
+/// Provides a validation rule that determines whether a value represents a syntactically valid directory path,
+/// supporting the inclusion of environment variable references like %UserProfile%.
///
public class DirectoryPathWithEnvironmentVariablesValidator : ValidationRule
{
///
- /// Check if the string is a valid directory path (like "C:\Temp\" or "%AppData%\Temp"). The directory path does not
- /// have to exist on the local system.
- ///
- /// Directory path like "C:\test" or "%AppData%\test".
- /// Culture to use for validation.
- /// True if the directory path is valid.
+ /// Validates whether the specified value represents a valid directory path, allowing for the inclusion of
+ /// environment variables like %UserProfile%.
+ ///
+ /// The value to validate as a directory path. May include environment variable references. Can be a string or an
+ /// object convertible to a string.
+ /// The culture-specific information relevant to the validation process. This parameter is not used in this
+ /// implementation.
+ /// A ValidationResult that indicates whether the value is a valid directory path. Returns
+ /// ValidationResult.ValidResult if the value is valid; otherwise, returns a ValidationResult with an error message.
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
- var path = Environment.ExpandEnvironmentVariables((string)value);
+ var path = $"{value}";
- return new Regex(RegexHelper.FilePathRegex, RegexOptions.IgnoreCase).IsMatch(path)
+ return RegexHelper.DirectoryPathWithEnvironmentVariablesRegex().IsMatch(path) || RegexHelper.UncPathRegex().IsMatch(path)
? ValidationResult.ValidResult
- : new ValidationResult(false, Strings.EnterValidFilePath);
+ : new ValidationResult(false, Strings.EnterValidFolderPath);
}
-}
\ No newline at end of file
+}
diff --git a/Source/NETworkManager/App.xaml.cs b/Source/NETworkManager/App.xaml.cs
index 42d0f8503e..1070bc951b 100644
--- a/Source/NETworkManager/App.xaml.cs
+++ b/Source/NETworkManager/App.xaml.cs
@@ -21,12 +21,13 @@ namespace NETworkManager;
* 2) Detect current configuration
* 3) Get assembly info
* 4) Load system-wide policies
- * 5) Load settings
- * 6) Load localization / language
+ * 5) Load local settings
+ * 6) Load settings
+ * 7) Load localization / language
*
* Class: MainWindow
- * 7) Load appearance
- * 8) Load profiles
+ * 8) Load appearance
+ * 9) Load profiles
*/
public partial class App
@@ -85,11 +86,12 @@ by BornToBeRoot
// Load system-wide policies
PolicyManager.Load();
+ // Load (or initialize) local settings
+ LocalSettingsManager.Load();
+
// Load (or initialize) settings
try
{
- Log.Info("Application settings are being loaded...");
-
if (CommandLineManager.Current.ResetSettings)
SettingsManager.Initialize();
else
@@ -112,21 +114,13 @@ by BornToBeRoot
var settingsVersion = Version.Parse(SettingsManager.Current.Version);
if (settingsVersion < AssemblyManager.Current.Version)
- {
- Log.Info(
- $"Application settings are on version {settingsVersion} and will be upgraded to {AssemblyManager.Current.Version}");
-
SettingsManager.Upgrade(settingsVersion, AssemblyManager.Current.Version);
-
- Log.Info($"Application settings upgraded to version {AssemblyManager.Current.Version}");
- }
else
- {
Log.Info($"Application settings are already on version {AssemblyManager.Current.Version}.");
- }
// Initialize localization
var localizationManager = LocalizationManager.GetInstance(SettingsManager.Current.Localization_CultureCode);
+
Strings.Culture = localizationManager.Culture;
Log.Info(
@@ -305,6 +299,13 @@ private void Application_Exit(object sender, ExitEventArgs e)
/// file is encrypted and not unlocked, profile data will not be saved and a warning is logged.
private void Save()
{
+ // Save local settings if they have changed
+ if (LocalSettingsManager.Current.SettingsChanged)
+ {
+ Log.Info("Save local settings...");
+ LocalSettingsManager.Save();
+ }
+
// Save settings if they have changed
if (SettingsManager.Current.SettingsChanged)
{
diff --git a/Source/NETworkManager/NETworkManager.csproj b/Source/NETworkManager/NETworkManager.csproj
index 3dee3d3535..6a6a3a04a2 100644
--- a/Source/NETworkManager/NETworkManager.csproj
+++ b/Source/NETworkManager/NETworkManager.csproj
@@ -8,10 +8,6 @@
win-x64x64false
-
sdktruetrue
@@ -62,7 +58,7 @@
-
+
@@ -73,9 +69,8 @@
-
-
-
+
+
diff --git a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs
index 330f98e21e..e14ee3639c 100644
--- a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs
+++ b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs
@@ -3,6 +3,7 @@
using NETworkManager.Utilities;
using System;
using System.Diagnostics;
+using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
@@ -12,12 +13,24 @@ namespace NETworkManager.ViewModels;
public class SettingsSettingsViewModel : ViewModelBase
{
#region Variables
+ ///
+ /// Gets or sets the action to execute when the associated object is closed.
+ ///
public Action CloseAction { get; set; }
+ ///
+ /// Indicates whether the settings are currently being loaded to prevent triggering change events during initialization.
+ ///
private readonly bool _isLoading;
+ ///
+ /// Private field of property.
+ ///
private string _location;
+ ///
+ /// Gets or sets the file system path to the settings location.
+ ///
public string Location
{
get => _location;
@@ -26,13 +39,69 @@ public string Location
if (value == _location)
return;
+ if (!_isLoading)
+ IsLocationChanged = !string.Equals(value, SettingsManager.GetSettingsFolderLocation(), StringComparison.OrdinalIgnoreCase);
+
_location = value;
OnPropertyChanged();
}
}
+ ///
+ /// Indicates whether the settings location is managed by a system-wide policy.
+ ///
+ public bool IsLocationManagedByPolicy => !string.IsNullOrWhiteSpace(PolicyManager.Current?.SettingsFolderLocation);
+
+ ///
+ /// Private field of property.
+ ///
+ private bool _isLocationChanged;
+
+ ///
+ /// Gets or sets a value indicating whether the location has changed.
+ ///
+ public bool IsLocationChanged
+ {
+ get => _isLocationChanged;
+ set
+ {
+ if (value == _isLocationChanged)
+ return;
+
+ _isLocationChanged = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Private field of property.
+ ///
+ private bool _isDefaultLocation;
+
+ ///
+ /// Indicates whether the current location is the default settings folder location.
+ ///
+ public bool IsDefaultLocation
+ {
+ get => _isDefaultLocation;
+ set
+ {
+ if (value == _isDefaultLocation)
+ return;
+
+ _isDefaultLocation = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Private field of property.
+ ///
private bool _isDailyBackupEnabled;
+ ///
+ /// Gets or sets a value indicating whether daily backups are enabled.
+ ///
public bool IsDailyBackupEnabled
{
get => _isDailyBackupEnabled;
@@ -49,8 +118,14 @@ public bool IsDailyBackupEnabled
}
}
+ ///
+ /// Private field of property.
+ ///
private int _maximumNumberOfBackups;
+ ///
+ /// Gets or sets the maximum number of backups to keep.
+ ///
public int MaximumNumberOfBackups
{
get => _maximumNumberOfBackups;
@@ -71,6 +146,9 @@ public int MaximumNumberOfBackups
#region Constructor, LoadSettings
+ ///
+ /// Initializes a new instance of the class and loads the current settings.
+ ///
public SettingsSettingsViewModel()
{
_isLoading = true;
@@ -80,9 +158,13 @@ public SettingsSettingsViewModel()
_isLoading = false;
}
+ ///
+ /// Loads the application settings from the current settings folder location.
+ ///
private void LoadSettings()
{
Location = SettingsManager.GetSettingsFolderLocation();
+ IsDefaultLocation = string.Equals(Location, SettingsManager.GetDefaultSettingsFolderLocation(), StringComparison.OrdinalIgnoreCase);
IsDailyBackupEnabled = SettingsManager.Current.Settings_IsDailyBackupEnabled;
MaximumNumberOfBackups = SettingsManager.Current.Settings_MaximumNumberOfBackups;
}
@@ -91,15 +173,27 @@ private void LoadSettings()
#region ICommands & Actions
+ ///
+ /// Gets the command that opens a location when executed.
+ ///
public ICommand OpenLocationCommand => new RelayCommand(_ => OpenLocationAction());
+ ///
+ /// Opens the settings folder location in Windows Explorer.
+ ///
private static void OpenLocationAction()
{
Process.Start("explorer.exe", SettingsManager.GetSettingsFolderLocation());
}
+ ///
+ /// Gets the command that resets the application settings to their default values.
+ ///
public ICommand ResetSettingsCommand => new RelayCommand(_ => ResetSettingsAction());
+ ///
+ /// Resets the application settings to their default values.
+ ///
private void ResetSettingsAction()
{
ResetSettings().ConfigureAwait(false);
@@ -108,7 +202,119 @@ private void ResetSettingsAction()
#endregion
#region Methods
+ ///
+ /// Gets the command that opens the location folder selection dialog.
+ ///
+ public ICommand BrowseLocationFolderCommand => new RelayCommand(p => BrowseLocationFolderAction());
+
+ ///
+ /// Opens a dialog that allows the user to select a folder location and updates the Location property with the
+ /// selected path if the user confirms the selection.
+ ///
+ /// If the Location property is set to a valid directory path, it is pre-selected in the dialog.
+ /// This method does not return a value and is intended for use in a user interface context where folder selection
+ /// is required.
+ private void BrowseLocationFolderAction()
+ {
+ using var dialog = new System.Windows.Forms.FolderBrowserDialog();
+
+ if (Directory.Exists(Location))
+ dialog.SelectedPath = Location;
+
+ var dialogResult = dialog.ShowDialog();
+
+ if (dialogResult == System.Windows.Forms.DialogResult.OK)
+ Location = dialog.SelectedPath;
+ }
+
+ ///
+ /// Sets the location path based on the provided drag-and-drop input.
+ ///
+ /// The path to set as the location. This value cannot be null or empty.
+ public void SetLocationPathFromDragDrop(string path)
+ {
+ Location = path;
+ }
+
+ ///
+ /// Gets the command that initiates the action to change the location.
+ ///
+ public ICommand ChangeLocationCommand => new RelayCommand(_ => ChangeLocationAction().ConfigureAwait(false));
+
+ ///
+ /// Prompts the user to confirm and then changes the location of the application's settings folder.
+ ///
+ /// This method displays a confirmation dialog to the user before changing the settings folder
+ /// location. If the user confirms, it saves the current settings, updates the settings folder location, and
+ /// restarts the application to apply the changes. No action is taken if the user cancels the confirmation
+ /// dialog.
+ /// A task that represents the asynchronous operation.
+ private async Task ChangeLocationAction()
+ {
+ var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow,
+ Strings.ChangeLocationQuestion,
+ string.Format(Strings.ChangeLocationSettingsMessage, SettingsManager.GetSettingsFolderLocation(), Location),
+ ChildWindowIcon.Question,
+ Strings.Change);
+
+ if (!result)
+ return;
+
+ // Save settings at the current location before changing it to prevent
+ // unintended saves to the new location (e.g., triggered by background timer or the app close & restart).
+ SettingsManager.Save();
+
+ // Set new location
+ LocalSettingsManager.Current.SettingsFolderLocation = Location;
+ LocalSettingsManager.Save();
+
+ // Restart the application
+ (Application.Current.MainWindow as MainWindow)?.RestartApplication();
+ }
+
+ ///
+ /// Gets the command that restores the default location settings asynchronously.
+ ///
+ public ICommand RestoreDefaultLocationCommand => new RelayCommand(_ => RestoreDefaultLocationActionAsync().ConfigureAwait(false));
+
+ ///
+ /// Restores the application's settings folder location to the default path after obtaining user confirmation.
+ ///
+ /// This method prompts the user to confirm the restoration of the default settings location. If
+ /// the user confirms, it saves the current settings, clears any custom location, and restarts the application to
+ /// apply the changes. Use this method when you want to revert to the default settings folder and ensure all changes
+ /// are properly saved and applied.
+ /// A task that represents the asynchronous operation.
+ private async Task RestoreDefaultLocationActionAsync()
+ {
+ var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow,
+ Strings.RestoreDefaultLocationQuestion,
+ string.Format(Strings.RestoreDefaultLocationSettingsMessage, SettingsManager.GetSettingsFolderLocation(), SettingsManager.GetDefaultSettingsFolderLocation()),
+ ChildWindowIcon.Question,
+ Strings.Restore);
+
+ if (!result)
+ return;
+
+ // Save settings at the current location before changing it to prevent
+ // unintended saves to the new location (e.g., triggered by background timer or the app close & restart).
+ SettingsManager.Save();
+
+ // Clear custom location to revert to default
+ LocalSettingsManager.Current.SettingsFolderLocation = null;
+ LocalSettingsManager.Save();
+
+ // Restart the application
+ (Application.Current.MainWindow as MainWindow)?.RestartApplication();
+ }
+ ///
+ /// Resets the application settings to their default values and restarts the application after user confirmation.
+ ///
+ /// Displays a confirmation dialog to the user before proceeding. If the user confirms, the
+ /// settings are reinitialized to their defaults and the application is restarted. No action is taken if the user
+ /// cancels the confirmation dialog.
+ /// A task that represents the asynchronous operation.
private async Task ResetSettings()
{
var result = await DialogHelper.ShowConfirmationMessageAsync(Application.Current.MainWindow,
diff --git a/Source/NETworkManager/Views/SettingsSettingsView.xaml b/Source/NETworkManager/Views/SettingsSettingsView.xaml
index b51f7fe9e6..d1db5b42be 100644
--- a/Source/NETworkManager/Views/SettingsSettingsView.xaml
+++ b/Source/NETworkManager/Views/SettingsSettingsView.xaml
@@ -7,31 +7,180 @@
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:viewModels="clr-namespace:NETworkManager.ViewModels"
xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization"
+ xmlns:converters="clr-namespace:NETworkManager.Converters;assembly=NETworkManager.Converters"
+ xmlns:validators="clr-namespace:NETworkManager.Validators;assembly=NETworkManager.Validators"
+ xmlns:settings="clr-namespace:NETworkManager.Settings;assembly=NETworkManager.Settings"
mc:Ignorable="d" Loaded="UserControl_Loaded"
d:DataContext="{d:DesignInstance viewModels:SettingsSettingsViewModel}">
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Check for updates at startup](../settings/update.md#check-for-updates-at-startup) documentation for more information)
+ - Settings folder location (see [Settings > Location](../settings/settings.md#location) documentation for more information)
+
+ :::note
+
+ If you have specific requirements for system-wide policies in your organization, please submit a feature request via the [GitHub issue tracker](https://github.com/BornToBeRoot/NETworkManager/issues/new/choose).
+
+ :::
+
- New language Ukrainian (`uk-UA`) has been added. Thanks to [@vadickkt](https://github.com/vadickkt) [#3240](https://github.com/BornToBeRoot/NETworkManager/pull/3240)
- Migrated all dialogs to child windows for improved usability and accessibility. [#3271](https://github.com/BornToBeRoot/NETworkManager/pull/3271)
-- System-wide policies can now be configured via a `config.json` file in the application directory to control settings for all users. Currently supports controlling the "Check for updates at startup" setting. This is useful for enterprise deployments where administrators need centralized control over update behavior. See [System-Wide Policies](../system-wide-policies.md) documentation for more information. [#3313](https://github.com/BornToBeRoot/NETworkManager/pull/3325) [#3313](https://github.com/BornToBeRoot/NETworkManager/pull/3325)
**DNS Lookup**
diff --git a/Website/docs/settings/settings.md b/Website/docs/settings/settings.md
index 32342b3d6e..3af08fd661 100644
--- a/Website/docs/settings/settings.md
+++ b/Website/docs/settings/settings.md
@@ -17,6 +17,34 @@ Folder where the application settings are stored.
| Setup / Archiv | `%UserProfile%\Documents\NETworkManager\Settings` |
| Portable | `\Settings` |
+:::info System-Wide Policy
+
+
+Click to expand
+
+This setting can be controlled by administrators using a system-wide policy. See [System-Wide Policies](../system-wide-policies.md) for more information.
+
+**Policy Property:** `SettingsFolderLocation`
+
+**Values:**
+
+- Absolute path (e.g., `C:\\Path\\To\\Settings`)
+- Path with environment variables (e.g., `%UserProfile%\\NETworkManager\\Settings`)
+- UNC path (e.g., `\\\\server\\share$\\NETworkManager\\Settings`)
+- Omit the property to allow the default location logic to apply (portable vs. non-portable)
+
+**Example:**
+
+```json
+{
+ "SettingsFolderLocation": "%UserProfile%\\NETworkManager\\Settings"
+}
+```
+
+
+
+:::
+
:::note
**Recommendation**
@@ -24,12 +52,14 @@ It is strongly recommended to regularly back up your settings files.
**Automatic backups**
NETworkManager automatically creates a backup of the settings files before applying any changes. See [Create daily backup](#create-daily-backup) and [Maximum number of backups](#maximum-number-of-backups) for configuration options.
+
- Location: `Settings\Backups` subfolder
- Naming: timestamped (e.g. `yyyyMMddHHmmss_Settings.json`)
- Frequency: **once per day** at most (even if multiple changes occur)
- Retention: keeps the **10 most recent backups** (default)
-**Restoring settings**
+**Restoring settings**
+
1. Completely close NETworkManager
2. Locate the desired backup in `Settings\Backups`
3. Copy the file(s) back to the original folder (overwriting existing files)
diff --git a/Website/docs/settings/update.md b/Website/docs/settings/update.md
index 9c006a108b..bf1c5dd5e8 100644
--- a/Website/docs/settings/update.md
+++ b/Website/docs/settings/update.md
@@ -14,11 +14,15 @@ Check for new program versions on GitHub when the application is launched.
:::info System-Wide Policy
+
+Click to expand
+
This setting can be controlled by administrators using a system-wide policy. See [System-Wide Policies](../system-wide-policies.md) for more information.
**Policy Property:** `Update_CheckForUpdatesAtStartup`
**Values:**
+
- `true` - Force enable automatic update checks at startup for all users
- `false` - Force disable automatic update checks at startup for all users
- Omit the property - Allow users to control this setting themselves
@@ -31,6 +35,8 @@ This setting can be controlled by administrators using a system-wide policy. See
}
```
+
+
:::
:::note
diff --git a/Website/docs/system-wide-policies.md b/Website/docs/system-wide-policies.md
index d8ef644453..d6aa0a5c9d 100644
--- a/Website/docs/system-wide-policies.md
+++ b/Website/docs/system-wide-policies.md
@@ -36,7 +36,8 @@ The `config.json` file uses a simple JSON structure to define policy values. An
```json
{
- "Update_CheckForUpdatesAtStartup": false
+ "Update_CheckForUpdatesAtStartup": false,
+ "SettingsFolderLocation": "C:\\CustomPath\\NETworkManager\\Settings"
}
```