diff --git a/SafeExamBrowser.Configuration/RuntimeInfo.cs b/SafeExamBrowser.Configuration/RuntimeInfo.cs index 18bc24d2..06524adb 100644 --- a/SafeExamBrowser.Configuration/RuntimeInfo.cs +++ b/SafeExamBrowser.Configuration/RuntimeInfo.cs @@ -19,7 +19,9 @@ namespace SafeExamBrowser.Configuration public string BrowserCachePath { get; set; } public string BrowserLogFile { get; set; } public string ClientLogFile { get; set; } + public string DefaultSettingsFileName { get; set; } public string ProgramCopyright { get; set; } + public string ProgramDataFolder { get; set; } public string ProgramTitle { get; set; } public string ProgramVersion { get; set; } public string RuntimeLogFile { get; set; } diff --git a/SafeExamBrowser.Contracts/Configuration/IRuntimeInfo.cs b/SafeExamBrowser.Contracts/Configuration/IRuntimeInfo.cs index 989e0f3b..c6ddef4e 100644 --- a/SafeExamBrowser.Contracts/Configuration/IRuntimeInfo.cs +++ b/SafeExamBrowser.Contracts/Configuration/IRuntimeInfo.cs @@ -37,11 +37,21 @@ namespace SafeExamBrowser.Contracts.Configuration /// string ClientLogFile { get; } + /// + /// The default file name for application settings. + /// + string DefaultSettingsFileName { get; } + /// /// The copyright information for the application (i.e. the executing assembly). /// string ProgramCopyright { get; } + /// + /// The path of the program data folder. + /// + string ProgramDataFolder { get; } + /// /// The program title of the application (i.e. the executing assembly). /// diff --git a/SafeExamBrowser.Contracts/I18n/TextKey.cs b/SafeExamBrowser.Contracts/I18n/TextKey.cs index 9bfd27bb..1666d67e 100644 --- a/SafeExamBrowser.Contracts/I18n/TextKey.cs +++ b/SafeExamBrowser.Contracts/I18n/TextKey.cs @@ -25,6 +25,7 @@ namespace SafeExamBrowser.Contracts.I18n Notification_LogTooltip, SplashScreen_EmptyClipboard, SplashScreen_InitializeBrowser, + SplashScreen_InitializeConfiguration, SplashScreen_InitializeProcessMonitoring, SplashScreen_InitializeTaskbar, SplashScreen_InitializeWindowMonitoring, diff --git a/SafeExamBrowser.Contracts/Runtime/IRuntimeController.cs b/SafeExamBrowser.Contracts/Runtime/IRuntimeController.cs index c6452974..770fe573 100644 --- a/SafeExamBrowser.Contracts/Runtime/IRuntimeController.cs +++ b/SafeExamBrowser.Contracts/Runtime/IRuntimeController.cs @@ -6,10 +6,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using SafeExamBrowser.Contracts.Configuration.Settings; + namespace SafeExamBrowser.Contracts.Runtime { public interface IRuntimeController { + /// + /// Allows to specify the application settings to be used during runtime. + /// + ISettings Settings { set; } + /// /// Wires up and starts the application event handling. /// diff --git a/SafeExamBrowser.Core/I18n/Text.xml b/SafeExamBrowser.Core/I18n/Text.xml index b9461f31..c85c1b67 100644 --- a/SafeExamBrowser.Core/I18n/Text.xml +++ b/SafeExamBrowser.Core/I18n/Text.xml @@ -10,6 +10,7 @@ Application Log Emptying clipboard Initializing browser + Initializing application configuration Initializing process monitoring Initializing taskbar Initializing window monitoring diff --git a/SafeExamBrowser.Runtime.UnitTests/Behaviour/Operations/ConfigurationOperationTests.cs b/SafeExamBrowser.Runtime.UnitTests/Behaviour/Operations/ConfigurationOperationTests.cs new file mode 100644 index 00000000..f5583165 --- /dev/null +++ b/SafeExamBrowser.Runtime.UnitTests/Behaviour/Operations/ConfigurationOperationTests.cs @@ -0,0 +1,153 @@ +/* + * 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.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Settings; +using SafeExamBrowser.Contracts.Logging; +using SafeExamBrowser.Contracts.Runtime; +using SafeExamBrowser.Contracts.UserInterface; +using SafeExamBrowser.Runtime.Behaviour.Operations; + +namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations +{ + [TestClass] + public class ConfigurationOperationTests + { + private Mock logger; + private Mock controller; + private Mock info; + private Mock repository; + private Mock splashScreen; + + private ConfigurationOperation sut; + + [TestInitialize] + public void Initialize() + { + logger = new Mock(); + controller = new Mock(); + info = new Mock(); + repository = new Mock(); + splashScreen = new Mock(); + + info.SetupGet(r => r.AppDataFolder).Returns(@"C:\Not\Really\AppData"); + info.SetupGet(r => r.DefaultSettingsFileName).Returns("SettingsDummy.txt"); + info.SetupGet(r => r.ProgramDataFolder).Returns(@"C:\Not\Really\ProgramData"); + } + + [TestMethod] + public void MustNotFailWithoutCommandLineArgs() + { + controller.SetupSet(c => c.Settings = It.IsAny()); + + sut = new ConfigurationOperation(logger.Object, controller.Object, info.Object, repository.Object, null) + { + SplashScreen = splashScreen.Object + }; + + sut.Perform(); + + sut = new ConfigurationOperation(logger.Object, controller.Object, info.Object, repository.Object, new string[] { }) + { + SplashScreen = splashScreen.Object + }; + + sut.Perform(); + + controller.VerifySet(c => c.Settings = It.IsAny(), Times.Exactly(2)); + } + + [TestMethod] + public void MustNotFailWithInvalidUri() + { + var path = @"an/invalid\path.'*%yolo/()"; + + sut = new ConfigurationOperation(logger.Object, controller.Object, info.Object, repository.Object, new [] { "blubb.exe", path }) + { + SplashScreen = splashScreen.Object + }; + + sut.Perform(); + } + + [TestMethod] + public void MustUseCommandLineArgumentAs1stPrio() + { + var path = @"http://www.safeexambrowser.org/whatever.seb"; + var location = Path.GetDirectoryName(GetType().Assembly.Location); + + info.SetupGet(r => r.ProgramDataFolder).Returns(location); + info.SetupGet(r => r.AppDataFolder).Returns(location); + + sut = new ConfigurationOperation(logger.Object, controller.Object, info.Object, repository.Object, new[] { "blubb.exe", path }) + { + SplashScreen = splashScreen.Object + }; + + sut.Perform(); + + controller.VerifySet(c => c.Settings = It.IsAny(), Times.Once); + repository.Verify(r => r.Load(It.Is(u => u.Equals(new Uri(path)))), Times.Once); + } + + [TestMethod] + public void MustUseProgramDataAs2ndPrio() + { + var location = Path.GetDirectoryName(GetType().Assembly.Location); + + info.SetupGet(r => r.ProgramDataFolder).Returns(location); + info.SetupGet(r => r.AppDataFolder).Returns($@"{location}\WRONG"); + + sut = new ConfigurationOperation(logger.Object, controller.Object, info.Object, repository.Object, null) + { + SplashScreen = splashScreen.Object + }; + + sut.Perform(); + + controller.VerifySet(c => c.Settings = It.IsAny(), Times.Once); + repository.Verify(r => r.Load(It.Is(u => u.Equals(new Uri(Path.Combine(location, "SettingsDummy.txt"))))), Times.Once); + } + + [TestMethod] + public void MustUseAppDataAs3rdPrio() + { + var location = Path.GetDirectoryName(GetType().Assembly.Location); + + info.SetupGet(r => r.AppDataFolder).Returns(location); + + sut = new ConfigurationOperation(logger.Object, controller.Object, info.Object, repository.Object, null) + { + SplashScreen = splashScreen.Object + }; + + sut.Perform(); + + controller.VerifySet(c => c.Settings = It.IsAny(), Times.Once); + repository.Verify(r => r.Load(It.Is(u => u.Equals(new Uri(Path.Combine(location, "SettingsDummy.txt"))))), Times.Once); + } + + [TestMethod] + public void MustFallbackToDefaultsAsLastPrio() + { + sut = new ConfigurationOperation(logger.Object, controller.Object, info.Object, repository.Object, null) + { + SplashScreen = splashScreen.Object + }; + + sut.Perform(); + + controller.VerifySet(c => c.Settings = It.IsAny(), Times.Once); + repository.Verify(r => r.LoadDefaults(), Times.Once); + } + } +} diff --git a/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj b/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj index c2e39afd..881e3ab3 100644 --- a/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj +++ b/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj @@ -80,6 +80,7 @@ + @@ -97,6 +98,14 @@ SafeExamBrowser.Runtime + + + Always + + + Always + + diff --git a/SafeExamBrowser.Runtime.UnitTests/SettingsDummy.txt b/SafeExamBrowser.Runtime.UnitTests/SettingsDummy.txt new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/SafeExamBrowser.Runtime.UnitTests/SettingsDummy.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/SafeExamBrowser.Runtime.UnitTests/WRONG/SettingsDummy.txt b/SafeExamBrowser.Runtime.UnitTests/WRONG/SettingsDummy.txt new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/SafeExamBrowser.Runtime.UnitTests/WRONG/SettingsDummy.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/SafeExamBrowser.Runtime/Behaviour/Operations/ConfigurationOperation.cs b/SafeExamBrowser.Runtime/Behaviour/Operations/ConfigurationOperation.cs new file mode 100644 index 00000000..e3c9f8d3 --- /dev/null +++ b/SafeExamBrowser.Runtime/Behaviour/Operations/ConfigurationOperation.cs @@ -0,0 +1,104 @@ +/* + * 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.IO; +using SafeExamBrowser.Contracts.Behaviour; +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Configuration.Settings; +using SafeExamBrowser.Contracts.I18n; +using SafeExamBrowser.Contracts.Logging; +using SafeExamBrowser.Contracts.Runtime; +using SafeExamBrowser.Contracts.UserInterface; + +namespace SafeExamBrowser.Runtime.Behaviour.Operations +{ + internal class ConfigurationOperation : IOperation + { + private ILogger logger; + private IRuntimeController controller; + private IRuntimeInfo runtimeInfo; + private ISettingsRepository repository; + private string[] commandLineArgs; + + public ISplashScreen SplashScreen { private get; set; } + + public ConfigurationOperation( + ILogger logger, + IRuntimeController controller, + IRuntimeInfo runtimeInfo, + ISettingsRepository repository, + string[] commandLineArgs) + { + this.logger = logger; + this.controller = controller; + this.commandLineArgs = commandLineArgs; + this.repository = repository; + this.runtimeInfo = runtimeInfo; + } + + public void Perform() + { + logger.Info("Initializing application configuration..."); + SplashScreen.UpdateText(TextKey.SplashScreen_InitializeConfiguration); + + var isValidUri = TryGetSettingsUri(out Uri uri); + + if (isValidUri) + { + logger.Info($"Loading configuration from '{uri.AbsolutePath}'..."); + controller.Settings = repository.Load(uri); + } + else + { + logger.Info("No valid settings file specified nor found in PROGRAMDATA or APPDATA - loading default settings..."); + controller.Settings = repository.LoadDefaults(); + } + + // TODO: Allow user to quit if in Configure Client mode - callback to terminate WPF application? + } + + public void Revert() + { + // Nothing to do here... + } + + private bool TryGetSettingsUri(out Uri uri) + { + var path = string.Empty; + var isValidUri = false; + var programDataSettings = Path.Combine(runtimeInfo.ProgramDataFolder, runtimeInfo.DefaultSettingsFileName); + var appDataSettings = Path.Combine(runtimeInfo.AppDataFolder, runtimeInfo.DefaultSettingsFileName); + + uri = null; + + if (commandLineArgs?.Length > 1) + { + path = commandLineArgs[1]; + isValidUri = Uri.TryCreate(path, UriKind.Absolute, out uri); + logger.Info($"Found command-line argument for settings file: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}."); + } + + if (!isValidUri && File.Exists(programDataSettings)) + { + path = programDataSettings; + isValidUri = Uri.TryCreate(path, UriKind.Absolute, out uri); + logger.Info($"Found settings file in PROGRAMDATA: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}."); + } + + if (!isValidUri && File.Exists(appDataSettings)) + { + path = appDataSettings; + isValidUri = Uri.TryCreate(path, UriKind.Absolute, out uri); + logger.Info($"Found settings file in APPDATA: '{path}', the URI is {(isValidUri ? "valid" : "invalid")}."); + } + + return isValidUri; + } + } +} diff --git a/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs b/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs index 54854f18..c6d89845 100644 --- a/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs +++ b/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Contracts.Runtime; @@ -15,6 +16,8 @@ namespace SafeExamBrowser.Runtime.Behaviour { private ILogger logger; + public ISettings Settings { private get; set; } + public RuntimeController(ILogger logger) { this.logger = logger; diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs index 42680ed4..48ee9791 100644 --- a/SafeExamBrowser.Runtime/CompositionRoot.cs +++ b/SafeExamBrowser.Runtime/CompositionRoot.cs @@ -6,12 +6,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - using System; using System.Collections.Generic; using System.IO; using System.Reflection; using SafeExamBrowser.Configuration; +using SafeExamBrowser.Configuration.Settings; using SafeExamBrowser.Contracts.Behaviour; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Logging; @@ -34,9 +34,11 @@ namespace SafeExamBrowser.Runtime internal void BuildObjectGraph() { + var args = Environment.GetCommandLineArgs(); var logger = new Logger(); var nativeMethods = new NativeMethods(); var runtimeInfo = new RuntimeInfo(); + var settingsRepository = new SettingsRepository(); var systemInfo = new SystemInfo(); var uiFactory = new UserInterfaceFactory(); @@ -51,8 +53,7 @@ namespace SafeExamBrowser.Runtime StartupOperations = new Queue(); StartupOperations.Enqueue(new I18nOperation(logger, text)); - // TODO - //StartupOperations.Enqueue(new ConfigurationOperation()); + StartupOperations.Enqueue(new ConfigurationOperation(logger, runtimeController, runtimeInfo, settingsRepository, args)); //StartupOperations.Enqueue(new KioskModeOperation()); StartupOperations.Enqueue(new RuntimeControllerOperation(runtimeController, logger)); } @@ -70,7 +71,9 @@ namespace SafeExamBrowser.Runtime runtimeInfo.BrowserCachePath = Path.Combine(appDataFolder, "Cache"); runtimeInfo.BrowserLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Browser.txt"); runtimeInfo.ClientLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Client.txt"); + runtimeInfo.DefaultSettingsFileName = "SebClientSettings.seb"; 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.RuntimeLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Runtime.txt"); diff --git a/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj b/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj index 35dcf99a..2f587880 100644 --- a/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj +++ b/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj @@ -87,6 +87,7 @@ +