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 @@
+