From 902b0c2b3b6442b97a2969d333a59e0fea369a94 Mon Sep 17 00:00:00 2001 From: dbuechel Date: Thu, 8 Nov 2018 09:39:52 +0100 Subject: [PATCH] SEBWIN-221: Implemented scaffolding for loading and parsing of configuration resources. --- .../BrowserApplicationInstance.cs | 12 +- .../Handlers/RequestHandler.cs | 4 +- SafeExamBrowser.Client/ClientController.cs | 2 +- .../ConfigurationRepositoryTests.cs | 5 +- .../ConfigurationRepository.cs | 138 +++++++++++++----- .../DataFormats/DefaultFormat.cs | 37 +++++ .../DataFormats/HtmlFormat.cs | 34 +++++ .../DataFormats/XmlFormat.cs | 34 +++++ .../ResourceLoader.cs | 22 --- .../ResourceLoaders/FileResourceLoader.cs | 46 ++++++ .../ResourceLoaders/NetworkResourceLoader.cs | 135 +++++++++++++++++ .../SafeExamBrowser.Configuration.csproj | 7 +- .../Configuration/AppConfig.cs | 8 + .../Configuration/IConfigurationRepository.cs | 23 ++- .../Configuration/IDataFormat.cs | 26 ++++ .../Configuration/IResourceLoader.cs | 11 +- .../Configuration/LoadStatus.cs | 16 +- .../Configuration/Settings/BrowserSettings.cs | 7 +- SafeExamBrowser.Contracts/I18n/TextKey.cs | 6 + .../SafeExamBrowser.Contracts.csproj | 1 + SafeExamBrowser.I18n/Text.xml | 18 +++ .../Operations/ConfigurationOperationTests.cs | 92 +++++------- SafeExamBrowser.Runtime.UnitTests/app.config | 4 + SafeExamBrowser.Runtime/CompositionRoot.cs | 22 ++- .../Operations/ConfigurationOperation.cs | 42 ++---- .../Events/InvalidDataMessageArgs.cs | 24 +++ .../Operations/Events/MessageEventArgs.cs | 30 ++++ .../Events/NotSupportedMessageArgs.cs | 24 +++ .../Events/UnexpectedErrorMessageArgs.cs | 24 +++ .../Operations/KioskModeOperation.cs | 4 - SafeExamBrowser.Runtime/RuntimeController.cs | 21 +++ .../SafeExamBrowser.Runtime.csproj | 4 + SafeExamBrowser.WindowsApi/ExplorerShell.cs | 1 + 33 files changed, 710 insertions(+), 174 deletions(-) create mode 100644 SafeExamBrowser.Configuration/DataFormats/DefaultFormat.cs create mode 100644 SafeExamBrowser.Configuration/DataFormats/HtmlFormat.cs create mode 100644 SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs delete mode 100644 SafeExamBrowser.Configuration/ResourceLoader.cs create mode 100644 SafeExamBrowser.Configuration/ResourceLoaders/FileResourceLoader.cs create mode 100644 SafeExamBrowser.Configuration/ResourceLoaders/NetworkResourceLoader.cs create mode 100644 SafeExamBrowser.Contracts/Configuration/IDataFormat.cs create mode 100644 SafeExamBrowser.Runtime/Operations/Events/InvalidDataMessageArgs.cs create mode 100644 SafeExamBrowser.Runtime/Operations/Events/MessageEventArgs.cs create mode 100644 SafeExamBrowser.Runtime/Operations/Events/NotSupportedMessageArgs.cs create mode 100644 SafeExamBrowser.Runtime/Operations/Events/UnexpectedErrorMessageArgs.cs diff --git a/SafeExamBrowser.Browser/BrowserApplicationInstance.cs b/SafeExamBrowser.Browser/BrowserApplicationInstance.cs index a694efd1..d38f2801 100644 --- a/SafeExamBrowser.Browser/BrowserApplicationInstance.cs +++ b/SafeExamBrowser.Browser/BrowserApplicationInstance.cs @@ -103,9 +103,17 @@ namespace SafeExamBrowser.Browser private void DownloadHandler_ConfigurationDownloadRequested(string fileName, DownloadEventArgs args) { - args.BrowserWindow = window; + if (settings.AllowConfigurationDownloads) + { + args.BrowserWindow = window; + logger.Debug($"Forwarding download request for configuration file '{fileName}'."); - ConfigurationDownloadRequested?.Invoke(fileName, args); + ConfigurationDownloadRequested?.Invoke(fileName, args); + } + else + { + logger.Debug($"Discarded download request for configuration file '{fileName}'."); + } } private void Window_AddressChanged(string address) diff --git a/SafeExamBrowser.Browser/Handlers/RequestHandler.cs b/SafeExamBrowser.Browser/Handlers/RequestHandler.cs index bcd75dec..eab33f4b 100644 --- a/SafeExamBrowser.Browser/Handlers/RequestHandler.cs +++ b/SafeExamBrowser.Browser/Handlers/RequestHandler.cs @@ -31,11 +31,11 @@ namespace SafeExamBrowser.Browser.Handlers if (uri.Scheme == appConfig.SebUriScheme) { - request.Url = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.ToString(); + request.Url = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.Uri.AbsoluteUri; } else if (uri.Scheme == appConfig.SebUriSchemeSecure) { - request.Url = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttps }.ToString(); + request.Url = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttps }.Uri.AbsoluteUri; } return base.OnBeforeResourceLoad(browserControl, browser, frame, request, callback); diff --git a/SafeExamBrowser.Client/ClientController.cs b/SafeExamBrowser.Client/ClientController.cs index 8564025f..3478a4a8 100644 --- a/SafeExamBrowser.Client/ClientController.cs +++ b/SafeExamBrowser.Client/ClientController.cs @@ -206,7 +206,7 @@ namespace SafeExamBrowser.Client { if (Settings.ConfigurationMode == ConfigurationMode.ConfigureClient) { - logger.Debug($"Received download request for configuration file '{fileName}'. Asking user to confirm the reconfiguration..."); + logger.Info($"Received download request for configuration file '{fileName}'. Asking user to confirm the reconfiguration..."); var message = TextKey.MessageBox_ReconfigurationQuestion; var title = TextKey.MessageBox_ReconfigurationQuestionTitle; diff --git a/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs b/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs index da13aa7c..ecab0473 100644 --- a/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs +++ b/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs @@ -9,7 +9,9 @@ using System; using System.Reflection; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Configuration.UnitTests { @@ -22,8 +24,9 @@ namespace SafeExamBrowser.Configuration.UnitTests public void Initialize() { var executablePath = Assembly.GetExecutingAssembly().Location; + var logger = new Mock(); - sut = new ConfigurationRepository(executablePath, string.Empty, string.Empty, string.Empty); + sut = new ConfigurationRepository(logger.Object, executablePath, string.Empty, string.Empty, string.Empty); } [TestMethod] diff --git a/SafeExamBrowser.Configuration/ConfigurationRepository.cs b/SafeExamBrowser.Configuration/ConfigurationRepository.cs index b52d4134..8689cb90 100644 --- a/SafeExamBrowser.Configuration/ConfigurationRepository.cs +++ b/SafeExamBrowser.Configuration/ConfigurationRepository.cs @@ -7,7 +7,11 @@ */ using System; +using System.Collections.Generic; using System.IO; +using System.IO.Compression; +using System.Linq; +using SafeExamBrowser.Configuration.DataFormats; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Logging; @@ -24,9 +28,16 @@ namespace SafeExamBrowser.Configuration private readonly string programVersion; private AppConfig appConfig; + private IList dataFormats; + private ILogger logger; + private IList resourceLoaders; - public ConfigurationRepository(string executablePath, string programCopyright, string programTitle, string programVersion) + public ConfigurationRepository(ILogger logger, string executablePath, string programCopyright, string programTitle, string programVersion) { + dataFormats = new List(); + resourceLoaders = new List(); + + this.logger = logger; this.executablePath = executablePath ?? string.Empty; this.programCopyright = programCopyright ?? string.Empty; this.programTitle = programTitle ?? string.Empty; @@ -73,38 +84,28 @@ namespace SafeExamBrowser.Configuration UpdateAppConfig(); - configuration.AppConfig = CloneAppConfig(); + configuration.AppConfig = appConfig.Clone(); configuration.Id = Guid.NewGuid(); configuration.StartupToken = Guid.NewGuid(); return configuration; } - public LoadStatus TryLoadSettings(Uri resource, out Settings settings, string adminPassword = null, string settingsPassword = null) - { - // TODO: Implement loading mechanism - - settings = LoadDefaultSettings(); - - return LoadStatus.Success; - } - public Settings LoadDefaultSettings() { - // TODO: Implement default settings - var settings = new Settings(); - settings.KioskMode = new Random().Next(10) < 5 ? KioskMode.CreateNewDesktop : KioskMode.DisableExplorerShell; + settings.KioskMode = KioskMode.None; settings.ServicePolicy = ServicePolicy.Optional; settings.Browser.StartUrl = "https://www.safeexambrowser.org/testing"; settings.Browser.AllowAddressBar = true; settings.Browser.AllowBackwardNavigation = true; + settings.Browser.AllowConfigurationDownloads = true; settings.Browser.AllowDeveloperConsole = true; + settings.Browser.AllowDownloads = true; settings.Browser.AllowForwardNavigation = true; settings.Browser.AllowReloading = true; - settings.Browser.AllowDownloads = true; settings.Taskbar.AllowApplicationLog = true; settings.Taskbar.AllowKeyboardLayout = true; @@ -113,33 +114,90 @@ namespace SafeExamBrowser.Configuration return settings; } - private AppConfig CloneAppConfig() + public void Register(IDataFormat dataFormat) { - return new AppConfig + dataFormats.Add(dataFormat); + } + + public void Register(IResourceLoader resourceLoader) + { + resourceLoaders.Add(resourceLoader); + } + + public LoadStatus TryLoadSettings(Uri resource, out Settings settings, string adminPassword = null, string settingsPassword = null) + { + settings = default(Settings); + + logger.Info($"Attempting to load '{resource}'..."); + + try { - AppDataFolder = appConfig.AppDataFolder, - ApplicationStartTime = appConfig.ApplicationStartTime, - BrowserCachePath = appConfig.BrowserCachePath, - BrowserLogFile = appConfig.BrowserLogFile, - ClientAddress = appConfig.ClientAddress, - ClientExecutablePath = appConfig.ClientExecutablePath, - ClientId = appConfig.ClientId, - ClientLogFile = appConfig.ClientLogFile, - ConfigurationFileExtension = appConfig.ConfigurationFileExtension, - DefaultSettingsFileName = appConfig.DefaultSettingsFileName, - DownloadDirectory = appConfig.DownloadDirectory, - LogLevel = appConfig.LogLevel, - ProgramCopyright = appConfig.ProgramCopyright, - ProgramDataFolder = appConfig.ProgramDataFolder, - ProgramTitle = appConfig.ProgramTitle, - ProgramVersion = appConfig.ProgramVersion, - RuntimeAddress = appConfig.RuntimeAddress, - RuntimeId = appConfig.RuntimeId, - RuntimeLogFile = appConfig.RuntimeLogFile, - SebUriScheme = appConfig.SebUriScheme, - SebUriSchemeSecure = appConfig.SebUriSchemeSecure, - ServiceAddress = appConfig.ServiceAddress - }; + var resourceLoader = resourceLoaders.FirstOrDefault(l => l.CanLoad(resource)); + + if (resourceLoader != null) + { + var data = resourceLoader.Load(resource); + var dataFormat = dataFormats.FirstOrDefault(f => f.CanParse(data)); + + logger.Info($"Successfully loaded {data.Length / 1000.0} KB data from '{resource}' using {resourceLoader.GetType().Name}."); + + if (dataFormat is HtmlFormat) + { + return HandleHtml(resource, out settings); + } + + if (dataFormat != null) + { + return dataFormat.TryParse(data, out settings, adminPassword, settingsPassword); + } + } + + logger.Warn($"No {(resourceLoader == null ? "resource loader" : "data format")} found for '{resource}'!"); + + return LoadStatus.NotSupported; + } + catch (Exception e) + { + logger.Error($"Unexpected error while trying to load '{resource}'!", e); + + return LoadStatus.UnexpectedError; + } + } + + private LoadStatus HandleHtml(Uri resource, out Settings settings) + { + logger.Info($"Loaded data appears to be HTML, loading default settings and using '{resource}' as startup URL."); + + settings = LoadDefaultSettings(); + settings.Browser.StartUrl = resource.AbsoluteUri; + + return LoadStatus.Success; + } + + private byte[] Decompress(byte[] bytes) + { + try + { + var buffer = new byte[4096]; + + using (var stream = new GZipStream(new MemoryStream(bytes), CompressionMode.Decompress)) + using (var decompressed = new MemoryStream()) + { + var bytesRead = 0; + + do + { + bytesRead = stream.Read(buffer, 0, buffer.Length); + decompressed.Write(buffer, 0, bytesRead); + } while (bytesRead > 0); + + return decompressed.ToArray(); + } + } + catch (InvalidDataException) + { + return bytes; + } } private void UpdateAppConfig() diff --git a/SafeExamBrowser.Configuration/DataFormats/DefaultFormat.cs b/SafeExamBrowser.Configuration/DataFormats/DefaultFormat.cs new file mode 100644 index 00000000..1393811f --- /dev/null +++ b/SafeExamBrowser.Configuration/DataFormats/DefaultFormat.cs @@ -0,0 +1,37 @@ +/* + * 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.Configuration; +using SafeExamBrowser.Contracts.Configuration.Settings; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Configuration.DataFormats +{ + public class DefaultFormat : IDataFormat + { + private ILogger logger; + + public DefaultFormat(ILogger logger) + { + this.logger = logger; + } + + public bool CanParse(byte[] data) + { + return true; + } + + public LoadStatus TryParse(byte[] data, out Settings settings, string adminPassword = null, string settingsPassword = null) + { + settings = new Settings(); + settings.ServicePolicy = ServicePolicy.Optional; + + return LoadStatus.Success; + } + } +} diff --git a/SafeExamBrowser.Configuration/DataFormats/HtmlFormat.cs b/SafeExamBrowser.Configuration/DataFormats/HtmlFormat.cs new file mode 100644 index 00000000..8b92a371 --- /dev/null +++ b/SafeExamBrowser.Configuration/DataFormats/HtmlFormat.cs @@ -0,0 +1,34 @@ +/* + * 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.Configuration; +using SafeExamBrowser.Contracts.Configuration.Settings; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Configuration.DataFormats +{ + public class HtmlFormat : IDataFormat + { + private ILogger logger; + + public HtmlFormat(ILogger logger) + { + this.logger = logger; + } + + public bool CanParse(byte[] data) + { + return false; + } + + public LoadStatus TryParse(byte[] data, out Settings settings, string adminPassword = null, string settingsPassword = null) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs b/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs new file mode 100644 index 00000000..f1355cb8 --- /dev/null +++ b/SafeExamBrowser.Configuration/DataFormats/XmlFormat.cs @@ -0,0 +1,34 @@ +/* + * 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.Configuration; +using SafeExamBrowser.Contracts.Configuration.Settings; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Configuration.DataFormats +{ + public class XmlFormat : IDataFormat + { + private ILogger logger; + + public XmlFormat(ILogger logger) + { + this.logger = logger; + } + + public bool CanParse(byte[] data) + { + return false; + } + + public LoadStatus TryParse(byte[] data, out Settings settings, string adminPassword = null, string settingsPassword = null) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/SafeExamBrowser.Configuration/ResourceLoader.cs b/SafeExamBrowser.Configuration/ResourceLoader.cs deleted file mode 100644 index 6bca4974..00000000 --- a/SafeExamBrowser.Configuration/ResourceLoader.cs +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 SafeExamBrowser.Contracts.Configuration; - -namespace SafeExamBrowser.Configuration -{ - public class ResourceLoader : IResourceLoader - { - public bool IsHtmlResource(Uri resource) - { - // TODO: Implement resource loader - return false; - } - } -} diff --git a/SafeExamBrowser.Configuration/ResourceLoaders/FileResourceLoader.cs b/SafeExamBrowser.Configuration/ResourceLoaders/FileResourceLoader.cs new file mode 100644 index 00000000..d023ac40 --- /dev/null +++ b/SafeExamBrowser.Configuration/ResourceLoaders/FileResourceLoader.cs @@ -0,0 +1,46 @@ +/* + * 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.Configuration; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Configuration.ResourceLoaders +{ + public class FileResourceLoader : IResourceLoader + { + private ILogger logger; + + public FileResourceLoader(ILogger logger) + { + this.logger = logger; + } + + public bool CanLoad(Uri resource) + { + if (resource.IsFile && File.Exists(resource.AbsolutePath)) + { + logger.Debug($"Can load '{resource}' as it references an existing file."); + + return true; + } + + logger.Debug($"Can't load '{resource}' since it isn't a file URI or no file exists at the specified path."); + + return false; + } + + public byte[] Load(Uri resource) + { + logger.Debug($"Loading data from '{resource}'..."); + + return File.ReadAllBytes(resource.AbsolutePath); + } + } +} diff --git a/SafeExamBrowser.Configuration/ResourceLoaders/NetworkResourceLoader.cs b/SafeExamBrowser.Configuration/ResourceLoaders/NetworkResourceLoader.cs new file mode 100644 index 00000000..0292e686 --- /dev/null +++ b/SafeExamBrowser.Configuration/ResourceLoaders/NetworkResourceLoader.cs @@ -0,0 +1,135 @@ +/* + * 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.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Configuration.ResourceLoaders +{ + public class NetworkResourceLoader : IResourceLoader + { + private AppConfig appConfig; + private ILogger logger; + + private string[] SupportedSchemes => new[] + { + appConfig.SebUriScheme, + appConfig.SebUriSchemeSecure, + Uri.UriSchemeHttp, + Uri.UriSchemeHttps + }; + + public NetworkResourceLoader(AppConfig appConfig, ILogger logger) + { + this.appConfig = appConfig; + this.logger = logger; + } + + public bool CanLoad(Uri resource) + { + if (SupportedSchemes.Contains(resource.Scheme) && IsAvailable(resource)) + { + logger.Debug($"Can load '{resource}' as it references an existing network resource."); + + return true; + } + + logger.Debug($"Can't load '{resource}' since its URI scheme is not supported or the resource is unavailable."); + + return false; + } + + public byte[] Load(Uri resource) + { + var uri = BuildUriFor(resource); + + logger.Debug($"Downloading data from '{uri}'..."); + + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var response = Execute(request); + + logger.Debug($"Sent GET request for '{uri}', received response '{(int) response.StatusCode} - {response.ReasonPhrase}'."); + + var data = Extract(response.Content); + + logger.Debug($"Extracted {data.Length / 1000.0} KB data from response."); + + return data; + } + + private bool IsAvailable(Uri resource) + { + try + { + var uri = BuildUriFor(resource); + var request = new HttpRequestMessage(HttpMethod.Head, uri); + var response = Execute(request); + + logger.Debug($"Sent HEAD request for '{uri}', received response '{(int) response.StatusCode} - {response.ReasonPhrase}'."); + + return response.IsSuccessStatusCode; + } + catch (Exception e) + { + logger.Error($"Failed to check availability of '{resource}'!", e); + + return false; + } + } + + private Uri BuildUriFor(Uri resource) + { + var scheme = GetSchemeFor(resource); + var builder = new UriBuilder(resource) { Scheme = scheme }; + + return builder.Uri; + } + + private string GetSchemeFor(Uri resource) + { + if (resource.Scheme == appConfig.SebUriScheme) + { + return Uri.UriSchemeHttp; + } + + if (resource.Scheme == appConfig.SebUriSchemeSecure) + { + return Uri.UriSchemeHttps; + } + + return resource.Scheme; + } + + private HttpResponseMessage Execute(HttpRequestMessage request) + { + var task = Task.Run(async () => + { + using (var client = new HttpClient()) + { + return await client.SendAsync(request); + } + }); + + return task.GetAwaiter().GetResult(); + } + + private byte[] Extract(HttpContent content) + { + var task = Task.Run(async () => + { + return await content.ReadAsByteArrayAsync(); + }); + + return task.GetAwaiter().GetResult(); + } + } +} diff --git a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj index 0947634c..d7c8327d 100644 --- a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj +++ b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj @@ -48,13 +48,18 @@ + - + + + + + diff --git a/SafeExamBrowser.Contracts/Configuration/AppConfig.cs b/SafeExamBrowser.Contracts/Configuration/AppConfig.cs index 62800bbe..9cd210ef 100644 --- a/SafeExamBrowser.Contracts/Configuration/AppConfig.cs +++ b/SafeExamBrowser.Contracts/Configuration/AppConfig.cs @@ -126,5 +126,13 @@ namespace SafeExamBrowser.Contracts.Configuration /// The communication address of the service component. /// public string ServiceAddress { get; set; } + + /// + /// Creates a shallow clone. + /// + public AppConfig Clone() + { + return MemberwiseClone() as AppConfig; + } } } diff --git a/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs b/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs index 319f7a8b..78eb4bb1 100644 --- a/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs +++ b/SafeExamBrowser.Contracts/Configuration/IConfigurationRepository.cs @@ -25,16 +25,25 @@ namespace SafeExamBrowser.Contracts.Configuration /// ISessionConfiguration InitializeSessionConfiguration(); - /// - /// Attempts to load settings from the specified resource, using the optional passwords. Returns a - /// indicating the result of the operation. As long as the result is not , the declared - /// will be null! - /// - LoadStatus TryLoadSettings(Uri resource, out Settings.Settings settings, string adminPassword = null, string settingsPassword = null); - /// /// Loads the default settings. /// Settings.Settings LoadDefaultSettings(); + + /// + /// Registers the specified as option to parse configuration data. + /// + void Register(IDataFormat dataFormat); + + /// + /// Registers the specified as option to load configuration data. + /// + void Register(IResourceLoader resourceLoader); + + /// + /// Attempts to load settings from the specified resource, using the optional passwords. As long as the result is not + /// , the referenced settings may be null or in an undefinable state! + /// + LoadStatus TryLoadSettings(Uri resource, out Settings.Settings settings, string adminPassword = null, string settingsPassword = null); } } diff --git a/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs b/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs new file mode 100644 index 00000000..659e8e2e --- /dev/null +++ b/SafeExamBrowser.Contracts/Configuration/IDataFormat.cs @@ -0,0 +1,26 @@ +/* + * 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 the data format for a configuration file. + /// + public interface IDataFormat + { + /// + /// Indicates whether the given data complies with the required format. + /// + bool CanParse(byte[] data); + + /// + /// Attempts to parse the given binary data. + /// + LoadStatus TryParse(byte[] data, out Settings.Settings settings, string adminPassword = null, string settingsPassword = null); + } +} diff --git a/SafeExamBrowser.Contracts/Configuration/IResourceLoader.cs b/SafeExamBrowser.Contracts/Configuration/IResourceLoader.cs index 6f4352eb..634ec9e7 100644 --- a/SafeExamBrowser.Contracts/Configuration/IResourceLoader.cs +++ b/SafeExamBrowser.Contracts/Configuration/IResourceLoader.cs @@ -11,13 +11,18 @@ using System; namespace SafeExamBrowser.Contracts.Configuration { /// - /// Loads configuration data from various sources (e.g. the internet) and provides related resource handling functionality. + /// Loads binary data from a particular resource. /// public interface IResourceLoader { /// - /// Indicates whether the given identifies a HTML resource. + /// Indicates whether the resource loader is able to load data from the specified resource. /// - bool IsHtmlResource(Uri resource); + bool CanLoad(Uri resource); + + /// + /// Loads the binary data from the specified resource. + /// + byte[] Load(Uri resource); } } diff --git a/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs b/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs index 41e823ef..f8580889 100644 --- a/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs +++ b/SafeExamBrowser.Contracts/Configuration/LoadStatus.cs @@ -9,7 +9,7 @@ namespace SafeExamBrowser.Contracts.Configuration { /// - /// Defines all possible results of . + /// Defines all possible results of an attempt to load a configuration file. /// public enum LoadStatus { @@ -23,14 +23,24 @@ namespace SafeExamBrowser.Contracts.Configuration /// InvalidData, + /// + /// Indicates that a resource is not supported. + /// + NotSupported, + /// /// Indicates that a settings password is needed in order to load the settings. /// SettingsPasswordNeeded, /// - /// The were loaded successfully. + /// The settings were loaded successfully. /// - Success + Success, + + /// + /// An unexpected error occurred while trying to load the settings. + /// + UnexpectedError } } diff --git a/SafeExamBrowser.Contracts/Configuration/Settings/BrowserSettings.cs b/SafeExamBrowser.Contracts/Configuration/Settings/BrowserSettings.cs index ba05c3ab..8e009dc1 100644 --- a/SafeExamBrowser.Contracts/Configuration/Settings/BrowserSettings.cs +++ b/SafeExamBrowser.Contracts/Configuration/Settings/BrowserSettings.cs @@ -26,13 +26,18 @@ namespace SafeExamBrowser.Contracts.Configuration.Settings /// public bool AllowBackwardNavigation { get; set; } + /// + /// Determines whether the user should be allowed to download configuration files. + /// + public bool AllowConfigurationDownloads { get; set; } + /// /// Determines whether the user should be allowed to open the developer console of a browser window. /// public bool AllowDeveloperConsole { get; set; } /// - /// Determines whether the user should be allowed to download files. + /// Determines whether the user should be allowed to download files (excluding configuration files). /// public bool AllowDownloads { get; set; } diff --git a/SafeExamBrowser.Contracts/I18n/TextKey.cs b/SafeExamBrowser.Contracts/I18n/TextKey.cs index 33121f19..79287bfe 100644 --- a/SafeExamBrowser.Contracts/I18n/TextKey.cs +++ b/SafeExamBrowser.Contracts/I18n/TextKey.cs @@ -22,6 +22,10 @@ namespace SafeExamBrowser.Contracts.I18n MessageBox_ClientConfigurationQuestionTitle, MessageBox_ConfigurationDownloadError, MessageBox_ConfigurationDownloadErrorTitle, + MessageBox_InvalidConfigurationData, + MessageBox_InvalidConfigurationDataTitle, + MessageBox_NotSupportedConfigurationResource, + MessageBox_NotSupportedConfigurationResourceTitle, MessageBox_Quit, MessageBox_QuitTitle, MessageBox_QuitError, @@ -42,6 +46,8 @@ namespace SafeExamBrowser.Contracts.I18n MessageBox_SingleInstanceTitle, MessageBox_StartupError, MessageBox_StartupErrorTitle, + MessageBox_UnexpectedConfigurationError, + MessageBox_UnexpectedConfigurationErrorTitle, Notification_AboutTooltip, Notification_LogTooltip, OperationStatus_CloseRuntimeConnection, diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj index 9224aafd..e2819818 100644 --- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj +++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj @@ -53,6 +53,7 @@ + diff --git a/SafeExamBrowser.I18n/Text.xml b/SafeExamBrowser.I18n/Text.xml index b7b8897a..8fbbc7d1 100644 --- a/SafeExamBrowser.I18n/Text.xml +++ b/SafeExamBrowser.I18n/Text.xml @@ -24,6 +24,18 @@ Download Error + + The configuration resource '%%URI%%' contains invalid data! + + + Configuration Error + + + The configuration resource '%%URI%%' is not supported! + + + Configuration Error + Would you really like to quit the application? @@ -78,6 +90,12 @@ Startup Error + + An unexpected error occurred while trying to load configuration resource '%%URI%%'! Please consult the application log for more information... + + + Configuration Error + About Safe Exam Browser diff --git a/SafeExamBrowser.Runtime.UnitTests/Operations/ConfigurationOperationTests.cs b/SafeExamBrowser.Runtime.UnitTests/Operations/ConfigurationOperationTests.cs index 816c2f1b..57281a9a 100644 --- a/SafeExamBrowser.Runtime.UnitTests/Operations/ConfigurationOperationTests.cs +++ b/SafeExamBrowser.Runtime.UnitTests/Operations/ConfigurationOperationTests.cs @@ -26,7 +26,6 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations private AppConfig appConfig; private Mock logger; private Mock repository; - private Mock resourceLoader; private Mock session; private SessionContext sessionContext; private Settings settings; @@ -39,7 +38,6 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations appConfig = new AppConfig(); logger = new Mock(); repository = new Mock(); - resourceLoader = new Mock(); session = new Mock(); sessionContext = new SessionContext(); settings = new Settings(); @@ -64,7 +62,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.Success); - sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.Perform(); var resource = new Uri(url); @@ -82,7 +80,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.Success); - sut = new ConfigurationOperation(null, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); sut.Perform(); var resource = new Uri(Path.Combine(location, "SettingsDummy.txt")); @@ -99,7 +97,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.Success); - sut = new ConfigurationOperation(null, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); sut.Perform(); var resource = new Uri(Path.Combine(location, "SettingsDummy.txt")); @@ -110,10 +108,19 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations [TestMethod] public void MustFallbackToDefaultsAsLastPrio() { - sut = new ConfigurationOperation(null, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + var actualSettings = default(Settings); + var defaultSettings = new Settings(); + + repository.Setup(r => r.LoadDefaultSettings()).Returns(defaultSettings); + session.SetupSet(s => s.Settings = It.IsAny()).Callback(s => actualSettings = s); + + sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); sut.Perform(); repository.Verify(r => r.LoadDefaultSettings(), Times.Once); + session.VerifySet(s => s.Settings = defaultSettings); + + Assert.AreSame(defaultSettings, actualSettings); } [TestMethod] @@ -122,7 +129,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations appConfig.ProgramDataFolder = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations)); repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.Success); - sut = new ConfigurationOperation(null, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => { if (args is ConfigurationCompletedEventArgs c) @@ -141,7 +148,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations { repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.Success); - sut = new ConfigurationOperation(null, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => { if (args is ConfigurationCompletedEventArgs c) @@ -161,7 +168,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations settings.ConfigurationMode = ConfigurationMode.Exam; repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.Success); - sut = new ConfigurationOperation(null, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => { if (args is ConfigurationCompletedEventArgs c) @@ -176,15 +183,25 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations [TestMethod] public void MustNotFailWithoutCommandLineArgs() { - repository.Setup(r => r.LoadDefaultSettings()); + var actualSettings = default(Settings); + var defaultSettings = new Settings(); - sut = new ConfigurationOperation(null, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + repository.Setup(r => r.LoadDefaultSettings()).Returns(defaultSettings); + session.SetupSet(s => s.Settings = It.IsAny()).Callback(s => actualSettings = s); + + sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); sut.Perform(); - sut = new ConfigurationOperation(new string[] { }, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + repository.Verify(r => r.LoadDefaultSettings(), Times.Once); + + Assert.AreSame(defaultSettings, actualSettings); + + sut = new ConfigurationOperation(new string[] { }, repository.Object, logger.Object, sessionContext); sut.Perform(); repository.Verify(r => r.LoadDefaultSettings(), Times.Exactly(2)); + + Assert.AreSame(defaultSettings, actualSettings); } [TestMethod] @@ -192,7 +209,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations { var uri = @"an/invalid\uri.'*%yolo/()你好"; - sut = new ConfigurationOperation(new[] { "blubb.exe", uri }, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(new[] { "blubb.exe", uri }, repository.Object, logger.Object, sessionContext); sut.Perform(); } @@ -203,7 +220,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.AdminPasswordNeeded); - sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => { if (args is PasswordRequiredEventArgs p) @@ -224,7 +241,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.SettingsPasswordNeeded); - sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => { if (args is PasswordRequiredEventArgs p) @@ -247,7 +264,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.AdminPasswordNeeded); repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, password, null)).Returns(LoadStatus.Success); - sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => { if (args is PasswordRequiredEventArgs p) @@ -272,7 +289,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.SettingsPasswordNeeded); repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, password)).Returns(LoadStatus.Success); - sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => { if (args is PasswordRequiredEventArgs p) @@ -295,7 +312,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.AdminPasswordNeeded); - sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => { if (args is PasswordRequiredEventArgs p) @@ -316,7 +333,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.SettingsPasswordNeeded); - sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => { if (args is PasswordRequiredEventArgs p) @@ -341,7 +358,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, settingsPassword)).Returns(LoadStatus.AdminPasswordNeeded); repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, adminPassword, settingsPassword)).Returns(LoadStatus.Success); - sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, sessionContext); sut.ActionRequired += args => { if (args is PasswordRequiredEventArgs p) @@ -358,37 +375,6 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations repository.Verify(r => r.TryLoadSettings(It.IsAny(), out settings, adminPassword, settingsPassword), Times.Once); } - [TestMethod] - public void MustHandleInvalidData() - { - var url = @"http://www.safeexambrowser.org/whatever.seb"; - - resourceLoader.Setup(r => r.IsHtmlResource(It.IsAny())).Returns(false); - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.InvalidData); - - sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, resourceLoader.Object, sessionContext); - - var result = sut.Perform(); - - Assert.AreEqual(OperationResult.Failed, result); - } - - [TestMethod] - public void MustHandleHtmlAsInvalidData() - { - var url = "http://www.blubb.org/some/resource.html"; - - resourceLoader.Setup(r => r.IsHtmlResource(It.IsAny())).Returns(true); - repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.InvalidData); - - sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, logger.Object, resourceLoader.Object, sessionContext); - - var result = sut.Perform(); - - Assert.AreEqual(OperationResult.Success, result); - Assert.AreEqual(url, settings.Browser.StartUrl); - } - [TestMethod] public void MustReconfigureSuccessfullyWithCorrectUri() { @@ -398,7 +384,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations sessionContext.ReconfigurationFilePath = resource.AbsolutePath; repository.Setup(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, null, null)).Returns(LoadStatus.Success); - sut = new ConfigurationOperation(null, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); var result = sut.Repeat(); @@ -415,7 +401,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations sessionContext.ReconfigurationFilePath = null; repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, null, null)).Returns(LoadStatus.Success); - sut = new ConfigurationOperation(null, repository.Object, logger.Object, resourceLoader.Object, sessionContext); + sut = new ConfigurationOperation(null, repository.Object, logger.Object, sessionContext); var result = sut.Repeat(); diff --git a/SafeExamBrowser.Runtime.UnitTests/app.config b/SafeExamBrowser.Runtime.UnitTests/app.config index 7d8c9227..7b7e0134 100644 --- a/SafeExamBrowser.Runtime.UnitTests/app.config +++ b/SafeExamBrowser.Runtime.UnitTests/app.config @@ -6,6 +6,10 @@ + + + + \ No newline at end of file diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs index 6f52867c..54b9b78a 100644 --- a/SafeExamBrowser.Runtime/CompositionRoot.cs +++ b/SafeExamBrowser.Runtime/CompositionRoot.cs @@ -13,6 +13,8 @@ using System.Reflection; using SafeExamBrowser.Communication.Hosts; using SafeExamBrowser.Communication.Proxies; using SafeExamBrowser.Configuration; +using SafeExamBrowser.Configuration.DataFormats; +using SafeExamBrowser.Configuration.ResourceLoaders; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Core; using SafeExamBrowser.Contracts.Core.OperationModel; @@ -32,6 +34,7 @@ namespace SafeExamBrowser.Runtime internal class CompositionRoot { private AppConfig appConfig; + private IConfigurationRepository configuration; private ILogger logger; private ISystemInfo systemInfo; private IText text; @@ -45,13 +48,12 @@ namespace SafeExamBrowser.Runtime const int FIFTEEN_SECONDS = 15000; var args = Environment.GetCommandLineArgs(); - var configuration = BuildConfigurationRepository(); var nativeMethods = new NativeMethods(); logger = new Logger(); - appConfig = configuration.InitializeAppConfig(); systemInfo = new SystemInfo(); + InitializeConfiguration(); InitializeLogging(); InitializeText(); @@ -60,7 +62,6 @@ namespace SafeExamBrowser.Runtime var explorerShell = new ExplorerShell(new ModuleLogger(logger, nameof(ExplorerShell)), nativeMethods); var processFactory = new ProcessFactory(new ModuleLogger(logger, nameof(ProcessFactory))); var proxyFactory = new ProxyFactory(new ProxyObjectFactory(), logger); - var resourceLoader = new ResourceLoader(); var runtimeHost = new RuntimeHost(appConfig.RuntimeAddress, new HostObjectFactory(), new ModuleLogger(logger, nameof(RuntimeHost)), FIVE_SECONDS); var serviceProxy = new ServiceProxy(appConfig.ServiceAddress, new ProxyObjectFactory(), new ModuleLogger(logger, nameof(ServiceProxy))); var sessionContext = new SessionContext(); @@ -73,7 +74,7 @@ namespace SafeExamBrowser.Runtime bootstrapOperations.Enqueue(new CommunicationHostOperation(runtimeHost, logger)); sessionOperations.Enqueue(new SessionInitializationOperation(configuration, logger, runtimeHost, sessionContext)); - sessionOperations.Enqueue(new ConfigurationOperation(args, configuration, logger, resourceLoader, sessionContext)); + sessionOperations.Enqueue(new ConfigurationOperation(args, configuration, logger, sessionContext)); sessionOperations.Enqueue(new ClientTerminationOperation(logger, processFactory, proxyFactory, runtimeHost, sessionContext, FIFTEEN_SECONDS)); sessionOperations.Enqueue(new KioskModeTerminationOperation(desktopFactory, explorerShell, logger, processFactory, sessionContext)); sessionOperations.Enqueue(new ServiceOperation(logger, serviceProxy, sessionContext)); @@ -105,15 +106,22 @@ namespace SafeExamBrowser.Runtime logger?.Log($"# Application terminated at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); } - private IConfigurationRepository BuildConfigurationRepository() + private void InitializeConfiguration() { var executable = Assembly.GetExecutingAssembly(); var programCopyright = executable.GetCustomAttribute().Copyright; var programTitle = executable.GetCustomAttribute().Title; var programVersion = executable.GetCustomAttribute().InformationalVersion; - var repository = new ConfigurationRepository(executable.Location, programCopyright, programTitle, programVersion); + var moduleLogger = new ModuleLogger(logger, nameof(ConfigurationRepository)); - return repository; + configuration = new ConfigurationRepository(moduleLogger, executable.Location, programCopyright, programTitle, programVersion); + appConfig = configuration.InitializeAppConfig(); + + configuration.Register(new DefaultFormat(new ModuleLogger(logger, nameof(DefaultFormat)))); + configuration.Register(new HtmlFormat(new ModuleLogger(logger, nameof(HtmlFormat)))); + configuration.Register(new XmlFormat(new ModuleLogger(logger, nameof(XmlFormat)))); + configuration.Register(new FileResourceLoader(new ModuleLogger(logger, nameof(FileResourceLoader)))); + configuration.Register(new NetworkResourceLoader(appConfig, new ModuleLogger(logger, nameof(NetworkResourceLoader)))); } private void InitializeLogging() diff --git a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs index 3270588e..fb8f291d 100644 --- a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs @@ -24,7 +24,6 @@ namespace SafeExamBrowser.Runtime.Operations private string[] commandLineArgs; private IConfigurationRepository configuration; private ILogger logger; - private IResourceLoader resourceLoader; public override event ActionRequiredEventHandler ActionRequired; public override event StatusChangedEventHandler StatusChanged; @@ -33,13 +32,11 @@ namespace SafeExamBrowser.Runtime.Operations string[] commandLineArgs, IConfigurationRepository configuration, ILogger logger, - IResourceLoader resourceLoader, SessionContext sessionContext) : base(sessionContext) { this.commandLineArgs = commandLineArgs; this.logger = logger; this.configuration = configuration; - this.resourceLoader = resourceLoader; } public override OperationResult Perform() @@ -51,7 +48,7 @@ namespace SafeExamBrowser.Runtime.Operations if (isValidUri) { - logger.Info($"Attempting to load settings from '{uri.AbsolutePath}'..."); + logger.Info($"Attempting to load settings from '{uri}'..."); var result = LoadSettings(uri); @@ -62,7 +59,7 @@ namespace SafeExamBrowser.Runtime.Operations } logger.Info("No valid settings resource specified nor found in PROGRAMDATA or APPDATA - loading default settings..."); - configuration.LoadDefaultSettings(); + Context.Next.Settings = configuration.LoadDefaultSettings(); return OperationResult.Success; } @@ -76,7 +73,7 @@ namespace SafeExamBrowser.Runtime.Operations if (isValidUri) { - logger.Info($"Attempting to load settings from '{uri.AbsolutePath}'..."); + logger.Info($"Attempting to load settings from '{uri}'..."); var result = LoadSettings(uri); @@ -126,11 +123,6 @@ namespace SafeExamBrowser.Runtime.Operations } } - if (status == LoadStatus.InvalidData) - { - HandleInvalidData(ref status, uri); - } - if (status == LoadStatus.Success) { Context.Next.Settings = settings; @@ -138,6 +130,18 @@ namespace SafeExamBrowser.Runtime.Operations return OperationResult.Success; } + switch (status) + { + case LoadStatus.InvalidData: + ActionRequired?.Invoke(new InvalidDataMessageArgs(uri.ToString())); + break; + case LoadStatus.NotSupported: + ActionRequired?.Invoke(new NotSupportedMessageArgs(uri.ToString())); + break; + case LoadStatus.UnexpectedError: + ActionRequired?.Invoke(new UnexpectedErrorMessageArgs(uri.ToString())); + break; + } return OperationResult.Failed; } @@ -152,22 +156,6 @@ namespace SafeExamBrowser.Runtime.Operations return args; } - private void HandleInvalidData(ref LoadStatus status, Uri uri) - { - if (resourceLoader.IsHtmlResource(uri)) - { - configuration.LoadDefaultSettings(); - Context.Next.Settings.Browser.StartUrl = uri.AbsoluteUri; - logger.Info($"The specified URI '{uri.AbsoluteUri}' appears to point to a HTML resource, setting it as startup URL."); - - status = LoadStatus.Success; - } - else - { - logger.Error($"The specified settings resource '{uri.AbsoluteUri}' is invalid!"); - } - } - private bool TryInitializeSettingsUri(out Uri uri) { var path = string.Empty; diff --git a/SafeExamBrowser.Runtime/Operations/Events/InvalidDataMessageArgs.cs b/SafeExamBrowser.Runtime/Operations/Events/InvalidDataMessageArgs.cs new file mode 100644 index 00000000..33749136 --- /dev/null +++ b/SafeExamBrowser.Runtime/Operations/Events/InvalidDataMessageArgs.cs @@ -0,0 +1,24 @@ +/* + * 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.I18n; +using SafeExamBrowser.Contracts.UserInterface.MessageBox; + +namespace SafeExamBrowser.Runtime.Operations.Events +{ + internal class InvalidDataMessageArgs : MessageEventArgs + { + internal InvalidDataMessageArgs(string uri) + { + Icon = MessageBoxIcon.Error; + Message = TextKey.MessageBox_InvalidConfigurationData; + MessagePlaceholders["%%URI%%"] = uri; + Title = TextKey.MessageBox_InvalidConfigurationDataTitle; + } + } +} diff --git a/SafeExamBrowser.Runtime/Operations/Events/MessageEventArgs.cs b/SafeExamBrowser.Runtime/Operations/Events/MessageEventArgs.cs new file mode 100644 index 00000000..b5a4f857 --- /dev/null +++ b/SafeExamBrowser.Runtime/Operations/Events/MessageEventArgs.cs @@ -0,0 +1,30 @@ +/* + * 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.Collections.Generic; +using SafeExamBrowser.Contracts.Core.OperationModel.Events; +using SafeExamBrowser.Contracts.I18n; +using SafeExamBrowser.Contracts.UserInterface.MessageBox; + +namespace SafeExamBrowser.Runtime.Operations.Events +{ + internal class MessageEventArgs : ActionRequiredEventArgs + { + internal MessageBoxIcon Icon { get; set; } + internal TextKey Message { get; set; } + internal TextKey Title { get; set; } + internal Dictionary MessagePlaceholders { get; private set; } + internal Dictionary TitlePlaceholders { get; private set; } + + public MessageEventArgs() + { + MessagePlaceholders = new Dictionary(); + TitlePlaceholders = new Dictionary(); + } + } +} diff --git a/SafeExamBrowser.Runtime/Operations/Events/NotSupportedMessageArgs.cs b/SafeExamBrowser.Runtime/Operations/Events/NotSupportedMessageArgs.cs new file mode 100644 index 00000000..7f81d255 --- /dev/null +++ b/SafeExamBrowser.Runtime/Operations/Events/NotSupportedMessageArgs.cs @@ -0,0 +1,24 @@ +/* + * 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.I18n; +using SafeExamBrowser.Contracts.UserInterface.MessageBox; + +namespace SafeExamBrowser.Runtime.Operations.Events +{ + internal class NotSupportedMessageArgs : MessageEventArgs + { + internal NotSupportedMessageArgs(string uri) + { + Icon = MessageBoxIcon.Error; + Message = TextKey.MessageBox_NotSupportedConfigurationResource; + MessagePlaceholders["%%URI%%"] = uri; + Title = TextKey.MessageBox_NotSupportedConfigurationResourceTitle; + } + } +} diff --git a/SafeExamBrowser.Runtime/Operations/Events/UnexpectedErrorMessageArgs.cs b/SafeExamBrowser.Runtime/Operations/Events/UnexpectedErrorMessageArgs.cs new file mode 100644 index 00000000..c0725783 --- /dev/null +++ b/SafeExamBrowser.Runtime/Operations/Events/UnexpectedErrorMessageArgs.cs @@ -0,0 +1,24 @@ +/* + * 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.I18n; +using SafeExamBrowser.Contracts.UserInterface.MessageBox; + +namespace SafeExamBrowser.Runtime.Operations.Events +{ + internal class UnexpectedErrorMessageArgs : MessageEventArgs + { + internal UnexpectedErrorMessageArgs(string uri) + { + Icon = MessageBoxIcon.Error; + Message = TextKey.MessageBox_UnexpectedConfigurationError; + MessagePlaceholders["%%URI%%"] = uri; + Title = TextKey.MessageBox_UnexpectedConfigurationErrorTitle; + } + } +} diff --git a/SafeExamBrowser.Runtime/Operations/KioskModeOperation.cs b/SafeExamBrowser.Runtime/Operations/KioskModeOperation.cs index ff6c04a2..63bf1bd0 100644 --- a/SafeExamBrowser.Runtime/Operations/KioskModeOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/KioskModeOperation.cs @@ -35,10 +35,6 @@ namespace SafeExamBrowser.Runtime.Operations set { Context.OriginalDesktop = value; } } - /// - /// TODO: This mechanism exposes the internal state of the operation! Find better solution which will keep the - /// state internal but still allow unit testing of both kiosk mode operations independently! - /// protected KioskMode? ActiveMode { get { return Context.ActiveMode; } diff --git a/SafeExamBrowser.Runtime/RuntimeController.cs b/SafeExamBrowser.Runtime/RuntimeController.cs index 5a362008..059fa4b2 100644 --- a/SafeExamBrowser.Runtime/RuntimeController.cs +++ b/SafeExamBrowser.Runtime/RuntimeController.cs @@ -363,6 +363,9 @@ namespace SafeExamBrowser.Runtime case ConfigurationCompletedEventArgs a: AskIfConfigurationSufficient(a); break; + case MessageEventArgs m: + ShowMessageBox(m); + break; case PasswordRequiredEventArgs p: AskForPassword(p); break; @@ -393,6 +396,24 @@ namespace SafeExamBrowser.Runtime } } + private void ShowMessageBox(MessageEventArgs args) + { + var message = text.Get(args.Message); + var title = text.Get(args.Title); + + foreach (var placeholder in args.MessagePlaceholders) + { + message = message.Replace(placeholder.Key, placeholder.Value); + } + + foreach (var placeholder in args.TitlePlaceholders) + { + title = title.Replace(placeholder.Key, placeholder.Value); + } + + messageBox.Show(message, title, MessageBoxAction.Confirm, args.Icon, runtimeWindow); + } + private void TryGetPasswordViaDialog(PasswordRequiredEventArgs args) { var isAdmin = args.Purpose == PasswordRequestPurpose.Administrator; diff --git a/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj b/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj index baeaea2b..df5110e6 100644 --- a/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj +++ b/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj @@ -90,7 +90,11 @@ + + + + diff --git a/SafeExamBrowser.WindowsApi/ExplorerShell.cs b/SafeExamBrowser.WindowsApi/ExplorerShell.cs index f7b7996a..8112d56c 100644 --- a/SafeExamBrowser.WindowsApi/ExplorerShell.cs +++ b/SafeExamBrowser.WindowsApi/ExplorerShell.cs @@ -64,6 +64,7 @@ namespace SafeExamBrowser.WindowsApi logger.Info($"Restored window '{window.Title}' with handle = {window.Handle}."); } + minimizedWindows.Clear(); logger.Info("Minimized windows successfully restored."); }