diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..9a0b2971 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# Defines coding style deviations from the Microsoft Minimum Recommended Rules ruleset, which is active per default in Visual Studio 2017 +# For more info, see https://editorconfig.org/ and https://docs.microsoft.com/en-us/visualstudio/ide/create-portable-custom-editor-options + +root = true + +[*] +end_of_line = crlf + +[*.cs] +dotnet_style_object_initializer = false:none +indent_style = tab + +[*.xml] +indent_style = space \ No newline at end of file diff --git a/SafeExamBrowser.Browser/BrowserApplicationController.cs b/SafeExamBrowser.Browser/BrowserApplicationController.cs index 1e09c6e1..b71c1755 100644 --- a/SafeExamBrowser.Browser/BrowserApplicationController.cs +++ b/SafeExamBrowser.Browser/BrowserApplicationController.cs @@ -8,12 +8,10 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; using CefSharp; using SafeExamBrowser.Browser.Handlers; -using SafeExamBrowser.Contracts.Behaviour; -using SafeExamBrowser.Contracts.Communication.Proxies; +using SafeExamBrowser.Contracts.Browser; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.I18n; using SafeExamBrowser.Contracts.Logging; @@ -24,30 +22,30 @@ using BrowserSettings = SafeExamBrowser.Contracts.Configuration.Settings.Browser namespace SafeExamBrowser.Browser { - public class BrowserApplicationController : IApplicationController + public class BrowserApplicationController : IBrowserApplicationController { private IApplicationButton button; - private IList instances = new List(); - private BrowserSettings settings; + private IList instances; private ILogger logger; private IMessageBox messageBox; - private IRuntimeProxy runtime; private RuntimeInfo runtimeInfo; - private IUserInterfaceFactory uiFactory; + private BrowserSettings settings; private IText text; + private IUserInterfaceFactory uiFactory; + + public event DownloadRequestedEventHandler ConfigurationDownloadRequested; public BrowserApplicationController( BrowserSettings settings, RuntimeInfo runtimeInfo, ILogger logger, IMessageBox messageBox, - IRuntimeProxy runtime, IText text, IUserInterfaceFactory uiFactory) { + this.instances = new List(); this.logger = logger; this.messageBox = messageBox; - this.runtime = runtime; this.runtimeInfo = runtimeInfo; this.settings = settings; this.text = text; @@ -84,15 +82,14 @@ namespace SafeExamBrowser.Browser private void CreateNewInstance() { - var instance = new BrowserApplicationInstance(settings, text, uiFactory, instances.Count == 0); + var instance = new BrowserApplicationInstance(settings, runtimeInfo, text, uiFactory, instances.Count == 0); instance.Initialize(); - instance.ConfigurationDetected += Instance_ConfigurationDetected; + instance.ConfigurationDownloadRequested += (fileName, args) => ConfigurationDownloadRequested?.Invoke(fileName, args); instance.Terminated += Instance_Terminated; button.RegisterInstance(instance); instances.Add(instance); - instance.Window.Show(); } @@ -124,36 +121,6 @@ namespace SafeExamBrowser.Browser } } - private void Instance_ConfigurationDetected(string url, CancelEventArgs args) - { - var result = messageBox.Show(TextKey.MessageBox_ReconfigurationQuestion, TextKey.MessageBox_ReconfigurationQuestionTitle, MessageBoxAction.YesNo, MessageBoxIcon.Question); - var reconfigure = result == MessageBoxResult.Yes; - var allowed = false; - - logger.Info($"Detected configuration request for '{url}'. The user chose to {(reconfigure ? "start" : "abort")} the reconfiguration."); - - if (reconfigure) - { - try - { - allowed = runtime.RequestReconfiguration(url); - logger.Info($"The runtime {(allowed ? "accepted" : "denied")} the reconfiguration request."); - - if (!allowed) - { - messageBox.Show(TextKey.MessageBox_ReconfigurationDenied, TextKey.MessageBox_ReconfigurationDeniedTitle); - } - } - catch (Exception e) - { - logger.Error("Failed to communicate the reconfiguration request to the runtime!", e); - messageBox.Show(TextKey.MessageBox_ReconfigurationError, TextKey.MessageBox_ReconfigurationErrorTitle, icon: MessageBoxIcon.Error); - } - } - - args.Cancel = !allowed; - } - private void Instance_Terminated(Guid id) { instances.Remove(instances.FirstOrDefault(i => i.Id == id)); diff --git a/SafeExamBrowser.Browser/BrowserApplicationInstance.cs b/SafeExamBrowser.Browser/BrowserApplicationInstance.cs index 7a686ca9..1b9aa6e3 100644 --- a/SafeExamBrowser.Browser/BrowserApplicationInstance.cs +++ b/SafeExamBrowser.Browser/BrowserApplicationInstance.cs @@ -8,6 +8,7 @@ using System; using SafeExamBrowser.Browser.Handlers; +using SafeExamBrowser.Contracts.Browser; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.I18n; @@ -22,6 +23,7 @@ namespace SafeExamBrowser.Browser private IBrowserControl control; private IBrowserWindow window; private bool isMainInstance; + private RuntimeInfo runtimeInfo; private BrowserSettings settings; private IText text; private IUserInterfaceFactory uiFactory; @@ -30,13 +32,19 @@ namespace SafeExamBrowser.Browser public string Name { get; private set; } public IWindow Window { get { return window; } } - internal event ConfigurationDetectedEventHandler ConfigurationDetected; - public event TerminatedEventHandler Terminated; + public event DownloadRequestedEventHandler ConfigurationDownloadRequested; public event NameChangedEventHandler NameChanged; + public event TerminatedEventHandler Terminated; - public BrowserApplicationInstance(BrowserSettings settings, IText text, IUserInterfaceFactory uiFactory, bool isMainInstance) + public BrowserApplicationInstance( + BrowserSettings settings, + RuntimeInfo runtimeInfo, + IText text, + IUserInterfaceFactory uiFactory, + bool isMainInstance) { this.isMainInstance = isMainInstance; + this.runtimeInfo = runtimeInfo; this.settings = settings; this.text = text; this.uiFactory = uiFactory; @@ -44,13 +52,17 @@ namespace SafeExamBrowser.Browser internal void Initialize() { + var downloadHandler = new DownloadHandler(settings, runtimeInfo); + Id = Guid.NewGuid(); + downloadHandler.ConfigurationDownloadRequested += (fileName, args) => ConfigurationDownloadRequested?.Invoke(fileName, args); control = new BrowserControl(settings, text); control.AddressChanged += Control_AddressChanged; - (control as BrowserControl).ConfigurationDetected += (url, args) => ConfigurationDetected?.Invoke(url, args); control.LoadingStateChanged += Control_LoadingStateChanged; control.TitleChanged += Control_TitleChanged; + (control as BrowserControl).DownloadHandler = downloadHandler; + (control as BrowserControl).Initialize(); window = uiFactory.CreateBrowserWindow(control, settings); window.IsMainWindow = isMainInstance; diff --git a/SafeExamBrowser.Browser/BrowserControl.cs b/SafeExamBrowser.Browser/BrowserControl.cs index 37fc0153..1a1daa0d 100644 --- a/SafeExamBrowser.Browser/BrowserControl.cs +++ b/SafeExamBrowser.Browser/BrowserControl.cs @@ -24,8 +24,6 @@ namespace SafeExamBrowser.Browser private LoadingStateChangedEventHandler loadingStateChanged; private TitleChangedEventHandler titleChanged; - internal event ConfigurationDetectedEventHandler ConfigurationDetected; - event AddressChangedEventHandler IBrowserControl.AddressChanged { add { addressChanged += value; } @@ -48,8 +46,17 @@ namespace SafeExamBrowser.Browser { this.settings = settings; this.text = text; + } - Initialize(); + public void Initialize() + { + AddressChanged += (o, args) => addressChanged?.Invoke(args.Address); + LoadingStateChanged += (o, args) => loadingStateChanged?.Invoke(args.IsLoading); + TitleChanged += (o, args) => titleChanged?.Invoke(args.Title); + + KeyboardHandler = new KeyboardHandler(settings); + MenuHandler = new ContextMenuHandler(settings, text); + RequestHandler = new RequestHandler(); } public void NavigateBackwards() @@ -74,19 +81,5 @@ namespace SafeExamBrowser.Browser { GetBrowser().Reload(); } - - private void Initialize() - { - var requestHandler = new RequestHandler(); - - AddressChanged += (o, args) => addressChanged?.Invoke(args.Address); - LoadingStateChanged += (o, args) => loadingStateChanged?.Invoke(args.IsLoading); - TitleChanged += (o, args) => titleChanged?.Invoke(args.Title); - requestHandler.ConfigurationDetected += (url, args) => ConfigurationDetected?.Invoke(url, args); - - KeyboardHandler = new KeyboardHandler(settings); - MenuHandler = new ContextMenuHandler(settings, text); - RequestHandler = requestHandler; - } } } diff --git a/SafeExamBrowser.Browser/Handlers/DownloadHandler.cs b/SafeExamBrowser.Browser/Handlers/DownloadHandler.cs new file mode 100644 index 00000000..f5366503 --- /dev/null +++ b/SafeExamBrowser.Browser/Handlers/DownloadHandler.cs @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * 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 System; +using System.Collections.Concurrent; +using System.IO; +using System.Threading.Tasks; +using CefSharp; +using SafeExamBrowser.Contracts.Browser; +using SafeExamBrowser.Contracts.Configuration; +using BrowserSettings = SafeExamBrowser.Contracts.Configuration.Settings.BrowserSettings; + +namespace SafeExamBrowser.Browser.Handlers +{ + /// + /// See https://cefsharp.github.io/api/63.0.0/html/T_CefSharp_IDownloadHandler.htm. + /// + internal class DownloadHandler : IDownloadHandler + { + private BrowserSettings settings; + private RuntimeInfo runtimeInfo; + private ConcurrentDictionary callbacks; + + public event DownloadRequestedEventHandler ConfigurationDownloadRequested; + + public DownloadHandler(BrowserSettings settings, RuntimeInfo runtimeInfo) + { + this.callbacks = new ConcurrentDictionary(); + this.settings = settings; + this.runtimeInfo = runtimeInfo; + } + + public void OnBeforeDownload(IBrowser browser, DownloadItem downloadItem, IBeforeDownloadCallback callback) + { + var uri = new Uri(downloadItem.Url); + var extension = Path.GetExtension(uri.AbsolutePath); + var isConfigFile = String.Equals(extension, runtimeInfo.ConfigurationFileExtension, StringComparison.InvariantCultureIgnoreCase); + + if (isConfigFile) + { + Task.Run(() => RequestConfigurationFileDownload(downloadItem, callback)); + } + else if (!isConfigFile && settings.AllowDownloads) + { + using (callback) + { + callback.Continue(null, true); + } + } + } + + public void OnDownloadUpdated(IBrowser browser, DownloadItem downloadItem, IDownloadItemCallback callback) + { + if (downloadItem.IsComplete || downloadItem.IsCancelled) + { + if (callbacks.TryRemove(downloadItem.Id, out DownloadFinishedCallback finished) && finished != null) + { + Task.Run(() => finished.Invoke(downloadItem.IsComplete, downloadItem.FullPath)); + } + } + } + + private void RequestConfigurationFileDownload(DownloadItem downloadItem, IBeforeDownloadCallback callback) + { + var args = new DownloadEventArgs(); + + ConfigurationDownloadRequested?.Invoke(downloadItem.SuggestedFileName, args); + + if (args.AllowDownload) + { + if (args.Callback != null) + { + callbacks[downloadItem.Id] = args.Callback; + } + + using (callback) + { + callback.Continue(args.DownloadPath, false); + } + } + } + } +} diff --git a/SafeExamBrowser.Browser/Handlers/RequestHandler.cs b/SafeExamBrowser.Browser/Handlers/RequestHandler.cs index 55f91fbe..6386b61a 100644 --- a/SafeExamBrowser.Browser/Handlers/RequestHandler.cs +++ b/SafeExamBrowser.Browser/Handlers/RequestHandler.cs @@ -7,46 +7,31 @@ */ using System; -using System.ComponentModel; -using System.IO; -using System.Threading.Tasks; using CefSharp; using CefSharp.Handler; namespace SafeExamBrowser.Browser.Handlers { - internal delegate void ConfigurationDetectedEventHandler(string url, CancelEventArgs args); - /// /// See https://cefsharp.github.io/api/63.0.0/html/T_CefSharp_Handler_DefaultRequestHandler.htm. /// internal class RequestHandler : DefaultRequestHandler { - internal event ConfigurationDetectedEventHandler ConfigurationDetected; - public override CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) { - Task.Run(() => + var uri = new Uri(request.Url); + + // TODO: Move to globals -> SafeExamBrowserUriScheme, SafeExamBrowserSecureUriScheme + if (uri.Scheme == "seb") { - var allow = true; - var uri = new Uri(request.Url); + request.Url = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.ToString(); + } + else if (uri.Scheme == "sebs") + { + request.Url = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttps }.ToString(); + } - if (uri.Scheme == "seb" || uri.Scheme == "sebs" || Path.HasExtension("seb")) - { - var args = new CancelEventArgs(); - - ConfigurationDetected?.Invoke(request.Url, args); - - allow = !args.Cancel; - } - - using (callback) - { - callback.Continue(allow); - } - }); - - return CefReturnValue.ContinueAsync; + return base.OnBeforeResourceLoad(browserControl, browser, frame, request, callback); } } } diff --git a/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj b/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj index 6296a2b8..0e656b81 100644 --- a/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj +++ b/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj @@ -51,7 +51,10 @@ pdbonly x86 prompt - MinimumRecommendedRules.ruleset + + + + @@ -67,6 +70,7 @@ Component + diff --git a/SafeExamBrowser.Client.UnitTests/SafeExamBrowser.Client.UnitTests.csproj b/SafeExamBrowser.Client.UnitTests/SafeExamBrowser.Client.UnitTests.csproj index 2c7a4b34..48c27aad 100644 --- a/SafeExamBrowser.Client.UnitTests/SafeExamBrowser.Client.UnitTests.csproj +++ b/SafeExamBrowser.Client.UnitTests/SafeExamBrowser.Client.UnitTests.csproj @@ -53,7 +53,6 @@ pdbonly x86 prompt - MinimumRecommendedRules.ruleset diff --git a/SafeExamBrowser.Client/Behaviour/ClientController.cs b/SafeExamBrowser.Client/Behaviour/ClientController.cs index f8dec7fb..d1fae0ac 100644 --- a/SafeExamBrowser.Client/Behaviour/ClientController.cs +++ b/SafeExamBrowser.Client/Behaviour/ClientController.cs @@ -7,8 +7,10 @@ */ using System; +using System.IO; using SafeExamBrowser.Contracts.Behaviour; using SafeExamBrowser.Contracts.Behaviour.OperationModel; +using SafeExamBrowser.Contracts.Browser; using SafeExamBrowser.Contracts.Communication.Hosts; using SafeExamBrowser.Contracts.Communication.Proxies; using SafeExamBrowser.Contracts.Configuration; @@ -38,6 +40,7 @@ namespace SafeExamBrowser.Client.Behaviour private IWindowMonitor windowMonitor; private RuntimeInfo runtimeInfo; + public IBrowserApplicationController Browser { private get; set; } public IClientHost ClientHost { private get; set; } public Guid SessionId { private get; set; } public Settings Settings { private get; set; } @@ -145,6 +148,7 @@ namespace SafeExamBrowser.Client.Behaviour private void RegisterEvents() { + Browser.ConfigurationDownloadRequested += Browser_ConfigurationDownloadRequested; ClientHost.Shutdown += ClientHost_Shutdown; displayMonitor.DisplayChanged += DisplayMonitor_DisplaySettingsChanged; processMonitor.ExplorerStarted += ProcessMonitor_ExplorerStarted; @@ -155,6 +159,7 @@ namespace SafeExamBrowser.Client.Behaviour private void DeregisterEvents() { + Browser.ConfigurationDownloadRequested -= Browser_ConfigurationDownloadRequested; ClientHost.Shutdown -= ClientHost_Shutdown; displayMonitor.DisplayChanged -= DisplayMonitor_DisplaySettingsChanged; processMonitor.ExplorerStarted -= ProcessMonitor_ExplorerStarted; @@ -183,6 +188,53 @@ namespace SafeExamBrowser.Client.Behaviour logger.Info("Desktop successfully restored."); } + private void Browser_ConfigurationDownloadRequested(string fileName, DownloadEventArgs args) + { + if (Settings.ConfigurationMode == ConfigurationMode.ConfigureClient) + { + logger.Info($"Detected download request for configuration file '{fileName}'."); + + var result = messageBox.Show(TextKey.MessageBox_ReconfigurationQuestion, TextKey.MessageBox_ReconfigurationQuestionTitle, MessageBoxAction.YesNo, MessageBoxIcon.Question); + var reconfigure = result == MessageBoxResult.Yes; + + logger.Info($"The user chose to {(reconfigure ? "start" : "abort")} the reconfiguration."); + + if (reconfigure) + { + args.AllowDownload = true; + args.Callback = Browser_ConfigurationDownloadFinished; + args.DownloadPath = Path.Combine(runtimeInfo.DownloadDirectory, fileName); + } + } + else + { + logger.Info($"Denied download request for configuration file '{fileName}' due to '{Settings.ConfigurationMode}' mode."); + messageBox.Show(TextKey.MessageBox_ReconfigurationDenied, TextKey.MessageBox_ReconfigurationDeniedTitle); + } + } + + private void Browser_ConfigurationDownloadFinished(bool success, string filePath = null) + { + if (success) + { + try + { + runtime.RequestReconfiguration(filePath); + logger.Info($"Sent reconfiguration request for '{filePath}' to the runtime."); + } + catch (Exception e) + { + logger.Error($"Failed to communicate reconfiguration request for '{filePath}'!", e); + messageBox.Show(TextKey.MessageBox_ReconfigurationError, TextKey.MessageBox_ReconfigurationErrorTitle, icon: MessageBoxIcon.Error); + } + } + else + { + logger.Error($"Failed to download configuration file '{filePath}'!"); + messageBox.Show(TextKey.MessageBox_ConfigurationDownloadError, TextKey.MessageBox_ConfigurationDownloadErrorTitle, icon: MessageBoxIcon.Error); + } + } + private void ClientHost_Shutdown() { taskbar.Close(); diff --git a/SafeExamBrowser.Client/Communication/ClientHost.cs b/SafeExamBrowser.Client/Communication/ClientHost.cs index 4f62cf73..b9445f61 100644 --- a/SafeExamBrowser.Client/Communication/ClientHost.cs +++ b/SafeExamBrowser.Client/Communication/ClientHost.cs @@ -7,8 +7,8 @@ */ using System; -using SafeExamBrowser.Contracts.Communication; using SafeExamBrowser.Contracts.Communication.Data; +using SafeExamBrowser.Contracts.Communication.Events; using SafeExamBrowser.Contracts.Communication.Hosts; using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Core.Communication.Hosts; diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs index 062224e9..1ecfc8b9 100644 --- a/SafeExamBrowser.Client/CompositionRoot.cs +++ b/SafeExamBrowser.Client/CompositionRoot.cs @@ -17,6 +17,7 @@ using SafeExamBrowser.Client.Notifications; using SafeExamBrowser.Configuration; using SafeExamBrowser.Contracts.Behaviour; using SafeExamBrowser.Contracts.Behaviour.OperationModel; +using SafeExamBrowser.Contracts.Browser; using SafeExamBrowser.Contracts.Communication.Hosts; using SafeExamBrowser.Contracts.Communication.Proxies; using SafeExamBrowser.Contracts.Configuration; @@ -47,6 +48,7 @@ namespace SafeExamBrowser.Client private string runtimeHostUri; private Guid startupToken; + private IBrowserApplicationController browserController; private ClientConfiguration configuration; private IClientHost clientHost; private ILogger logger; @@ -88,7 +90,6 @@ namespace SafeExamBrowser.Client operations.Enqueue(new RuntimeConnectionOperation(logger, runtimeProxy, startupToken)); operations.Enqueue(new ConfigurationOperation(configuration, logger, runtimeProxy)); operations.Enqueue(new DelayedInitializationOperation(BuildCommunicationHostOperation)); - operations.Enqueue(new DelegateOperation(UpdateClientControllerDependencies)); // TODO //operations.Enqueue(new DelayedInitializationOperation(BuildKeyboardInterceptorOperation)); //operations.Enqueue(new WindowMonitorOperation(logger, windowMonitor)); @@ -98,6 +99,7 @@ namespace SafeExamBrowser.Client operations.Enqueue(new DelayedInitializationOperation(BuildBrowserOperation)); operations.Enqueue(new ClipboardOperation(logger, nativeMethods)); //operations.Enqueue(new DelayedInitializationOperation(BuildMouseInterceptorOperation)); + operations.Enqueue(new DelegateOperation(UpdateClientControllerDependencies)); var sequence = new OperationSequence(logger, operations); @@ -150,10 +152,12 @@ namespace SafeExamBrowser.Client private IOperation BuildBrowserOperation() { var moduleLogger = new ModuleLogger(logger, typeof(BrowserApplicationController)); - var browserController = new BrowserApplicationController(configuration.Settings.Browser, configuration.RuntimeInfo, moduleLogger, messageBox, runtimeProxy, text, uiFactory); + var browserController = new BrowserApplicationController(configuration.Settings.Browser, configuration.RuntimeInfo, moduleLogger, messageBox, text, uiFactory); var browserInfo = new BrowserApplicationInfo(); var operation = new BrowserOperation(browserController, browserInfo, logger, Taskbar, uiFactory); + this.browserController = browserController; + return operation; } @@ -200,6 +204,7 @@ namespace SafeExamBrowser.Client private void UpdateClientControllerDependencies() { + ClientController.Browser = browserController; ClientController.ClientHost = clientHost; ClientController.RuntimeInfo = configuration.RuntimeInfo; ClientController.SessionId = configuration.SessionId; diff --git a/SafeExamBrowser.Client/SafeExamBrowser.Client.csproj b/SafeExamBrowser.Client/SafeExamBrowser.Client.csproj index 805099b1..86f6b40c 100644 --- a/SafeExamBrowser.Client/SafeExamBrowser.Client.csproj +++ b/SafeExamBrowser.Client/SafeExamBrowser.Client.csproj @@ -44,8 +44,8 @@ full x86 prompt - MinimumRecommendedRules.ruleset true + MinimumRecommendedRules.ruleset bin\x86\Release\ @@ -54,7 +54,6 @@ pdbonly x86 prompt - MinimumRecommendedRules.ruleset true diff --git a/SafeExamBrowser.Configuration/ConfigurationRepository.cs b/SafeExamBrowser.Configuration/ConfigurationRepository.cs index c4b830b3..83a6b693 100644 --- a/SafeExamBrowser.Configuration/ConfigurationRepository.cs +++ b/SafeExamBrowser.Configuration/ConfigurationRepository.cs @@ -23,7 +23,7 @@ namespace SafeExamBrowser.Configuration public ISessionData CurrentSession { get; private set; } public Settings CurrentSettings { get; private set; } - public string ReconfigurationUrl { get; set; } + public string ReconfigurationFilePath { get; set; } public RuntimeInfo RuntimeInfo { @@ -66,35 +66,34 @@ namespace SafeExamBrowser.Configuration } } - public Settings LoadSettings(Uri path) + public LoadStatus LoadSettings(Uri resource, string settingsPassword = null, string adminPassword = null) { // TODO: Implement loading mechanism - return LoadDefaultSettings(); + LoadDefaultSettings(); + + return LoadStatus.Success; } - public Settings LoadDefaultSettings() + public void LoadDefaultSettings() { - var settings = new Settings() - { - // TODO: Implement default settings - ServicePolicy = ServicePolicy.Optional - }; + // TODO: Implement default settings - settings.Browser.StartUrl = "https://www.safeexambrowser.org/testing"; - settings.Browser.AllowAddressBar = true; - settings.Browser.AllowBackwardNavigation = true; - settings.Browser.AllowDeveloperConsole = true; - settings.Browser.AllowForwardNavigation = true; - settings.Browser.AllowReloading = true; + CurrentSettings = new Settings(); - settings.Taskbar.AllowApplicationLog = true; - settings.Taskbar.AllowKeyboardLayout = true; - settings.Taskbar.AllowWirelessNetwork = true; + CurrentSettings.ServicePolicy = ServicePolicy.Optional; - CurrentSettings = settings; + CurrentSettings.Browser.StartUrl = "https://www.safeexambrowser.org/testing"; + CurrentSettings.Browser.AllowAddressBar = true; + CurrentSettings.Browser.AllowBackwardNavigation = true; + CurrentSettings.Browser.AllowDeveloperConsole = true; + CurrentSettings.Browser.AllowForwardNavigation = true; + CurrentSettings.Browser.AllowReloading = true; + CurrentSettings.Browser.AllowDownloads = true; - return settings; + CurrentSettings.Taskbar.AllowApplicationLog = true; + CurrentSettings.Taskbar.AllowKeyboardLayout = true; + CurrentSettings.Taskbar.AllowWirelessNetwork = true; } private void InitializeRuntimeInfo() @@ -105,26 +104,26 @@ namespace SafeExamBrowser.Configuration var logFolder = Path.Combine(appDataFolder, "Logs"); var logFilePrefix = startTime.ToString("yyyy-MM-dd\\_HH\\hmm\\mss\\s"); - runtimeInfo = new RuntimeInfo - { - ApplicationStartTime = startTime, - AppDataFolder = appDataFolder, - BrowserCachePath = Path.Combine(appDataFolder, "Cache"), - BrowserLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Browser.txt"), - ClientId = Guid.NewGuid(), - ClientAddress = $"{BASE_ADDRESS}/client/{Guid.NewGuid()}", - ClientExecutablePath = Path.Combine(Path.GetDirectoryName(executable.Location), $"{nameof(SafeExamBrowser)}.Client.exe"), - ClientLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Client.txt"), - DefaultSettingsFileName = "SebClientSettings.seb", - ProgramCopyright = executable.GetCustomAttribute().Copyright, - ProgramDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), nameof(SafeExamBrowser)), - ProgramTitle = executable.GetCustomAttribute().Title, - ProgramVersion = executable.GetCustomAttribute().InformationalVersion, - RuntimeId = Guid.NewGuid(), - RuntimeAddress = $"{BASE_ADDRESS}/runtime/{Guid.NewGuid()}", - RuntimeLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Runtime.txt"), - ServiceAddress = $"{BASE_ADDRESS}/service" - }; + runtimeInfo = new RuntimeInfo(); + runtimeInfo.ApplicationStartTime = startTime; + runtimeInfo.AppDataFolder = appDataFolder; + runtimeInfo.BrowserCachePath = Path.Combine(appDataFolder, "Cache"); + runtimeInfo.BrowserLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Browser.txt"); + runtimeInfo.ClientId = Guid.NewGuid(); + runtimeInfo.ClientAddress = $"{BASE_ADDRESS}/client/{Guid.NewGuid()}"; + runtimeInfo.ClientExecutablePath = Path.Combine(Path.GetDirectoryName(executable.Location), $"{nameof(SafeExamBrowser)}.Client.exe"); + runtimeInfo.ClientLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Client.txt"); + runtimeInfo.ConfigurationFileExtension = ".seb"; + runtimeInfo.DefaultSettingsFileName = "SebClientSettings.seb"; + runtimeInfo.DownloadDirectory = Path.Combine(appDataFolder, "Downloads"); + runtimeInfo.ProgramCopyright = executable.GetCustomAttribute().Copyright; + runtimeInfo.ProgramDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), nameof(SafeExamBrowser)); + runtimeInfo.ProgramTitle = executable.GetCustomAttribute().Title; + runtimeInfo.ProgramVersion = executable.GetCustomAttribute().InformationalVersion; + runtimeInfo.RuntimeId = Guid.NewGuid(); + runtimeInfo.RuntimeAddress = $"{BASE_ADDRESS}/runtime/{Guid.NewGuid()}"; + runtimeInfo.RuntimeLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Runtime.txt"); + runtimeInfo.ServiceAddress = $"{BASE_ADDRESS}/service"; } private void UpdateRuntimeInfo() diff --git a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj index 9462738c..877e2d13 100644 --- a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj +++ b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj @@ -45,7 +45,6 @@ pdbonly x86 prompt - MinimumRecommendedRules.ruleset diff --git a/SafeExamBrowser.Contracts/Behaviour/IClientController.cs b/SafeExamBrowser.Contracts/Behaviour/IClientController.cs index 0c824d28..c56377f0 100644 --- a/SafeExamBrowser.Contracts/Behaviour/IClientController.cs +++ b/SafeExamBrowser.Contracts/Behaviour/IClientController.cs @@ -7,6 +7,7 @@ */ using System; +using SafeExamBrowser.Contracts.Browser; using SafeExamBrowser.Contracts.Communication.Hosts; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Configuration.Settings; @@ -18,6 +19,11 @@ namespace SafeExamBrowser.Contracts.Behaviour /// public interface IClientController { + /// + /// The controller for the browser application. + /// + IBrowserApplicationController Browser { set; } + /// /// The client host used for communication handling. /// diff --git a/SafeExamBrowser.Contracts/Browser/DownloadEventArgs.cs b/SafeExamBrowser.Contracts/Browser/DownloadEventArgs.cs new file mode 100644 index 00000000..ddda083a --- /dev/null +++ b/SafeExamBrowser.Contracts/Browser/DownloadEventArgs.cs @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * 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.Contracts.Browser +{ + /// + /// TODO + /// + public class DownloadEventArgs + { + /// + /// Determines whether the specified download is allowed. + /// + public bool AllowDownload { get; set; } + + /// + /// Callback executed once a download has been finished. + /// + public DownloadFinishedCallback Callback { get; set; } + + /// + /// The full path under which the specified file should be saved. + /// + public string DownloadPath { get; set; } + } +} diff --git a/SafeExamBrowser.Contracts/Browser/DownloadFinishedCallback.cs b/SafeExamBrowser.Contracts/Browser/DownloadFinishedCallback.cs new file mode 100644 index 00000000..344dd746 --- /dev/null +++ b/SafeExamBrowser.Contracts/Browser/DownloadFinishedCallback.cs @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * 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.Contracts.Browser +{ + public delegate void DownloadFinishedCallback(bool success, string filePath = null); +} diff --git a/SafeExamBrowser.Contracts/Browser/DownloadRequestedEventHandler.cs b/SafeExamBrowser.Contracts/Browser/DownloadRequestedEventHandler.cs new file mode 100644 index 00000000..a96f730b --- /dev/null +++ b/SafeExamBrowser.Contracts/Browser/DownloadRequestedEventHandler.cs @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * 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.Contracts.Browser +{ + public delegate void DownloadRequestedEventHandler(string fileName, DownloadEventArgs args); +} diff --git a/SafeExamBrowser.Contracts/Browser/IBrowserApplicationController.cs b/SafeExamBrowser.Contracts/Browser/IBrowserApplicationController.cs new file mode 100644 index 00000000..4310302e --- /dev/null +++ b/SafeExamBrowser.Contracts/Browser/IBrowserApplicationController.cs @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * 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.Contracts.Behaviour; + +namespace SafeExamBrowser.Contracts.Browser +{ + /// + /// Controls the lifetime and functionality of the browser application. + /// + public interface IBrowserApplicationController : IApplicationController + { + /// + /// Event fired when the browser application detects a download request for an application configuration file. + /// + event DownloadRequestedEventHandler ConfigurationDownloadRequested; + } +} diff --git a/SafeExamBrowser.Contracts/Communication/Data/ReconfigurationMessage.cs b/SafeExamBrowser.Contracts/Communication/Data/ReconfigurationMessage.cs index cb11e704..800a4c79 100644 --- a/SafeExamBrowser.Contracts/Communication/Data/ReconfigurationMessage.cs +++ b/SafeExamBrowser.Contracts/Communication/Data/ReconfigurationMessage.cs @@ -17,13 +17,13 @@ namespace SafeExamBrowser.Contracts.Communication.Data public class ReconfigurationMessage : Message { /// - /// The locator of the new configuration to be used. + /// The full path of the configuration file to be used. /// - public string ConfigurationUrl { get; private set; } + public string ConfigurationPath { get; private set; } - public ReconfigurationMessage(string url) + public ReconfigurationMessage(string path) { - ConfigurationUrl = url; + ConfigurationPath = path; } } } diff --git a/SafeExamBrowser.Contracts/Communication/Events/CommunicationEventArgs.cs b/SafeExamBrowser.Contracts/Communication/Events/CommunicationEventArgs.cs new file mode 100644 index 00000000..31e7b5f3 --- /dev/null +++ b/SafeExamBrowser.Contracts/Communication/Events/CommunicationEventArgs.cs @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * 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.Contracts.Communication.Events +{ + /// + /// Base class which must be used for all event parameters T of . + /// + public abstract class CommunicationEventArgs + { + } +} diff --git a/SafeExamBrowser.Contracts/Communication/Events/CommunicationEventHandler.cs b/SafeExamBrowser.Contracts/Communication/Events/CommunicationEventHandler.cs new file mode 100644 index 00000000..8526bede --- /dev/null +++ b/SafeExamBrowser.Contracts/Communication/Events/CommunicationEventHandler.cs @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * 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 System.Threading.Tasks; + +namespace SafeExamBrowser.Contracts.Communication.Events +{ + /// + /// The default handler for communication events of an interlocutor. + /// + public delegate void CommunicationEventHandler(); + + /// + /// The handler with parameter for communication events of an interlocutor. + /// + public delegate void CommunicationEventHandler(T args) where T : CommunicationEventArgs; + + public static class CommunicationEventHandlerExtensions + { + /// + /// Executes the event handler asynchronously, i.e. on a separate thread. + /// + public static async Task InvokeAsync(this CommunicationEventHandler handler) + { + await Task.Run(() => handler?.Invoke()); + } + + /// + /// Executes the event handler asynchronously, i.e. on a separate thread. + /// + public static async Task InvokeAsync(this CommunicationEventHandler handler, T args) where T : CommunicationEventArgs + { + await Task.Run(() => handler?.Invoke(args)); + } + } +} diff --git a/SafeExamBrowser.Contracts/Communication/Events/ReconfigurationEventArgs.cs b/SafeExamBrowser.Contracts/Communication/Events/ReconfigurationEventArgs.cs new file mode 100644 index 00000000..62d14257 --- /dev/null +++ b/SafeExamBrowser.Contracts/Communication/Events/ReconfigurationEventArgs.cs @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * 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.Contracts.Communication.Events +{ + /// + /// The event arguments used for the reconfiguration event fired by the . + /// + public class ReconfigurationEventArgs : CommunicationEventArgs + { + /// + /// The full path to the configuration file to be used for reconfiguration. + /// + public string ConfigurationPath { get; set; } + } +} diff --git a/SafeExamBrowser.Contracts/Communication/Hosts/IClientHost.cs b/SafeExamBrowser.Contracts/Communication/Hosts/IClientHost.cs index 62be030d..073bf146 100644 --- a/SafeExamBrowser.Contracts/Communication/Hosts/IClientHost.cs +++ b/SafeExamBrowser.Contracts/Communication/Hosts/IClientHost.cs @@ -7,6 +7,7 @@ */ using System; +using SafeExamBrowser.Contracts.Communication.Events; namespace SafeExamBrowser.Contracts.Communication.Hosts { diff --git a/SafeExamBrowser.Contracts/Communication/Hosts/IRuntimeHost.cs b/SafeExamBrowser.Contracts/Communication/Hosts/IRuntimeHost.cs index 3a7334fd..070a6d1b 100644 --- a/SafeExamBrowser.Contracts/Communication/Hosts/IRuntimeHost.cs +++ b/SafeExamBrowser.Contracts/Communication/Hosts/IRuntimeHost.cs @@ -7,6 +7,7 @@ */ using System; +using SafeExamBrowser.Contracts.Communication.Events; namespace SafeExamBrowser.Contracts.Communication.Hosts { @@ -31,9 +32,9 @@ namespace SafeExamBrowser.Contracts.Communication.Hosts event CommunicationEventHandler ClientReady; /// - /// Event fired when the client detected a reconfiguration request. + /// Event fired when the client requested a reconfiguration of the application. /// - event CommunicationEventHandler ReconfigurationRequested; + event CommunicationEventHandler ReconfigurationRequested; /// /// Event fired when the client requests to shut down the application. diff --git a/SafeExamBrowser.Contracts/Communication/ICommunicationHost.cs b/SafeExamBrowser.Contracts/Communication/ICommunicationHost.cs index 2b116de7..8fcc6ef7 100644 --- a/SafeExamBrowser.Contracts/Communication/ICommunicationHost.cs +++ b/SafeExamBrowser.Contracts/Communication/ICommunicationHost.cs @@ -8,8 +8,6 @@ namespace SafeExamBrowser.Contracts.Communication { - public delegate void CommunicationEventHandler(); - /// /// Defines the common functionality for all communication hosts. A communication host can be hosted by an application component to /// allow for inter-process communication with other components (e.g. runtime -> client communication). diff --git a/SafeExamBrowser.Contracts/Communication/ICommunicationProxy.cs b/SafeExamBrowser.Contracts/Communication/ICommunicationProxy.cs index 152ae784..510d2ded 100644 --- a/SafeExamBrowser.Contracts/Communication/ICommunicationProxy.cs +++ b/SafeExamBrowser.Contracts/Communication/ICommunicationProxy.cs @@ -7,6 +7,7 @@ */ using System; +using SafeExamBrowser.Contracts.Communication.Events; namespace SafeExamBrowser.Contracts.Communication { diff --git a/SafeExamBrowser.Contracts/Communication/Proxies/IRuntimeProxy.cs b/SafeExamBrowser.Contracts/Communication/Proxies/IRuntimeProxy.cs index c9a519ba..7d74e4c0 100644 --- a/SafeExamBrowser.Contracts/Communication/Proxies/IRuntimeProxy.cs +++ b/SafeExamBrowser.Contracts/Communication/Proxies/IRuntimeProxy.cs @@ -34,10 +34,9 @@ namespace SafeExamBrowser.Contracts.Communication.Proxies void RequestShutdown(); /// - /// Requests the runtime to reconfigure the application with the configuration from the given location. Returns true if - /// the runtime accepted the request, otherwise false. + /// Requests the runtime to reconfigure the application with the specified configuration. /// /// If the communication failed. - bool RequestReconfiguration(string url); + void RequestReconfiguration(string filePath); } } diff --git a/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs b/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs index 4e1ca43d..4fd13f9c 100644 --- a/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs +++ b/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs @@ -28,9 +28,9 @@ namespace SafeExamBrowser.Contracts.Configuration Settings.Settings CurrentSettings { get; } /// - /// The locator of the configuration to be used when reconfiguring the application. + /// The path of the settings file to be used when reconfiguring the application. /// - string ReconfigurationUrl { get; set; } + string ReconfigurationFilePath { get; set; } /// /// The runtime information for the currently running application instance. @@ -48,14 +48,14 @@ namespace SafeExamBrowser.Contracts.Configuration void InitializeSessionConfiguration(); /// - /// Attempts to load settings from the specified path. + /// Attempts to load settings from the specified resource, using the optional passwords. Returns a + /// indicating the result of the operation. /// - /// Thrown if the given path cannot be resolved to a settings file. - Settings.Settings LoadSettings(Uri path); + LoadStatus LoadSettings(Uri resource, string settingsPassword = null, string adminPassword = null); /// /// Loads the default settings. /// - Settings.Settings LoadDefaultSettings(); + void LoadDefaultSettings(); } } diff --git a/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs b/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs new file mode 100644 index 00000000..41e823ef --- /dev/null +++ b/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * 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.Contracts.Configuration +{ + /// + /// Defines all possible results of . + /// + public enum LoadStatus + { + /// + /// Indicates that an admin password is needed in order to load the settings. + /// + AdminPasswordNeeded = 1, + + /// + /// Indicates that a resource does not comply with the required data format. + /// + InvalidData, + + /// + /// Indicates that a settings password is needed in order to load the settings. + /// + SettingsPasswordNeeded, + + /// + /// The were loaded successfully. + /// + Success + } +} diff --git a/SafeExamBrowser.Contracts/Configuration/RuntimeInfo.cs b/SafeExamBrowser.Contracts/Configuration/RuntimeInfo.cs index 41581069..87c305b6 100644 --- a/SafeExamBrowser.Contracts/Configuration/RuntimeInfo.cs +++ b/SafeExamBrowser.Contracts/Configuration/RuntimeInfo.cs @@ -12,6 +12,7 @@ namespace SafeExamBrowser.Contracts.Configuration { /// /// Defines the fundamental, global configuration information for all application components. + /// TODO: Rename to Globals or GlobalConfiguration or alike! /// [Serializable] public class RuntimeInfo @@ -56,11 +57,21 @@ namespace SafeExamBrowser.Contracts.Configuration /// public string ClientLogFile { get; set; } + /// + /// The file extension of configuration files for the application (including the period). + /// + public string ConfigurationFileExtension { get; set; } + /// /// The default file name for application settings. /// public string DefaultSettingsFileName { get; set; } + /// + /// The default directory for file downloads. + /// + public string DownloadDirectory { get; set; } + /// /// The copyright information for the application (i.e. the executing assembly). /// diff --git a/SafeExamBrowser.Contracts/Configuration/Settings/BrowserSettings.cs b/SafeExamBrowser.Contracts/Configuration/Settings/BrowserSettings.cs index 393e1762..ba05c3ab 100644 --- a/SafeExamBrowser.Contracts/Configuration/Settings/BrowserSettings.cs +++ b/SafeExamBrowser.Contracts/Configuration/Settings/BrowserSettings.cs @@ -31,6 +31,11 @@ namespace SafeExamBrowser.Contracts.Configuration.Settings /// public bool AllowDeveloperConsole { get; set; } + /// + /// Determines whether the user should be allowed to download files. + /// + public bool AllowDownloads { get; set; } + /// /// Determines whether the user should be allowed to navigate forwards in a browser window. /// diff --git a/SafeExamBrowser.Contracts/I18n/TextKey.cs b/SafeExamBrowser.Contracts/I18n/TextKey.cs index a03e07f3..95d25833 100644 --- a/SafeExamBrowser.Contracts/I18n/TextKey.cs +++ b/SafeExamBrowser.Contracts/I18n/TextKey.cs @@ -20,6 +20,8 @@ namespace SafeExamBrowser.Contracts.I18n MessageBox_ApplicationErrorTitle, MessageBox_ClientConfigurationQuestion, MessageBox_ClientConfigurationQuestionTitle, + MessageBox_ConfigurationDownloadError, + MessageBox_ConfigurationDownloadErrorTitle, MessageBox_Quit, MessageBox_QuitTitle, MessageBox_QuitError, diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj index f4b5c8e8..c726f27d 100644 --- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj +++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj @@ -45,7 +45,6 @@ pdbonly x86 prompt - MinimumRecommendedRules.ruleset @@ -57,6 +56,13 @@ + + + + + + + @@ -84,6 +90,7 @@ + diff --git a/SafeExamBrowser.Core.UnitTests/Communication/Proxies/RuntimeProxyTests.cs b/SafeExamBrowser.Core.UnitTests/Communication/Proxies/RuntimeProxyTests.cs index 7c6a7f9c..7b5ae328 100644 --- a/SafeExamBrowser.Core.UnitTests/Communication/Proxies/RuntimeProxyTests.cs +++ b/SafeExamBrowser.Core.UnitTests/Communication/Proxies/RuntimeProxyTests.cs @@ -96,47 +96,56 @@ namespace SafeExamBrowser.Core.UnitTests.Communication.Proxies [TestMethod] public void MustCorrectlyRequestReconfiguration() { - var url = "sebs://some/url.seb"; - var response = new ReconfigurationResponse - { - Accepted = true - }; + //var url = "sebs://some/url.seb"; + //var response = new ReconfigurationResponse + //{ + // Accepted = true + //}; - proxy.Setup(p => p.Send(It.Is(m => m.ConfigurationUrl == url))).Returns(response); + //proxy.Setup(p => p.Send(It.Is(m => m.ConfigurationUrl == url))).Returns(response); - var accepted = sut.RequestReconfiguration(url); + //var accepted = sut.RequestReconfiguration(url); - proxy.Verify(p => p.Send(It.Is(m => m.ConfigurationUrl == url)), Times.Once); + //proxy.Verify(p => p.Send(It.Is(m => m.ConfigurationUrl == url)), Times.Once); - Assert.IsTrue(accepted); + //Assert.IsTrue(accepted); + + // TODO + Assert.Fail(); } [TestMethod] public void MustCorrectlyHandleDeniedReconfigurationRequest() { - var url = "sebs://some/url.seb"; - var response = new ReconfigurationResponse - { - Accepted = false - }; + //var url = "sebs://some/url.seb"; + //var response = new ReconfigurationResponse + //{ + // Accepted = false + //}; - proxy.Setup(p => p.Send(It.Is(m => m.ConfigurationUrl == url))).Returns(response); + //proxy.Setup(p => p.Send(It.Is(m => m.ConfigurationUrl == url))).Returns(response); - var accepted = sut.RequestReconfiguration(url); + //var accepted = sut.RequestReconfiguration(url); - Assert.IsFalse(accepted); + //Assert.IsFalse(accepted); + + // TODO + Assert.Fail(); } [TestMethod] public void MustNotFailIfIncorrectResponseToReconfigurationRequest() { - var url = "sebs://some/url.seb"; + //var url = "sebs://some/url.seb"; - proxy.Setup(p => p.Send(It.Is(m => m.ConfigurationUrl == url))).Returns(null); + //proxy.Setup(p => p.Send(It.Is(m => m.ConfigurationUrl == url))).Returns(null); - var accepted = sut.RequestReconfiguration(url); + //var accepted = sut.RequestReconfiguration(url); - Assert.IsFalse(accepted); + //Assert.IsFalse(accepted); + + // TODO + Assert.Fail(); } [TestMethod] diff --git a/SafeExamBrowser.Core.UnitTests/SafeExamBrowser.Core.UnitTests.csproj b/SafeExamBrowser.Core.UnitTests/SafeExamBrowser.Core.UnitTests.csproj index 630f0e30..382a44ab 100644 --- a/SafeExamBrowser.Core.UnitTests/SafeExamBrowser.Core.UnitTests.csproj +++ b/SafeExamBrowser.Core.UnitTests/SafeExamBrowser.Core.UnitTests.csproj @@ -53,7 +53,6 @@ pdbonly x86 prompt - MinimumRecommendedRules.ruleset diff --git a/SafeExamBrowser.Core/Communication/Proxies/BaseProxy.cs b/SafeExamBrowser.Core/Communication/Proxies/BaseProxy.cs index 862d2a20..c2817488 100644 --- a/SafeExamBrowser.Core/Communication/Proxies/BaseProxy.cs +++ b/SafeExamBrowser.Core/Communication/Proxies/BaseProxy.cs @@ -11,6 +11,7 @@ using System.ServiceModel; using System.Timers; using SafeExamBrowser.Contracts.Communication; using SafeExamBrowser.Contracts.Communication.Data; +using SafeExamBrowser.Contracts.Communication.Events; using SafeExamBrowser.Contracts.Communication.Proxies; using SafeExamBrowser.Contracts.Logging; diff --git a/SafeExamBrowser.Core/Communication/Proxies/RuntimeProxy.cs b/SafeExamBrowser.Core/Communication/Proxies/RuntimeProxy.cs index cb4afaaa..71ec8a68 100644 --- a/SafeExamBrowser.Core/Communication/Proxies/RuntimeProxy.cs +++ b/SafeExamBrowser.Core/Communication/Proxies/RuntimeProxy.cs @@ -45,16 +45,14 @@ namespace SafeExamBrowser.Core.Communication.Proxies } } - public bool RequestReconfiguration(string url) + public void RequestReconfiguration(string filePath) { - var response = Send(new ReconfigurationMessage(url)); + var response = Send(new ReconfigurationMessage(filePath)); - if (response is ReconfigurationResponse reconfiguration) + if (!IsAcknowledged(response)) { - return reconfiguration.Accepted; + throw new CommunicationException($"Runtime did not acknowledge reconfiguration request! Response: {ToString(response)}."); } - - return false; } public void RequestShutdown() diff --git a/SafeExamBrowser.Core/I18n/Text.xml b/SafeExamBrowser.Core/I18n/Text.xml index accbfecb..2e71af35 100644 --- a/SafeExamBrowser.Core/I18n/Text.xml +++ b/SafeExamBrowser.Core/I18n/Text.xml @@ -18,6 +18,12 @@ Configuration Successful + + Failed to download the new application configuration. Please try again or contact technical support. + + + Download Error + Would you really like to quit the application? diff --git a/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj b/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj index 553c2361..444e494f 100644 --- a/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj +++ b/SafeExamBrowser.Core/SafeExamBrowser.Core.csproj @@ -45,7 +45,6 @@ pdbonly x86 prompt - MinimumRecommendedRules.ruleset diff --git a/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj b/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj index fcbc3c42..a3adfb66 100644 --- a/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj +++ b/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj @@ -45,7 +45,6 @@ pdbonly x86 prompt - MinimumRecommendedRules.ruleset diff --git a/SafeExamBrowser.Runtime.UnitTests/Behaviour/Operations/ConfigurationOperationTests.cs b/SafeExamBrowser.Runtime.UnitTests/Behaviour/Operations/ConfigurationOperationTests.cs index 94f361d4..5ced1c62 100644 --- a/SafeExamBrowser.Runtime.UnitTests/Behaviour/Operations/ConfigurationOperationTests.cs +++ b/SafeExamBrowser.Runtime.UnitTests/Behaviour/Operations/ConfigurationOperationTests.cs @@ -45,8 +45,9 @@ namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations info.AppDataFolder = @"C:\Not\Really\AppData"; info.DefaultSettingsFileName = "SettingsDummy.txt"; info.ProgramDataFolder = @"C:\Not\Really\ProgramData"; - repository.Setup(r => r.LoadSettings(It.IsAny())).Returns(settings); - repository.Setup(r => r.LoadDefaultSettings()).Returns(settings); + // TODO + //repository.Setup(r => r.LoadSettings(It.IsAny())).Returns(settings); + //repository.Setup(r => r.LoadDefaultSettings()).Returns(settings); } [TestMethod] @@ -88,7 +89,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations sut.Perform(); - repository.Verify(r => r.LoadSettings(It.Is(u => u.Equals(new Uri(path)))), Times.Once); + Assert.Fail(); + //repository.Verify(r => r.LoadSettings(It.Is(u => u.Equals(new Uri(path)))), Times.Once); } [TestMethod] @@ -103,7 +105,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations sut.Perform(); - repository.Verify(r => r.LoadSettings(It.Is(u => u.Equals(new Uri(Path.Combine(location, "SettingsDummy.txt"))))), Times.Once); + Assert.Fail(); + //repository.Verify(r => r.LoadSettings(It.Is(u => u.Equals(new Uri(Path.Combine(location, "SettingsDummy.txt"))))), Times.Once); } [TestMethod] @@ -117,7 +120,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations sut.Perform(); - repository.Verify(r => r.LoadSettings(It.Is(u => u.Equals(new Uri(Path.Combine(location, "SettingsDummy.txt"))))), Times.Once); + Assert.Fail(); + //repository.Verify(r => r.LoadSettings(It.Is(u => u.Equals(new Uri(Path.Combine(location, "SettingsDummy.txt"))))), Times.Once); } [TestMethod] diff --git a/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj b/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj index c5e57a0f..28350f1a 100644 --- a/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj +++ b/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj @@ -53,7 +53,6 @@ pdbonly x86 prompt - MinimumRecommendedRules.ruleset diff --git a/SafeExamBrowser.Runtime/App.cs b/SafeExamBrowser.Runtime/App.cs index ca893a35..881540f2 100644 --- a/SafeExamBrowser.Runtime/App.cs +++ b/SafeExamBrowser.Runtime/App.cs @@ -75,6 +75,9 @@ namespace SafeExamBrowser.Runtime instances.RuntimeController.Terminate(); instances.LogShutdownInformation(); + // TODO: Which UI operation is being cancelled without the timeout? Same problem with client? -> Debug! + Thread.Sleep(20); + base.Shutdown(); } diff --git a/SafeExamBrowser.Runtime/Behaviour/Operations/ClientOperation.cs b/SafeExamBrowser.Runtime/Behaviour/Operations/ClientOperation.cs index ba491e98..1622ac55 100644 --- a/SafeExamBrowser.Runtime/Behaviour/Operations/ClientOperation.cs +++ b/SafeExamBrowser.Runtime/Behaviour/Operations/ClientOperation.cs @@ -8,7 +8,7 @@ using System.Threading; using SafeExamBrowser.Contracts.Behaviour.OperationModel; -using SafeExamBrowser.Contracts.Communication; +using SafeExamBrowser.Contracts.Communication.Events; using SafeExamBrowser.Contracts.Communication.Hosts; using SafeExamBrowser.Contracts.Communication.Proxies; using SafeExamBrowser.Contracts.Configuration; diff --git a/SafeExamBrowser.Runtime/Behaviour/Operations/ConfigurationOperation.cs b/SafeExamBrowser.Runtime/Behaviour/Operations/ConfigurationOperation.cs index 2b9fd6ea..2b32609b 100644 --- a/SafeExamBrowser.Runtime/Behaviour/Operations/ConfigurationOperation.cs +++ b/SafeExamBrowser.Runtime/Behaviour/Operations/ConfigurationOperation.cs @@ -50,35 +50,58 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations logger.Info("Initializing application configuration..."); ProgressIndicator?.UpdateText(TextKey.ProgressIndicator_InitializeConfiguration); - var isValidUri = TryGetSettingsUri(out Uri uri); + var isValidUri = TryInitializeSettingsUri(out Uri uri); if (isValidUri) { - logger.Info($"Loading configuration from '{uri.AbsolutePath}'..."); + logger.Info($"Loading settings from '{uri.AbsolutePath}'..."); - var abort = LoadSettings(uri); + var result = LoadSettings(uri); - if (abort) + if (result == OperationResult.Success && repository.CurrentSettings.ConfigurationMode == ConfigurationMode.ConfigureClient) { - return OperationResult.Aborted; + var abort = IsConfigurationSufficient(); + + logger.Info($"The user chose to {(abort ? "abort" : "continue")} after successful client configuration."); + + if (abort) + { + return OperationResult.Aborted; + } } + + LogOperationResult(result); + + return result; } - else - { - logger.Info("No valid settings file specified nor found in PROGRAMDATA or APPDATA - loading default settings..."); - repository.LoadDefaultSettings(); - } + + logger.Info("No valid settings resource specified nor found in PROGRAMDATA or APPDATA - loading default settings..."); + repository.LoadDefaultSettings(); return OperationResult.Success; } public OperationResult Repeat() { - // TODO: How will the new settings be retrieved? Uri passed to the repository? If yes, how does the Uri get here?! - // -> IDEA: Use configuration repository as container? - // -> IDEA: Introduce IRepeatParams or alike? + logger.Info("Initializing new application configuration..."); + ProgressIndicator?.UpdateText(TextKey.ProgressIndicator_InitializeConfiguration); - return OperationResult.Success; + var isValidUri = TryValidateSettingsUri(repository.ReconfigurationFilePath, out Uri uri); + + if (isValidUri) + { + logger.Info($"Loading settings from '{uri.AbsolutePath}'..."); + + var result = LoadSettings(uri); + + LogOperationResult(result); + + return result; + } + + logger.Warn($"The resource specified for reconfiguration does not exist or is not a file!"); + + return OperationResult.Failed; } public void Revert() @@ -86,7 +109,79 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations // Nothing to do here... } - private bool TryGetSettingsUri(out Uri uri) + private OperationResult LoadSettings(Uri uri) + { + var adminPassword = default(string); + var settingsPassword = default(string); + var status = default(LoadStatus); + + for (int adminAttempts = 0, settingsAttempts = 0; adminAttempts < 5 && settingsAttempts < 5;) + { + status = repository.LoadSettings(uri, settingsPassword, adminPassword); + + if (status == LoadStatus.InvalidData || status == LoadStatus.Success) + { + break; + } + else if (status == LoadStatus.AdminPasswordNeeded || status == LoadStatus.SettingsPasswordNeeded) + { + var isAdmin = status == LoadStatus.AdminPasswordNeeded; + var success = isAdmin ? TryGetAdminPassword(out adminPassword) : TryGetSettingsPassword(out settingsPassword); + + if (success) + { + adminAttempts += isAdmin ? 1 : 0; + settingsAttempts += isAdmin ? 0 : 1; + } + else + { + return OperationResult.Aborted; + } + } + } + + if (status == LoadStatus.InvalidData) + { + if (IsHtmlPage(uri)) + { + repository.LoadDefaultSettings(); + repository.CurrentSettings.Browser.StartUrl = uri.AbsoluteUri; + logger.Info($"The specified URI '{uri.AbsoluteUri}' appears to point to a HTML page, setting it as startup URL."); + + return OperationResult.Success; + } + + logger.Error($"The specified settings resource '{uri.AbsoluteUri}' is invalid!"); + } + + return status == LoadStatus.Success ? OperationResult.Success : OperationResult.Failed; + } + + private bool IsHtmlPage(Uri uri) + { + // TODO + return false; + } + + private bool TryGetAdminPassword(out string password) + { + password = default(string); + + // TODO + + return true; + } + + private bool TryGetSettingsPassword(out string password) + { + password = default(string); + + // TODO + + return true; + } + + private bool TryInitializeSettingsUri(out Uri uri) { var path = string.Empty; var isValidUri = false; @@ -119,19 +214,14 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations return isValidUri; } - private bool LoadSettings(Uri uri) + private bool TryValidateSettingsUri(string path, out Uri uri) { - var abort = false; - var settings = repository.LoadSettings(uri); + var isValidUri = Uri.TryCreate(path, UriKind.Absolute, out uri); - if (settings.ConfigurationMode == ConfigurationMode.ConfigureClient) - { - abort = IsConfigurationSufficient(); + isValidUri &= uri != null && uri.IsFile; + isValidUri &= File.Exists(path); - logger.Info($"The user chose to {(abort ? "abort" : "continue")} after successful client configuration."); - } - - return abort; + return isValidUri; } private bool IsConfigurationSufficient() @@ -142,5 +232,21 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations return abort == MessageBoxResult.Yes; } + + private void LogOperationResult(OperationResult result) + { + switch (result) + { + case OperationResult.Aborted: + logger.Info("The configuration was aborted by the user."); + break; + case OperationResult.Failed: + logger.Warn("The configuration has failed!"); + break; + case OperationResult.Success: + logger.Info("The configuration was successful."); + break; + } + } } } diff --git a/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs b/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs index e525972b..c1e038f8 100644 --- a/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs +++ b/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs @@ -9,6 +9,7 @@ using System; using SafeExamBrowser.Contracts.Behaviour; using SafeExamBrowser.Contracts.Behaviour.OperationModel; +using SafeExamBrowser.Contracts.Communication.Events; using SafeExamBrowser.Contracts.Communication.Hosts; using SafeExamBrowser.Contracts.Communication.Proxies; using SafeExamBrowser.Contracts.Configuration; @@ -169,13 +170,14 @@ namespace SafeExamBrowser.Runtime.Behaviour if (result == OperationResult.Failed) { + // TODO: Check if message box is rendered on new desktop as well! -> E.g. if settings for reconfiguration are invalid messageBox.Show(TextKey.MessageBox_SessionStartError, TextKey.MessageBox_SessionStartErrorTitle, icon: MessageBoxIcon.Error); - } - if (!initial) - { - logger.Info("Terminating application..."); - shutdown.Invoke(); + if (!initial) + { + logger.Info("Terminating application..."); + shutdown.Invoke(); + } } } } @@ -245,10 +247,22 @@ namespace SafeExamBrowser.Runtime.Behaviour shutdown.Invoke(); } - private void RuntimeHost_ReconfigurationRequested() + private void RuntimeHost_ReconfigurationRequested(ReconfigurationEventArgs args) { - logger.Info($"Starting reconfiguration..."); - StartSession(); + var mode = configuration.CurrentSettings.ConfigurationMode; + + if (mode == ConfigurationMode.ConfigureClient) + { + logger.Info($"Accepted request for reconfiguration with '{args.ConfigurationPath}'."); + configuration.ReconfigurationFilePath = args.ConfigurationPath; + + StartSession(); + } + else + { + logger.Info($"Denied request for reconfiguration with '{args.ConfigurationPath}' due to '{mode}' mode!"); + // TODO: configuration.CurrentSession.ClientProxy.InformReconfigurationDenied(); + } } private void RuntimeHost_ShutdownRequested() diff --git a/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs b/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs index d4520d10..63cf64c3 100644 --- a/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs +++ b/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs @@ -7,12 +7,10 @@ */ using System; -using System.Threading.Tasks; -using SafeExamBrowser.Contracts.Communication; using SafeExamBrowser.Contracts.Communication.Data; +using SafeExamBrowser.Contracts.Communication.Events; using SafeExamBrowser.Contracts.Communication.Hosts; using SafeExamBrowser.Contracts.Configuration; -using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Core.Communication.Hosts; @@ -27,7 +25,7 @@ namespace SafeExamBrowser.Runtime.Communication public event CommunicationEventHandler ClientDisconnected; public event CommunicationEventHandler ClientReady; - public event CommunicationEventHandler ReconfigurationRequested; + public event CommunicationEventHandler ReconfigurationRequested; public event CommunicationEventHandler ShutdownRequested; public RuntimeHost(string address, IConfigurationRepository configuration, IHostObjectFactory factory, ILogger logger) : base(address, factory, logger) @@ -64,9 +62,9 @@ namespace SafeExamBrowser.Runtime.Communication { switch (message) { - case ReconfigurationMessage reconfigurationMessage: - // TODO: Not the job of the host, fire event or alike! - return Handle(reconfigurationMessage); + case ReconfigurationMessage r: + ReconfigurationRequested?.InvokeAsync(new ReconfigurationEventArgs { ConfigurationPath = r.ConfigurationPath }); + return new SimpleResponse(SimpleResponsePurport.Acknowledged); } return new SimpleResponse(SimpleResponsePurport.UnknownMessage); @@ -89,22 +87,5 @@ namespace SafeExamBrowser.Runtime.Communication return new SimpleResponse(SimpleResponsePurport.UnknownMessage); } - - private Response Handle(ReconfigurationMessage message) - { - var isExam = configuration.CurrentSettings.ConfigurationMode == ConfigurationMode.Exam; - var isValidUri = Uri.TryCreate(message.ConfigurationUrl, UriKind.Absolute, out _); - var allowed = !isExam && isValidUri; - - Logger.Info($"Received reconfiguration request for '{message.ConfigurationUrl}', {(allowed ? "accepted" : "denied")} it."); - - if (allowed) - { - configuration.ReconfigurationUrl = message.ConfigurationUrl; - Task.Run(() => ReconfigurationRequested?.Invoke()); - } - - return new ReconfigurationResponse { Accepted = allowed }; - } } } diff --git a/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj b/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj index b4fe06cb..9f194f71 100644 --- a/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj +++ b/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj @@ -61,8 +61,8 @@ full x86 prompt - MinimumRecommendedRules.ruleset true + MinimumRecommendedRules.ruleset bin\x86\Release\ @@ -71,7 +71,6 @@ pdbonly x86 prompt - MinimumRecommendedRules.ruleset true diff --git a/SafeExamBrowser.SystemComponents/SafeExamBrowser.SystemComponents.csproj b/SafeExamBrowser.SystemComponents/SafeExamBrowser.SystemComponents.csproj index 4302968c..7a0d1012 100644 --- a/SafeExamBrowser.SystemComponents/SafeExamBrowser.SystemComponents.csproj +++ b/SafeExamBrowser.SystemComponents/SafeExamBrowser.SystemComponents.csproj @@ -45,7 +45,6 @@ pdbonly x86 prompt - MinimumRecommendedRules.ruleset diff --git a/SafeExamBrowser.UserInterface.Classic/SafeExamBrowser.UserInterface.Classic.csproj b/SafeExamBrowser.UserInterface.Classic/SafeExamBrowser.UserInterface.Classic.csproj index 8af616a1..d47fa8ea 100644 --- a/SafeExamBrowser.UserInterface.Classic/SafeExamBrowser.UserInterface.Classic.csproj +++ b/SafeExamBrowser.UserInterface.Classic/SafeExamBrowser.UserInterface.Classic.csproj @@ -47,7 +47,6 @@ pdbonly x86 prompt - MinimumRecommendedRules.ruleset diff --git a/SafeExamBrowser.UserInterface.Windows10/SafeExamBrowser.UserInterface.Windows10.csproj b/SafeExamBrowser.UserInterface.Windows10/SafeExamBrowser.UserInterface.Windows10.csproj index 554cb850..48b11485 100644 --- a/SafeExamBrowser.UserInterface.Windows10/SafeExamBrowser.UserInterface.Windows10.csproj +++ b/SafeExamBrowser.UserInterface.Windows10/SafeExamBrowser.UserInterface.Windows10.csproj @@ -46,7 +46,6 @@ pdbonly x86 prompt - MinimumRecommendedRules.ruleset diff --git a/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj b/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj index 596479ef..4ffd72d5 100644 --- a/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj +++ b/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj @@ -45,7 +45,6 @@ pdbonly x86 prompt - MinimumRecommendedRules.ruleset diff --git a/SafeExamBrowser.sln b/SafeExamBrowser.sln index 3207399f..46584cd9 100644 --- a/SafeExamBrowser.sln +++ b/SafeExamBrowser.sln @@ -9,6 +9,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeExamBrowser.Core", "Saf EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0A9E6674-2FB4-42EA-85DE-B2445B9AE2D9}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig LICENSE.txt = LICENSE.txt EndProjectSection EndProject