2018-01-18 15:14:05 +01:00
|
|
|
|
/*
|
|
|
|
|
* 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;
|
2018-06-27 14:02:16 +02:00
|
|
|
|
using System.Threading;
|
2018-03-06 11:49:51 +01:00
|
|
|
|
using SafeExamBrowser.Contracts.Behaviour.OperationModel;
|
2018-06-27 14:02:16 +02:00
|
|
|
|
using SafeExamBrowser.Contracts.Communication.Data;
|
|
|
|
|
using SafeExamBrowser.Contracts.Communication.Events;
|
|
|
|
|
using SafeExamBrowser.Contracts.Communication.Hosts;
|
2018-01-18 15:14:05 +01:00
|
|
|
|
using SafeExamBrowser.Contracts.Configuration;
|
|
|
|
|
using SafeExamBrowser.Contracts.Configuration.Settings;
|
|
|
|
|
using SafeExamBrowser.Contracts.I18n;
|
|
|
|
|
using SafeExamBrowser.Contracts.Logging;
|
|
|
|
|
using SafeExamBrowser.Contracts.UserInterface;
|
2018-03-14 12:07:20 +01:00
|
|
|
|
using SafeExamBrowser.Contracts.UserInterface.MessageBox;
|
2018-01-18 15:14:05 +01:00
|
|
|
|
|
|
|
|
|
namespace SafeExamBrowser.Runtime.Behaviour.Operations
|
|
|
|
|
{
|
|
|
|
|
internal class ConfigurationOperation : IOperation
|
|
|
|
|
{
|
2018-02-06 15:12:11 +01:00
|
|
|
|
private IConfigurationRepository repository;
|
2018-01-18 15:14:05 +01:00
|
|
|
|
private ILogger logger;
|
2018-03-14 12:07:20 +01:00
|
|
|
|
private IMessageBox messageBox;
|
2018-06-28 13:40:30 +02:00
|
|
|
|
private IResourceLoader resourceLoader;
|
2018-06-27 14:02:16 +02:00
|
|
|
|
private IRuntimeHost runtimeHost;
|
2018-03-21 10:23:15 +01:00
|
|
|
|
private RuntimeInfo runtimeInfo;
|
2018-06-27 14:02:16 +02:00
|
|
|
|
private IText text;
|
|
|
|
|
private IUserInterfaceFactory uiFactory;
|
2018-01-18 15:14:05 +01:00
|
|
|
|
private string[] commandLineArgs;
|
|
|
|
|
|
2018-02-02 09:18:35 +01:00
|
|
|
|
public IProgressIndicator ProgressIndicator { private get; set; }
|
2018-01-18 15:14:05 +01:00
|
|
|
|
|
|
|
|
|
public ConfigurationOperation(
|
2018-02-06 15:12:11 +01:00
|
|
|
|
IConfigurationRepository repository,
|
2018-01-18 15:14:05 +01:00
|
|
|
|
ILogger logger,
|
2018-03-14 12:07:20 +01:00
|
|
|
|
IMessageBox messageBox,
|
2018-06-28 13:40:30 +02:00
|
|
|
|
IResourceLoader resourceLoader,
|
2018-06-27 14:02:16 +02:00
|
|
|
|
IRuntimeHost runtimeHost,
|
2018-02-15 15:42:54 +01:00
|
|
|
|
RuntimeInfo runtimeInfo,
|
2018-01-19 09:23:09 +01:00
|
|
|
|
IText text,
|
2018-06-27 14:02:16 +02:00
|
|
|
|
IUserInterfaceFactory uiFactory,
|
2018-01-18 15:14:05 +01:00
|
|
|
|
string[] commandLineArgs)
|
|
|
|
|
{
|
2018-02-06 15:12:11 +01:00
|
|
|
|
this.repository = repository;
|
2018-01-18 15:14:05 +01:00
|
|
|
|
this.logger = logger;
|
2018-03-14 12:07:20 +01:00
|
|
|
|
this.messageBox = messageBox;
|
2018-06-28 13:40:30 +02:00
|
|
|
|
this.resourceLoader = resourceLoader;
|
2018-06-27 14:02:16 +02:00
|
|
|
|
this.runtimeHost = runtimeHost;
|
2018-01-18 15:14:05 +01:00
|
|
|
|
this.runtimeInfo = runtimeInfo;
|
2018-01-19 09:23:09 +01:00
|
|
|
|
this.text = text;
|
2018-06-27 14:02:16 +02:00
|
|
|
|
this.uiFactory = uiFactory;
|
2018-06-28 13:40:30 +02:00
|
|
|
|
this.commandLineArgs = commandLineArgs;
|
2018-01-18 15:14:05 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-28 15:49:06 +01:00
|
|
|
|
public OperationResult Perform()
|
2018-01-18 15:14:05 +01:00
|
|
|
|
{
|
|
|
|
|
logger.Info("Initializing application configuration...");
|
2018-02-02 09:30:41 +01:00
|
|
|
|
ProgressIndicator?.UpdateText(TextKey.ProgressIndicator_InitializeConfiguration);
|
2018-01-18 15:14:05 +01:00
|
|
|
|
|
2018-06-21 07:56:25 +02:00
|
|
|
|
var isValidUri = TryInitializeSettingsUri(out Uri uri);
|
2018-01-18 15:14:05 +01:00
|
|
|
|
|
|
|
|
|
if (isValidUri)
|
|
|
|
|
{
|
2018-06-27 14:02:16 +02:00
|
|
|
|
logger.Info($"Attempting to load settings from '{uri.AbsolutePath}'...");
|
2018-02-28 15:49:06 +01:00
|
|
|
|
|
2018-06-21 07:56:25 +02:00
|
|
|
|
var result = LoadSettings(uri);
|
2018-02-28 15:49:06 +01:00
|
|
|
|
|
2018-06-27 14:02:16 +02:00
|
|
|
|
HandleClientConfiguration(ref result);
|
2018-06-21 07:56:25 +02:00
|
|
|
|
LogOperationResult(result);
|
|
|
|
|
|
|
|
|
|
return result;
|
2018-01-18 15:14:05 +01:00
|
|
|
|
}
|
2018-02-28 15:49:06 +01:00
|
|
|
|
|
2018-06-21 07:56:25 +02:00
|
|
|
|
logger.Info("No valid settings resource specified nor found in PROGRAMDATA or APPDATA - loading default settings...");
|
|
|
|
|
repository.LoadDefaultSettings();
|
|
|
|
|
|
2018-02-28 15:49:06 +01:00
|
|
|
|
return OperationResult.Success;
|
2018-01-18 15:14:05 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-28 15:49:06 +01:00
|
|
|
|
public OperationResult Repeat()
|
2018-02-01 08:37:12 +01:00
|
|
|
|
{
|
2018-06-21 07:56:25 +02:00
|
|
|
|
logger.Info("Initializing new application configuration...");
|
|
|
|
|
ProgressIndicator?.UpdateText(TextKey.ProgressIndicator_InitializeConfiguration);
|
2018-02-28 15:49:06 +01:00
|
|
|
|
|
2018-06-21 07:56:25 +02:00
|
|
|
|
var isValidUri = TryValidateSettingsUri(repository.ReconfigurationFilePath, out Uri uri);
|
|
|
|
|
|
|
|
|
|
if (isValidUri)
|
|
|
|
|
{
|
2018-06-27 14:02:16 +02:00
|
|
|
|
logger.Info($"Attempting to load settings from '{uri.AbsolutePath}'...");
|
2018-06-21 07:56:25 +02:00
|
|
|
|
|
|
|
|
|
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;
|
2018-02-01 08:37:12 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-18 15:14:05 +01:00
|
|
|
|
public void Revert()
|
|
|
|
|
{
|
|
|
|
|
// Nothing to do here...
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-21 07:56:25 +02:00
|
|
|
|
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;)
|
|
|
|
|
{
|
2018-06-27 14:02:16 +02:00
|
|
|
|
status = repository.LoadSettings(uri, adminPassword, settingsPassword);
|
2018-06-21 07:56:25 +02:00
|
|
|
|
|
2018-06-21 11:07:46 +02:00
|
|
|
|
if (status == LoadStatus.AdminPasswordNeeded || status == LoadStatus.SettingsPasswordNeeded)
|
2018-06-21 07:56:25 +02:00
|
|
|
|
{
|
2018-06-27 14:02:16 +02:00
|
|
|
|
var purpose = status == LoadStatus.AdminPasswordNeeded ? PasswordRequestPurpose.Administrator : PasswordRequestPurpose.Settings;
|
|
|
|
|
var aborted = !TryGetPassword(purpose, out string password);
|
2018-06-21 07:56:25 +02:00
|
|
|
|
|
2018-06-27 14:02:16 +02:00
|
|
|
|
adminAttempts += purpose == PasswordRequestPurpose.Administrator ? 1 : 0;
|
|
|
|
|
adminPassword = purpose == PasswordRequestPurpose.Administrator ? password : adminPassword;
|
|
|
|
|
settingsAttempts += purpose == PasswordRequestPurpose.Settings ? 1 : 0;
|
|
|
|
|
settingsPassword = purpose == PasswordRequestPurpose.Settings ? password : settingsPassword;
|
|
|
|
|
|
|
|
|
|
if (aborted)
|
2018-06-21 07:56:25 +02:00
|
|
|
|
{
|
|
|
|
|
return OperationResult.Aborted;
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-06-21 11:07:46 +02:00
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
2018-06-21 07:56:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status == LoadStatus.InvalidData)
|
|
|
|
|
{
|
2018-06-27 14:02:16 +02:00
|
|
|
|
HandleInvalidData(ref status, uri);
|
2018-06-21 07:56:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return status == LoadStatus.Success ? OperationResult.Success : OperationResult.Failed;
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-27 14:02:16 +02:00
|
|
|
|
private bool TryGetPassword(PasswordRequestPurpose purpose, out string password)
|
2018-06-21 07:56:25 +02:00
|
|
|
|
{
|
2018-06-27 14:02:16 +02:00
|
|
|
|
var isStartup = repository.CurrentSession == null;
|
|
|
|
|
var isRunningOnDefaultDesktop = repository.CurrentSettings?.KioskMode == KioskMode.DisableExplorerShell;
|
|
|
|
|
|
|
|
|
|
if (isStartup || isRunningOnDefaultDesktop)
|
|
|
|
|
{
|
|
|
|
|
return TryGetPasswordViaDialog(purpose, out password);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
return TryGetPasswordViaClient(purpose, out password);
|
|
|
|
|
}
|
2018-06-21 07:56:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-27 14:02:16 +02:00
|
|
|
|
private bool TryGetPasswordViaDialog(PasswordRequestPurpose purpose, out string password)
|
2018-06-21 07:56:25 +02:00
|
|
|
|
{
|
2018-06-27 14:02:16 +02:00
|
|
|
|
var isAdmin = purpose == PasswordRequestPurpose.Administrator;
|
|
|
|
|
var message = isAdmin ? TextKey.PasswordDialog_AdminPasswordRequired : TextKey.PasswordDialog_SettingsPasswordRequired;
|
|
|
|
|
var title = isAdmin ? TextKey.PasswordDialog_AdminPasswordRequiredTitle : TextKey.PasswordDialog_SettingsPasswordRequiredTitle;
|
|
|
|
|
var dialog = uiFactory.CreatePasswordDialog(text.Get(message), text.Get(title));
|
|
|
|
|
var result = dialog.Show();
|
2018-06-21 07:56:25 +02:00
|
|
|
|
|
2018-06-27 14:02:16 +02:00
|
|
|
|
if (result.Success)
|
|
|
|
|
{
|
|
|
|
|
password = result.Password;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
password = default(string);
|
|
|
|
|
}
|
2018-06-21 07:56:25 +02:00
|
|
|
|
|
2018-06-27 14:02:16 +02:00
|
|
|
|
return result.Success;
|
2018-06-21 07:56:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-27 14:02:16 +02:00
|
|
|
|
private bool TryGetPasswordViaClient(PasswordRequestPurpose purpose, out string password)
|
2018-06-21 07:56:25 +02:00
|
|
|
|
{
|
2018-06-27 14:02:16 +02:00
|
|
|
|
var requestId = Guid.NewGuid();
|
|
|
|
|
var response = default(PasswordEventArgs);
|
|
|
|
|
var responseEvent = new AutoResetEvent(false);
|
|
|
|
|
var responseEventHandler = new CommunicationEventHandler<PasswordEventArgs>((args) =>
|
|
|
|
|
{
|
|
|
|
|
if (args.RequestId == requestId)
|
|
|
|
|
{
|
|
|
|
|
response = args;
|
|
|
|
|
responseEvent.Set();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
runtimeHost.PasswordReceived += responseEventHandler;
|
|
|
|
|
repository.CurrentSession.ClientProxy.RequestPassword(purpose, requestId);
|
|
|
|
|
responseEvent.WaitOne();
|
|
|
|
|
runtimeHost.PasswordReceived -= responseEventHandler;
|
|
|
|
|
|
|
|
|
|
if (response.Success)
|
|
|
|
|
{
|
|
|
|
|
password = response.Password;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
password = default(string);
|
|
|
|
|
}
|
2018-06-21 07:56:25 +02:00
|
|
|
|
|
2018-06-27 14:02:16 +02:00
|
|
|
|
return response.Success;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void HandleInvalidData(ref LoadStatus status, Uri uri)
|
|
|
|
|
{
|
2018-06-28 13:40:30 +02:00
|
|
|
|
if (resourceLoader.IsHtmlResource(uri))
|
2018-06-27 14:02:16 +02:00
|
|
|
|
{
|
|
|
|
|
repository.LoadDefaultSettings();
|
|
|
|
|
repository.CurrentSettings.Browser.StartUrl = uri.AbsoluteUri;
|
2018-06-28 13:40:30 +02:00
|
|
|
|
logger.Info($"The specified URI '{uri.AbsoluteUri}' appears to point to a HTML resource, setting it as startup URL.");
|
2018-06-27 14:02:16 +02:00
|
|
|
|
|
|
|
|
|
status = LoadStatus.Success;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
logger.Error($"The specified settings resource '{uri.AbsoluteUri}' is invalid!");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-21 07:56:25 +02:00
|
|
|
|
private bool TryInitializeSettingsUri(out Uri uri)
|
2018-01-18 15:14:05 +01:00
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
2018-01-19 14:04:12 +01:00
|
|
|
|
|
2018-06-21 07:56:25 +02:00
|
|
|
|
private bool TryValidateSettingsUri(string path, out Uri uri)
|
2018-03-21 10:23:15 +01:00
|
|
|
|
{
|
2018-06-21 07:56:25 +02:00
|
|
|
|
var isValidUri = Uri.TryCreate(path, UriKind.Absolute, out uri);
|
2018-03-21 10:23:15 +01:00
|
|
|
|
|
2018-06-21 07:56:25 +02:00
|
|
|
|
isValidUri &= uri != null && uri.IsFile;
|
|
|
|
|
isValidUri &= File.Exists(path);
|
2018-03-21 10:23:15 +01:00
|
|
|
|
|
2018-06-21 07:56:25 +02:00
|
|
|
|
return isValidUri;
|
2018-03-21 10:23:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-27 14:02:16 +02:00
|
|
|
|
private void HandleClientConfiguration(ref OperationResult result)
|
|
|
|
|
{
|
|
|
|
|
if (result == OperationResult.Success && repository.CurrentSettings.ConfigurationMode == ConfigurationMode.ConfigureClient)
|
|
|
|
|
{
|
|
|
|
|
var abort = IsConfigurationSufficient();
|
|
|
|
|
|
|
|
|
|
logger.Info($"The user chose to {(abort ? "abort" : "continue")} after successful client configuration.");
|
|
|
|
|
|
|
|
|
|
if (abort)
|
|
|
|
|
{
|
|
|
|
|
result = OperationResult.Aborted;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-22 16:15:06 +01:00
|
|
|
|
private bool IsConfigurationSufficient()
|
2018-01-19 14:04:12 +01:00
|
|
|
|
{
|
2018-03-08 15:27:12 +01:00
|
|
|
|
var message = text.Get(TextKey.MessageBox_ClientConfigurationQuestion);
|
|
|
|
|
var title = text.Get(TextKey.MessageBox_ClientConfigurationQuestionTitle);
|
2018-03-14 12:07:20 +01:00
|
|
|
|
var abort = messageBox.Show(message, title, MessageBoxAction.YesNo, MessageBoxIcon.Question);
|
2018-01-19 14:04:12 +01:00
|
|
|
|
|
2018-01-23 15:33:54 +01:00
|
|
|
|
return abort == MessageBoxResult.Yes;
|
2018-01-19 14:04:12 +01:00
|
|
|
|
}
|
2018-06-21 07:56:25 +02:00
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-18 15:14:05 +01:00
|
|
|
|
}
|
|
|
|
|
}
|