diff --git a/SafeExamBrowser.Client.UnitTests/ClientControllerTests.cs b/SafeExamBrowser.Client.UnitTests/ClientControllerTests.cs index 29d14843..59b7f23d 100644 --- a/SafeExamBrowser.Client.UnitTests/ClientControllerTests.cs +++ b/SafeExamBrowser.Client.UnitTests/ClientControllerTests.cs @@ -33,6 +33,7 @@ using SafeExamBrowser.Server.Contracts; using SafeExamBrowser.Server.Contracts.Data; using SafeExamBrowser.Settings; using SafeExamBrowser.Settings.Monitoring; +using SafeExamBrowser.SystemComponents.Contracts.Network; using SafeExamBrowser.SystemComponents.Contracts.Registry; using SafeExamBrowser.UserInterface.Contracts; using SafeExamBrowser.UserInterface.Contracts.FileSystemDialog; @@ -61,6 +62,7 @@ namespace SafeExamBrowser.Client.UnitTests private Mock integrityModule; private Mock logger; private Mock messageBox; + private Mock networkAdapter; private Mock operationSequence; private Mock registry; private Mock runtimeProxy; @@ -94,6 +96,7 @@ namespace SafeExamBrowser.Client.UnitTests integrityModule = new Mock(); logger = new Mock(); messageBox = new Mock(); + networkAdapter = new Mock(); operationSequence = new Mock(); registry = new Mock(); runtimeProxy = new Mock(); @@ -122,6 +125,7 @@ namespace SafeExamBrowser.Client.UnitTests hashAlgorithm.Object, logger.Object, messageBox.Object, + networkAdapter.Object, operationSequence.Object, registry.Object, runtimeProxy.Object, diff --git a/SafeExamBrowser.Client/ClientController.cs b/SafeExamBrowser.Client/ClientController.cs index e6f706de..9c53f207 100644 --- a/SafeExamBrowser.Client/ClientController.cs +++ b/SafeExamBrowser.Client/ClientController.cs @@ -35,6 +35,8 @@ using SafeExamBrowser.Proctoring.Contracts.Events; using SafeExamBrowser.Server.Contracts; using SafeExamBrowser.Server.Contracts.Data; using SafeExamBrowser.Settings; +using SafeExamBrowser.SystemComponents.Contracts.Network; +using SafeExamBrowser.SystemComponents.Contracts.Network.Events; using SafeExamBrowser.SystemComponents.Contracts.Registry; using SafeExamBrowser.UserInterface.Contracts; using SafeExamBrowser.UserInterface.Contracts.FileSystemDialog; @@ -57,6 +59,7 @@ namespace SafeExamBrowser.Client private readonly IHashAlgorithm hashAlgorithm; private readonly ILogger logger; private readonly IMessageBox messageBox; + private readonly INetworkAdapter networkAdapter; private readonly IOperationSequence operations; private readonly IRegistry registry; private readonly IRuntimeProxy runtime; @@ -87,6 +90,7 @@ namespace SafeExamBrowser.Client IHashAlgorithm hashAlgorithm, ILogger logger, IMessageBox messageBox, + INetworkAdapter networkAdapter, IOperationSequence operations, IRegistry registry, IRuntimeProxy runtime, @@ -106,6 +110,7 @@ namespace SafeExamBrowser.Client this.hashAlgorithm = hashAlgorithm; this.logger = logger; this.messageBox = messageBox; + this.networkAdapter = networkAdapter; this.operations = operations; this.registry = registry; this.runtime = runtime; @@ -214,6 +219,7 @@ namespace SafeExamBrowser.Client ClientHost.ServerFailureActionRequested += ClientHost_ServerFailureActionRequested; ClientHost.Shutdown += ClientHost_Shutdown; displayMonitor.DisplayChanged += DisplayMonitor_DisplaySettingsChanged; + networkAdapter.CredentialsRequired += NetworkAdapter_CredentialsRequired; registry.ValueChanged += Registry_ValueChanged; runtime.ConnectionLost += Runtime_ConnectionLost; systemMonitor.SessionChanged += SystemMonitor_SessionChanged; @@ -690,6 +696,16 @@ namespace SafeExamBrowser.Client } } + private void NetworkAdapter_CredentialsRequired(CredentialsRequiredEventArgs args) + { + var dialog = uiFactory.CreateNetworkDialog("TODO", "TODO"); + var result = dialog.Show(); + + args.Password = result.Password; + args.Success = result.Success; + args.Username = result.Username; + } + private void Operations_ActionRequired(ActionRequiredEventArgs args) { switch (args) diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs index fe9879c1..f878c35c 100644 --- a/SafeExamBrowser.Client/CompositionRoot.cs +++ b/SafeExamBrowser.Client/CompositionRoot.cs @@ -154,6 +154,7 @@ namespace SafeExamBrowser.Client hashAlgorithm, logger, messageBox, + networkAdapter, sequence, registry, runtimeProxy, diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs index 61b84d98..d49961fe 100644 --- a/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs +++ b/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs @@ -26,6 +26,26 @@ namespace SafeExamBrowser.Configuration.ConfigurationData InitializeClipboardSettings(settings); InitializeProctoringSettings(settings); RemoveLegacyBrowsers(settings); + + settings.Applications.Blacklist.Clear(); + settings.Applications.Whitelist.Add(new WhitelistApplication { ExecutableName = "mspaint.exe", ShowInShell = true }); + settings.Browser.AdditionalWindow.AllowAddressBar = true; + settings.Browser.MainWindow.AllowAddressBar = true; + settings.Browser.MainWindow.AllowDeveloperConsole = true; + settings.Browser.MainWindow.AllowBackwardNavigation = true; + settings.Browser.MainWindow.AllowForwardNavigation = true; + settings.Browser.MainWindow.ShowHomeButton = true; + settings.Browser.MainWindow.UrlPolicy = Settings.Browser.UrlPolicy.BeforeTitle; + settings.Keyboard.AllowPrintScreen = true; + settings.LogLevel = Settings.Logging.LogLevel.Debug; + settings.Security.AllowApplicationLogAccess = true; + settings.Security.ClipboardPolicy = ClipboardPolicy.Allow; + settings.Security.KioskMode = KioskMode.CreateNewDesktop; + settings.Security.QuitPasswordHash = default; + settings.Service.IgnoreService = true; + settings.Service.Policy = Settings.Service.ServicePolicy.Optional; + settings.Security.VersionRestrictions.Clear(); + settings.Taskbar.ShowApplicationLog = true; } private void AllowBrowserToolbarForReloading(AppSettings settings) diff --git a/SafeExamBrowser.SystemComponents.Contracts/Network/Events/CredentialsRequiredEventArgs.cs b/SafeExamBrowser.SystemComponents.Contracts/Network/Events/CredentialsRequiredEventArgs.cs new file mode 100644 index 00000000..111aff92 --- /dev/null +++ b/SafeExamBrowser.SystemComponents.Contracts/Network/Events/CredentialsRequiredEventArgs.cs @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 ETH Zürich, IT Services + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +namespace SafeExamBrowser.SystemComponents.Contracts.Network.Events +{ + /// + /// + /// + public class CredentialsRequiredEventArgs + { + /// + /// + /// + public string Password { get; set; } + + /// + /// + /// + public bool Success { get; set; } + + /// + /// + /// + public string Username { get; set; } + } +} diff --git a/SafeExamBrowser.SystemComponents.Contracts/Network/Events/CredentialsRequiredEventHandler.cs b/SafeExamBrowser.SystemComponents.Contracts/Network/Events/CredentialsRequiredEventHandler.cs new file mode 100644 index 00000000..888d97a6 --- /dev/null +++ b/SafeExamBrowser.SystemComponents.Contracts/Network/Events/CredentialsRequiredEventHandler.cs @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2024 ETH Zürich, IT Services + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +namespace SafeExamBrowser.SystemComponents.Contracts.Network.Events +{ + /// + /// + /// + public delegate void CredentialsRequiredEventHandler(CredentialsRequiredEventArgs args); +} diff --git a/SafeExamBrowser.SystemComponents.Contracts/Network/INetworkAdapter.cs b/SafeExamBrowser.SystemComponents.Contracts/Network/INetworkAdapter.cs index 306bd535..207397cf 100644 --- a/SafeExamBrowser.SystemComponents.Contracts/Network/INetworkAdapter.cs +++ b/SafeExamBrowser.SystemComponents.Contracts/Network/INetworkAdapter.cs @@ -6,7 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -using System; using System.Collections.Generic; using SafeExamBrowser.SystemComponents.Contracts.Network.Events; @@ -33,9 +32,14 @@ namespace SafeExamBrowser.SystemComponents.Contracts.Network event ChangedEventHandler Changed; /// - /// Attempts to connect to the wireless network with the given ID. + /// Fired when credentials are required to connect to a network. /// - void ConnectToWirelessNetwork(Guid id); + event CredentialsRequiredEventHandler CredentialsRequired; + + /// + /// Attempts to connect to the wireless network with the given name. + /// + void ConnectToWirelessNetwork(string name); /// /// Retrieves all currently available wireless networks. diff --git a/SafeExamBrowser.SystemComponents.Contracts/Network/IWirelessNetwork.cs b/SafeExamBrowser.SystemComponents.Contracts/Network/IWirelessNetwork.cs index 24c20d24..965d5ed3 100644 --- a/SafeExamBrowser.SystemComponents.Contracts/Network/IWirelessNetwork.cs +++ b/SafeExamBrowser.SystemComponents.Contracts/Network/IWirelessNetwork.cs @@ -6,8 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -using System; - namespace SafeExamBrowser.SystemComponents.Contracts.Network { /// @@ -15,11 +13,6 @@ namespace SafeExamBrowser.SystemComponents.Contracts.Network /// public interface IWirelessNetwork { - /// - /// The unique identifier of the network. - /// - Guid Id { get; } - /// /// The network name. /// diff --git a/SafeExamBrowser.SystemComponents.Contracts/SafeExamBrowser.SystemComponents.Contracts.csproj b/SafeExamBrowser.SystemComponents.Contracts/SafeExamBrowser.SystemComponents.Contracts.csproj index 5724831b..7816c2b7 100644 --- a/SafeExamBrowser.SystemComponents.Contracts/SafeExamBrowser.SystemComponents.Contracts.csproj +++ b/SafeExamBrowser.SystemComponents.Contracts/SafeExamBrowser.SystemComponents.Contracts.csproj @@ -58,6 +58,8 @@ + + diff --git a/SafeExamBrowser.SystemComponents/Network/NetworkAdapter.cs b/SafeExamBrowser.SystemComponents/Network/NetworkAdapter.cs index 1942fbbc..124cfadc 100644 --- a/SafeExamBrowser.SystemComponents/Network/NetworkAdapter.cs +++ b/SafeExamBrowser.SystemComponents/Network/NetworkAdapter.cs @@ -10,7 +10,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net.NetworkInformation; -using System.Timers; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.SystemComponents.Contracts.Network; using SafeExamBrowser.SystemComponents.Contracts.Network.Events; @@ -18,12 +17,22 @@ using SafeExamBrowser.WindowsApi.Contracts; using SimpleWifi; using SimpleWifi.Win32; using SimpleWifi.Win32.Interop; +using Timer = System.Timers.Timer; namespace SafeExamBrowser.SystemComponents.Network { + /// + /// Switch to the following WiFi library: + /// https://github.com/emoacht/ManagedNativeWifi + /// https://www.nuget.org/packages/ManagedNativeWifi + /// + /// Potentially useful: + /// https://learn.microsoft.com/en-us/dotnet/api/system.net.networkinformation.networkinterface?view=netframework-4.8 + /// public class NetworkAdapter : INetworkAdapter { private readonly object @lock = new object(); + private readonly ILogger logger; private readonly INativeMethods nativeMethods; private readonly List wirelessNetworks; @@ -35,6 +44,7 @@ namespace SafeExamBrowser.SystemComponents.Network public ConnectionType Type { get; private set; } public event ChangedEventHandler Changed; + public event CredentialsRequiredEventHandler CredentialsRequired; public NetworkAdapter(ILogger logger, INativeMethods nativeMethods) { @@ -43,22 +53,27 @@ namespace SafeExamBrowser.SystemComponents.Network this.wirelessNetworks = new List(); } - public void ConnectToWirelessNetwork(Guid id) + public void ConnectToWirelessNetwork(string name) { lock (@lock) { - var network = wirelessNetworks.FirstOrDefault(n => n.Id == id); + var network = wirelessNetworks.FirstOrDefault(n => n.Name == name); if (network != default) { try { - var request = new AuthRequest(network.AccessPoint); + var accessPoint = network.AccessPoint; + var request = new AuthRequest(accessPoint); - logger.Info($"Attempting to connect to '{network.Name}'..."); + if (accessPoint.HasProfile || accessPoint.IsConnected || TryGetCredentials(request)) + { + logger.Info($"Attempting to connect to wireless network '{network.Name}' with{(request.Password == default ? "out" : "")} credentials..."); - network.AccessPoint.ConnectAsync(request, false, (success) => AccessPoint_OnConnectCompleted(network.Name, success)); - Status = ConnectionStatus.Connecting; + // TODO: Retry resp. alert of password error on failure and then ignore profile?! + accessPoint.ConnectAsync(request, false, (success) => ConnectionAttemptCompleted(network.Name, success)); + Status = ConnectionStatus.Connecting; + } } catch (Exception e) { @@ -67,7 +82,7 @@ namespace SafeExamBrowser.SystemComponents.Network } else { - logger.Warn($"Could not find network with id '{id}'!"); + logger.Warn($"Could not find wireless network '{name}'!"); } } @@ -111,7 +126,7 @@ namespace SafeExamBrowser.SystemComponents.Network } } - private void AccessPoint_OnConnectCompleted(string name, bool success) + private void ConnectionAttemptCompleted(string name, bool success) { lock (@lock) { @@ -129,12 +144,28 @@ namespace SafeExamBrowser.SystemComponents.Network } } + private bool TryGetCredentials(AuthRequest request) + { + var args = new CredentialsRequiredEventArgs(); + + CredentialsRequired?.Invoke(args); + + if (args.Success) + { + request.Password = args.Password; + request.Username = args.Username; + } + + return args.Success; + } + private void Update() { try { lock (@lock) { + var current = default(WirelessNetwork); var hasInternet = nativeMethods.HasInternetConnection(); var hasWireless = !wifi.NoWifiAvailable && !IsTurnedOff(); var isConnecting = Status == ConnectionStatus.Connecting; @@ -144,12 +175,13 @@ namespace SafeExamBrowser.SystemComponents.Network if (hasWireless) { - foreach (var accessPoint in wifi.GetAccessPoints()) + foreach (var wirelessNetwork in wifi.GetAccessPoints().Select(a => ToWirelessNetwork(a))) { - // The user may only connect to an already configured or connected wireless network! - if (accessPoint.HasProfile || accessPoint.IsConnected) + wirelessNetworks.Add(wirelessNetwork); + + if (wirelessNetwork.Status == ConnectionStatus.Connected) { - wirelessNetworks.Add(ToWirelessNetwork(accessPoint)); + current = wirelessNetwork; } } } @@ -159,7 +191,7 @@ namespace SafeExamBrowser.SystemComponents.Network if (previousStatus != ConnectionStatus.Connected && Status == ConnectionStatus.Connected) { - logger.Info("Connection established."); + logger.Info($"Connection established ({Type}{(current != default ? $", {current.Name}" : "")})."); } if (previousStatus != ConnectionStatus.Disconnected && Status == ConnectionStatus.Disconnected) diff --git a/SafeExamBrowser.SystemComponents/Network/WirelessNetwork.cs b/SafeExamBrowser.SystemComponents/Network/WirelessNetwork.cs index 1c7e528b..35bce4cf 100644 --- a/SafeExamBrowser.SystemComponents/Network/WirelessNetwork.cs +++ b/SafeExamBrowser.SystemComponents/Network/WirelessNetwork.cs @@ -6,7 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -using System; using SafeExamBrowser.SystemComponents.Contracts.Network; using SimpleWifi; @@ -16,14 +15,8 @@ namespace SafeExamBrowser.SystemComponents.Network { internal AccessPoint AccessPoint { get; set; } - public Guid Id { get; } public string Name { get; set; } public int SignalStrength { get; set; } public ConnectionStatus Status { get; set; } - - public WirelessNetwork() - { - Id = Guid.NewGuid(); - } } } diff --git a/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs index cc49412e..94825649 100644 --- a/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs @@ -83,6 +83,11 @@ namespace SafeExamBrowser.UserInterface.Contracts /// ISystemControl CreateNetworkControl(INetworkAdapter adapter, Location location); + /// + /// Creates a network dialog with the given message and title. + /// + INetworkDialog CreateNetworkDialog(string message, string title); + /// /// Creates a notification control for the given notification, initialized for the specified location. /// diff --git a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj index 24306ed0..698d2010 100644 --- a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj +++ b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj @@ -96,11 +96,13 @@ + + diff --git a/SafeExamBrowser.UserInterface.Contracts/Windows/Data/NetworkDialogResult.cs b/SafeExamBrowser.UserInterface.Contracts/Windows/Data/NetworkDialogResult.cs new file mode 100644 index 00000000..439814d4 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Contracts/Windows/Data/NetworkDialogResult.cs @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 ETH Zürich, IT Services + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +namespace SafeExamBrowser.UserInterface.Contracts.Windows.Data +{ + /// + /// Defines the user interaction result of an . + /// + public class NetworkDialogResult + { + /// + /// The password entered by the user, or default(string) if the interaction was unsuccessful. + /// + public string Password { get; set; } + + /// + /// Indicates whether the user confirmed the dialog or not. + /// + public bool Success { get; set; } + + /// + /// The username entered by the user, or default(string) if no username is required or the interaction was unsuccessful. + /// + public string Username { get; set; } + } +} diff --git a/SafeExamBrowser.UserInterface.Contracts/Windows/INetworkDialog.cs b/SafeExamBrowser.UserInterface.Contracts/Windows/INetworkDialog.cs new file mode 100644 index 00000000..a5c347e5 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Contracts/Windows/INetworkDialog.cs @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 ETH Zürich, IT Services + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using SafeExamBrowser.UserInterface.Contracts.Windows.Data; + +namespace SafeExamBrowser.UserInterface.Contracts.Windows +{ + /// + /// Defines the functionality of a network dialog. + /// + public interface INetworkDialog : IWindow + { + /// + /// Shows the dialog as topmost window. If a parent window is specified, the dialog is rendered modally for the given parent. + /// + NetworkDialogResult Show(IWindow parent = null); + } +} diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/NetworkControl.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/NetworkControl.xaml.cs index 554e171d..72c03e1c 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/NetworkControl.xaml.cs +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/NetworkControl.xaml.cs @@ -100,7 +100,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.Controls.ActionCenter { var button = new NetworkButton(network); - button.NetworkSelected += (o, args) => adapter.ConnectToWirelessNetwork(network.Id); + button.NetworkSelected += (o, args) => adapter.ConnectToWirelessNetwork(network.Name); if (network.Status == ConnectionStatus.Connected) { diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/NetworkControl.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/NetworkControl.xaml.cs index 90604710..8f058b05 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/NetworkControl.xaml.cs +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/NetworkControl.xaml.cs @@ -112,7 +112,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.Controls.Taskbar { var button = new NetworkButton(network); - button.NetworkSelected += (o, args) => adapter.ConnectToWirelessNetwork(network.Id); + button.NetworkSelected += (o, args) => adapter.ConnectToWirelessNetwork(network.Name); if (network.Status == ConnectionStatus.Connected) { diff --git a/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj b/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj index 9667b599..e9e76f25 100644 --- a/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj +++ b/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj @@ -168,6 +168,9 @@ LogWindow.xaml + + NetworkDialog.xaml + PasswordDialog.xaml @@ -394,6 +397,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs index cfc4ff22..597e7840 100644 --- a/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs @@ -143,6 +143,11 @@ namespace SafeExamBrowser.UserInterface.Desktop } } + public INetworkDialog CreateNetworkDialog(string message, string title) + { + return Application.Current.Dispatcher.Invoke(() => new NetworkDialog(message, title, text)); + } + public INotificationControl CreateNotificationControl(INotification notification, Location location) { if (location == Location.ActionCenter) diff --git a/SafeExamBrowser.UserInterface.Desktop/Windows/NetworkDialog.xaml b/SafeExamBrowser.UserInterface.Desktop/Windows/NetworkDialog.xaml new file mode 100644 index 00000000..29a4c611 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Desktop/Windows/NetworkDialog.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +