diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.cs
index 7083ffd6..d800b638 100644
--- a/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.cs
+++ b/SafeExamBrowser.Configuration/ConfigurationData/DataMapper.cs
@@ -23,6 +23,7 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
new GeneralDataMapper(),
new InputDataMapper(),
new SecurityDataMapper(),
+ new ServerDataMapper(),
new ServiceDataMapper(),
new UserInterfaceDataMapper()
};
diff --git a/SafeExamBrowser.I18n.Contracts/TextKey.cs b/SafeExamBrowser.I18n.Contracts/TextKey.cs
index ba794121..173de3a4 100644
--- a/SafeExamBrowser.I18n.Contracts/TextKey.cs
+++ b/SafeExamBrowser.I18n.Contracts/TextKey.cs
@@ -31,6 +31,10 @@ namespace SafeExamBrowser.I18n.Contracts
BrowserWindow_DownloadComplete,
BrowserWindow_ZoomMenuItem,
Build,
+ ExamSelectionDialog_Cancel,
+ ExamSelectionDialog_Message,
+ ExamSelectionDialog_Select,
+ ExamSelectionDialog_Title,
FileSystemDialog_Cancel,
FileSystemDialog_LoadError,
FileSystemDialog_Loading,
diff --git a/SafeExamBrowser.I18n/Data/de.xml b/SafeExamBrowser.I18n/Data/de.xml
index 13cb99c5..4c55906d 100644
--- a/SafeExamBrowser.I18n/Data/de.xml
+++ b/SafeExamBrowser.I18n/Data/de.xml
@@ -51,6 +51,18 @@
Build
+
+ Abbrechen
+
+
+ Bitte wählen Sie eine der verfügbaren SEB-Server-Prüfungen:
+
+
+ Auswählen
+
+
+ SEB-Server-Prüfungen
+
Abbrechen
diff --git a/SafeExamBrowser.I18n/Data/en.xml b/SafeExamBrowser.I18n/Data/en.xml
index 913063c0..348b38fc 100644
--- a/SafeExamBrowser.I18n/Data/en.xml
+++ b/SafeExamBrowser.I18n/Data/en.xml
@@ -51,6 +51,18 @@
Build
+
+ Cancel
+
+
+ Please select one of the available SEB-Server exams:
+
+
+ Select
+
+
+ SEB-Server Exams
+
Cancel
diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs
index fa5757a6..4ed0bade 100644
--- a/SafeExamBrowser.Runtime/CompositionRoot.cs
+++ b/SafeExamBrowser.Runtime/CompositionRoot.cs
@@ -67,7 +67,7 @@ namespace SafeExamBrowser.Runtime
var proxyFactory = new ProxyFactory(new ProxyObjectFactory(), ModuleLogger(nameof(ProxyFactory)));
var runtimeHost = new RuntimeHost(appConfig.RuntimeAddress, new HostObjectFactory(), ModuleLogger(nameof(RuntimeHost)), FIVE_SECONDS);
var runtimeWindow = uiFactory.CreateRuntimeWindow(appConfig);
- var server = new ServerProxy();
+ var server = new ServerProxy(ModuleLogger(nameof(ServerProxy)));
var serviceProxy = new ServiceProxy(appConfig.ServiceAddress, new ProxyObjectFactory(), ModuleLogger(nameof(ServiceProxy)), Interlocutor.Runtime);
var sessionContext = new SessionContext();
var splashScreen = uiFactory.CreateSplashScreen(appConfig);
diff --git a/SafeExamBrowser.Runtime/Operations/Events/ExamSelectionEventArgs.cs b/SafeExamBrowser.Runtime/Operations/Events/ExamSelectionEventArgs.cs
index ef2af434..d469ec0b 100644
--- a/SafeExamBrowser.Runtime/Operations/Events/ExamSelectionEventArgs.cs
+++ b/SafeExamBrowser.Runtime/Operations/Events/ExamSelectionEventArgs.cs
@@ -16,6 +16,7 @@ namespace SafeExamBrowser.Runtime.Operations.Events
{
internal IEnumerable Exams { get; set; }
internal Exam SelectedExam { get; set; }
+ internal bool Success { get; set; }
internal ExamSelectionEventArgs(IEnumerable exams)
{
diff --git a/SafeExamBrowser.Runtime/Operations/ServerOperation.cs b/SafeExamBrowser.Runtime/Operations/ServerOperation.cs
index 85f9671c..21f20382 100644
--- a/SafeExamBrowser.Runtime/Operations/ServerOperation.cs
+++ b/SafeExamBrowser.Runtime/Operations/ServerOperation.cs
@@ -47,7 +47,9 @@ namespace SafeExamBrowser.Runtime.Operations
logger.Info("Initializing server...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeServer);
- var (abort, fallback, success) = TryPerformWithFallback(() => server.Connect(Context.Next.Settings.Server));
+ server.Initialize(Context.Next.Settings.Server);
+
+ var (abort, fallback, success) = TryPerformWithFallback(() => server.Connect(), out var token);
if (success)
{
@@ -55,24 +57,32 @@ namespace SafeExamBrowser.Runtime.Operations
if (success)
{
- var exam = SelectExam(exams);
-
- (abort, fallback, success) = TryPerformWithFallback(() => server.GetConfigurationFor(exam), out var uri);
+ success = TrySelectExam(exams, out var exam);
if (success)
{
- var status = TryLoadSettings(uri, UriSource.Server, out _, out var settings);
+ (abort, fallback, success) = TryPerformWithFallback(() => server.GetConfigurationFor(exam), out var uri);
- if (status == LoadStatus.Success)
+ if (success)
{
- Context.Next.Settings = settings;
- result = OperationResult.Success;
- }
- else
- {
- result = OperationResult.Failed;
+ var status = TryLoadSettings(uri, UriSource.Server, out _, out var settings);
+
+ if (status == LoadStatus.Success)
+ {
+ Context.Next.Settings = settings;
+ result = OperationResult.Success;
+ }
+ else
+ {
+ result = OperationResult.Failed;
+ }
}
}
+ else
+ {
+ logger.Info("The user aborted the exam selection.");
+ result = OperationResult.Aborted;
+ }
}
}
@@ -192,13 +202,14 @@ namespace SafeExamBrowser.Runtime.Operations
return args.Retry;
}
- private Exam SelectExam(IEnumerable exams)
+ private bool TrySelectExam(IEnumerable exams, out Exam exam)
{
var args = new ExamSelectionEventArgs(exams);
ActionRequired?.Invoke(args);
+ exam = args.SelectedExam;
- return args.SelectedExam;
+ return args.Success;
}
}
}
diff --git a/SafeExamBrowser.Runtime/RuntimeController.cs b/SafeExamBrowser.Runtime/RuntimeController.cs
index 6691e9da..88a2d823 100644
--- a/SafeExamBrowser.Runtime/RuntimeController.cs
+++ b/SafeExamBrowser.Runtime/RuntimeController.cs
@@ -399,12 +399,23 @@ namespace SafeExamBrowser.Runtime
}
}
- private void AskForExamSelection(ExamSelectionEventArgs a)
+ private void AskForExamSelection(ExamSelectionEventArgs args)
{
- // TODO: Also implement mechanism to retrieve selection via client!!
+ var isStartup = !SessionIsRunning;
+ var isRunningOnDefaultDesktop = SessionIsRunning && Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell;
+
+ if (isStartup || isRunningOnDefaultDesktop)
+ {
+ TryAskForExamSelectionViaDialog(args);
+ }
+ else
+ {
+ // TODO: Also implement mechanism to retrieve selection via client!!
+ // TryAskForExamSelectionViaClient(args);
+ }
}
- private void AskForServerFailureAction(ServerFailureEventArgs a)
+ private void AskForServerFailureAction(ServerFailureEventArgs args)
{
// TODO: Also implement mechanism to retrieve selection via client!!
}
@@ -490,6 +501,17 @@ namespace SafeExamBrowser.Runtime
return result;
}
+ private void TryAskForExamSelectionViaDialog(ExamSelectionEventArgs args)
+ {
+ var message = TextKey.ExamSelectionDialog_Message;
+ var title = TextKey.ExamSelectionDialog_Title;
+ var dialog = uiFactory.CreateExamSelectionDialog(text.Get(message), text.Get(title), args.Exams);
+ var result = dialog.Show(runtimeWindow);
+
+ args.SelectedExam = result.SelectedExam;
+ args.Success = result.Success;
+ }
+
private void TryGetPasswordViaDialog(PasswordRequiredEventArgs args)
{
var message = default(TextKey);
diff --git a/SafeExamBrowser.Server.Contracts/Exam.cs b/SafeExamBrowser.Server.Contracts/Exam.cs
index 0fe5e16c..96a4d09c 100644
--- a/SafeExamBrowser.Server.Contracts/Exam.cs
+++ b/SafeExamBrowser.Server.Contracts/Exam.cs
@@ -9,10 +9,23 @@
namespace SafeExamBrowser.Server.Contracts
{
///
- ///
+ /// Defines a server exam.
///
public class Exam
{
+ ///
+ /// The identifier of the exam.
+ ///
+ public string Id { get; set; }
+ ///
+ /// The name of the exam.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// The URL of the exam.
+ ///
+ public string Url { get; set; }
}
}
diff --git a/SafeExamBrowser.Server.Contracts/IServerProxy.cs b/SafeExamBrowser.Server.Contracts/IServerProxy.cs
index e9108449..157efcdb 100644
--- a/SafeExamBrowser.Server.Contracts/IServerProxy.cs
+++ b/SafeExamBrowser.Server.Contracts/IServerProxy.cs
@@ -13,14 +13,15 @@ using SafeExamBrowser.Settings.Server;
namespace SafeExamBrowser.Server.Contracts
{
///
- ///
+ /// Defines the communication options with a server.
///
public interface IServerProxy
{
///
- ///
+ /// TODO: Return API as well or re-load in proxy instance of client?
+ /// Attempts to initialize a connection to the server. If successful, returns a OAuth2 token as response value.
///
- ServerResponse Connect(ServerSettings settings);
+ ServerResponse Connect();
///
///
@@ -37,6 +38,11 @@ namespace SafeExamBrowser.Server.Contracts
///
ServerResponse GetConfigurationFor(Exam exam);
+ ///
+ /// Initializes the server settings to be used for communication.
+ ///
+ void Initialize(ServerSettings settings);
+
///
///
///
diff --git a/SafeExamBrowser.Server/Data/ApiVersion1.cs b/SafeExamBrowser.Server/Data/ApiVersion1.cs
new file mode 100644
index 00000000..23df1ca8
--- /dev/null
+++ b/SafeExamBrowser.Server/Data/ApiVersion1.cs
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2020 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.Server.Data
+{
+ internal class ApiVersion1
+ {
+ public string AccessTokenEndpoint { get; set; }
+
+ public string HandshakeEndpoint { get; set; }
+
+ public string ConfigurationEndpoint { get; set; }
+
+ public string PingEndpoint { get; set; }
+
+ public string LogEndpoint { get; set; }
+ }
+}
diff --git a/SafeExamBrowser.Server/SafeExamBrowser.Server.csproj b/SafeExamBrowser.Server/SafeExamBrowser.Server.csproj
index 75c02513..16f24cf8 100644
--- a/SafeExamBrowser.Server/SafeExamBrowser.Server.csproj
+++ b/SafeExamBrowser.Server/SafeExamBrowser.Server.csproj
@@ -50,14 +50,23 @@
MinimumRecommendedRules.ruleset
+
+ ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll
+
+
+
+
+ {64ea30fb-11d4-436a-9c2b-88566285363e}
+ SafeExamBrowser.Logging.Contracts
+
{db701e6f-bddc-4cec-b662-335a9dc11809}
SafeExamBrowser.Server.Contracts
@@ -67,5 +76,8 @@
SafeExamBrowser.Settings
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.Server/ServerProxy.cs b/SafeExamBrowser.Server/ServerProxy.cs
index 50f5387e..63340d50 100644
--- a/SafeExamBrowser.Server/ServerProxy.cs
+++ b/SafeExamBrowser.Server/ServerProxy.cs
@@ -8,36 +8,313 @@
using System;
using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Server.Contracts;
+using SafeExamBrowser.Server.Data;
using SafeExamBrowser.Settings.Server;
namespace SafeExamBrowser.Server
{
public class ServerProxy : IServerProxy
{
- public ServerResponse Connect(ServerSettings settings)
+ private ApiVersion1 api;
+ private string connectionToken;
+ private HttpClient httpClient;
+ private ILogger logger;
+ private string oauth2Token;
+ private ServerSettings settings;
+
+ public ServerProxy(ILogger logger)
{
- throw new NotImplementedException();
+ this.api = new ApiVersion1();
+ this.httpClient = new HttpClient();
+ this.logger = logger;
+ }
+
+ public ServerResponse Connect()
+ {
+ var success = TryExecute(HttpMethod.Get, settings.ApiUrl, out var response);
+ var message = ToString(response);
+
+ if (success && TryParseApi(response.Content))
+ {
+ logger.Info("Successfully loaded server API.");
+
+ var secret = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{settings.ClientName}:{settings.ClientSecret}"));
+ var authorization = ("Authorization", $"Basic {secret}");
+ var content = "grant_type=client_credentials&scope=read write";
+ var contentType = "application/x-www-form-urlencoded";
+
+ success = TryExecute(HttpMethod.Post, api.AccessTokenEndpoint, out response, content, contentType, authorization);
+ message = ToString(response);
+
+ if (success && TryParseOauth2Token(response.Content))
+ {
+ logger.Info("Successfully retrieved OAuth2 token.");
+ }
+ else
+ {
+ logger.Error("Failed to retrieve OAuth2 token!");
+ }
+ }
+ else
+ {
+ logger.Error("Failed to load server API!");
+ }
+
+ return new ServerResponse(success, oauth2Token, message);
}
public ServerResponse Disconnect()
{
- throw new NotImplementedException();
+ return new ServerResponse(false, "Some error message here");
}
public ServerResponse> GetAvailableExams()
{
- throw new NotImplementedException();
+ var authorization = ("Authorization", $"Bearer {oauth2Token}");
+ var content = $"institutionId={settings.Institution}";
+ var contentType = "application/x-www-form-urlencoded";
+
+ var success = TryExecute(HttpMethod.Post, api.HandshakeEndpoint, out var response, content, contentType, authorization);
+ var message = ToString(response);
+ var hasToken = TryParseConnectionToken(response);
+ var hasExams = TryParseExams(response.Content, out var exams);
+
+ if (success && hasExams && hasToken)
+ {
+ logger.Info("Successfully retrieved connection token and available exams.");
+ }
+ else if (!hasExams)
+ {
+ logger.Error("Failed to retrieve available exams!");
+ }
+ else if (!hasToken)
+ {
+ logger.Error("Failed to retrieve connection token!");
+ }
+ else
+ {
+ logger.Error("Failed to load connection token and available exams!");
+ }
+
+ return new ServerResponse>(hasExams && hasToken, exams, message);
}
public ServerResponse GetConfigurationFor(Exam exam)
{
- throw new NotImplementedException();
+ // 4. Send exam ID
+
+ return new ServerResponse(false, default(Uri), "Some error message here");
+ }
+
+ public void Initialize(ServerSettings settings)
+ {
+ this.settings = settings;
+ httpClient.BaseAddress = new Uri(settings.ServerUrl);
+
+ if (settings.RequestTimeout > 0)
+ {
+ httpClient.Timeout = TimeSpan.FromMilliseconds(settings.RequestTimeout);
+ }
}
public ServerResponse SendSessionInfo(string sessionId)
{
- throw new NotImplementedException();
+ return new ServerResponse(false, "Some error message here");
+ }
+
+ private bool TryParseApi(HttpContent content)
+ {
+ var success = false;
+
+ try
+ {
+ var json = JsonConvert.DeserializeObject(Extract(content)) as JObject;
+ var apis = json["api-versions"];
+
+ foreach (var api in apis.AsJEnumerable())
+ {
+ if (api["name"].Value().Equals("v1"))
+ {
+ foreach (var endpoint in api["endpoints"].AsJEnumerable())
+ {
+ var name = endpoint["name"].Value();
+ var location = endpoint["location"].Value();
+
+ switch (name)
+ {
+ case "access-token-endpoint":
+ this.api.AccessTokenEndpoint = location;
+ break;
+ case "seb-configuration-endpoint":
+ this.api.ConfigurationEndpoint = location;
+ break;
+ case "seb-handshake-endpoint":
+ this.api.HandshakeEndpoint = location;
+ break;
+ case "seb-log-endpoint":
+ this.api.LogEndpoint = location;
+ break;
+ case "seb-ping-endpoint":
+ this.api.PingEndpoint = location;
+ break;
+ }
+ }
+
+ success = true;
+ }
+
+ if (!success)
+ {
+ logger.Error("The selected SEB server instance does not support the required API version!");
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ logger.Error("Failed to parse server API!", e);
+ }
+
+ return success;
+ }
+
+ private bool TryParseConnectionToken(HttpResponseMessage response)
+ {
+ try
+ {
+ var hasHeader = response.Headers.TryGetValues("SEBConnectionToken", out var values);
+
+ if (hasHeader)
+ {
+ connectionToken = values.First();
+ }
+ else
+ {
+ logger.Error("Failed to retrieve connection token!");
+ }
+ }
+ catch (Exception e)
+ {
+ logger.Error("Failed to parse connection token!", e);
+ }
+
+ return connectionToken != default(string);
+ }
+
+ private bool TryParseExams(HttpContent content, out IList exams)
+ {
+ exams = new List();
+
+ try
+ {
+ var json = JsonConvert.DeserializeObject(Extract(content)) as JArray;
+
+ foreach (var exam in json.AsJEnumerable())
+ {
+ exams.Add(new Exam
+ {
+ Id = exam["examId"].Value(),
+ Name = exam["name"].Value(),
+ Url = exam["url"].Value()
+ });
+ }
+ }
+ catch (Exception e)
+ {
+ logger.Error("Failed to parse exams!", e);
+ }
+
+ return exams.Any();
+ }
+
+ private bool TryParseOauth2Token(HttpContent content)
+ {
+ try
+ {
+ var json = JsonConvert.DeserializeObject(Extract(content)) as JObject;
+
+ oauth2Token = json["access_token"].Value();
+ }
+ catch (Exception e)
+ {
+ logger.Error("Failed to parse Oauth2 token!", e);
+ }
+
+ return oauth2Token != default(string);
+ }
+
+ private bool TryExecute(
+ HttpMethod method,
+ string url,
+ out HttpResponseMessage response,
+ string content = default(string),
+ string contentType = default(string),
+ params (string name, string value)[] headers)
+ {
+ response = default(HttpResponseMessage);
+
+ for (var attempt = 0; attempt < settings.RequestAttempts && response == default(HttpResponseMessage); attempt++)
+ {
+ var request = new HttpRequestMessage(method, url);
+
+ if (content != default(string))
+ {
+ request.Content = new StringContent(content, Encoding.UTF8);
+
+ if (contentType != default(string))
+ {
+ request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
+ }
+ }
+
+ foreach (var (name, value) in headers)
+ {
+ request.Headers.Add(name, value);
+ }
+
+ try
+ {
+ response = httpClient.SendAsync(request).GetAwaiter().GetResult();
+ logger.Debug($"Request was successful: {request.Method} '{request.RequestUri}' -> {ToString(response)}");
+ }
+ catch (TaskCanceledException)
+ {
+ logger.Error($"Request {request.Method} '{request.RequestUri}' did not complete within {settings.RequestTimeout}ms!");
+ break;
+ }
+ catch (Exception e)
+ {
+ logger.Debug($"Request {request.Method} '{request.RequestUri}' failed due to {e}");
+ }
+ }
+
+ return response != default(HttpResponseMessage) && response.IsSuccessStatusCode;
+ }
+
+ private string Extract(HttpContent content)
+ {
+ var task = Task.Run(async () =>
+ {
+ return await content.ReadAsStreamAsync();
+ });
+ var stream = task.GetAwaiter().GetResult();
+ var reader = new StreamReader(stream);
+
+ return reader.ReadToEnd();
+ }
+
+ private string ToString(HttpResponseMessage response)
+ {
+ return $"{(int) response.StatusCode} {response.StatusCode} {response.ReasonPhrase}";
}
}
}
diff --git a/SafeExamBrowser.Server/packages.config b/SafeExamBrowser.Server/packages.config
new file mode 100644
index 00000000..a9de8b55
--- /dev/null
+++ b/SafeExamBrowser.Server/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs
index 375f9705..16a98687 100644
--- a/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs
+++ b/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs
@@ -12,6 +12,7 @@ using SafeExamBrowser.Client.Contracts;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
+using SafeExamBrowser.Server.Contracts;
using SafeExamBrowser.Settings.Browser;
using SafeExamBrowser.SystemComponents.Contracts.Audio;
using SafeExamBrowser.SystemComponents.Contracts.Keyboard;
@@ -54,6 +55,11 @@ namespace SafeExamBrowser.UserInterface.Contracts
///
IBrowserWindow CreateBrowserWindow(IBrowserControl control, BrowserSettings settings, bool isMainWindow);
+ ///
+ /// Creates an exam selection dialog for the given exams.
+ ///
+ IExamSelectionDialog CreateExamSelectionDialog(string message, string title, IEnumerable exams);
+
///
/// Creates a system control which allows to change the keyboard layout of the computer.
///
diff --git a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj
index 46e9f08d..84b0b350 100644
--- a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj
+++ b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj
@@ -86,9 +86,11 @@
+
+
@@ -117,6 +119,10 @@
{64ea30fb-11d4-436a-9c2b-88566285363e}
SafeExamBrowser.Logging.Contracts
+
+ {db701e6f-bddc-4cec-b662-335a9dc11809}
+ SafeExamBrowser.Server.Contracts
+
{30b2d907-5861-4f39-abad-c4abf1b3470e}
SafeExamBrowser.Settings
diff --git a/SafeExamBrowser.UserInterface.Contracts/Windows/Data/ExamSelectionDialogResult.cs b/SafeExamBrowser.UserInterface.Contracts/Windows/Data/ExamSelectionDialogResult.cs
new file mode 100644
index 00000000..db4190bf
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Contracts/Windows/Data/ExamSelectionDialogResult.cs
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2020 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.Server.Contracts;
+
+namespace SafeExamBrowser.UserInterface.Contracts.Windows.Data
+{
+ ///
+ /// Defines the user interaction result of an .
+ ///
+ public class ExamSelectionDialogResult
+ {
+ ///
+ /// The exam selected by the user.
+ ///
+ public Exam SelectedExam { get; set; }
+
+ ///
+ /// Indicates whether the user confirmed the dialog or not.
+ ///
+ public bool Success { get; set; }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Contracts/Windows/IExamSelectionDialog.cs b/SafeExamBrowser.UserInterface.Contracts/Windows/IExamSelectionDialog.cs
new file mode 100644
index 00000000..9d4d524d
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Contracts/Windows/IExamSelectionDialog.cs
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2020 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.UserInterface.Contracts.Windows.Data;
+
+namespace SafeExamBrowser.UserInterface.Contracts.Windows
+{
+ ///
+ /// Defines the functionality of an exam selection dialog.
+ ///
+ public interface IExamSelectionDialog
+ {
+ ///
+ /// Shows the dialog as topmost window. If a parent window is specified, the dialog is rendered modally for the given parent.
+ ///
+ ExamSelectionDialogResult Show(IWindow parent = null);
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj b/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj
index b283a8fa..4a249a14 100644
--- a/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj
+++ b/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj
@@ -148,6 +148,9 @@
WindowControl.xaml
+
+ ExamSelectionDialog.xaml
+
FileSystemDialog.xaml
@@ -332,6 +335,10 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
@@ -474,6 +481,10 @@
{64ea30fb-11d4-436a-9c2b-88566285363e}
SafeExamBrowser.Logging.Contracts
+
+ {DB701E6F-BDDC-4CEC-B662-335A9DC11809}
+ SafeExamBrowser.Server.Contracts
+
{30b2d907-5861-4f39-abad-c4abf1b3470e}
SafeExamBrowser.Settings
diff --git a/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs
index 3ec92cf0..6c834894 100644
--- a/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs
+++ b/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs
@@ -16,6 +16,7 @@ using SafeExamBrowser.Client.Contracts;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
+using SafeExamBrowser.Server.Contracts;
using SafeExamBrowser.Settings.Browser;
using SafeExamBrowser.SystemComponents.Contracts.Audio;
using SafeExamBrowser.SystemComponents.Contracts.Keyboard;
@@ -81,6 +82,11 @@ namespace SafeExamBrowser.UserInterface.Desktop
return Application.Current.Dispatcher.Invoke(() => new BrowserWindow(control, settings, isMainWindow, text));
}
+ public IExamSelectionDialog CreateExamSelectionDialog(string message, string title, IEnumerable exams)
+ {
+ return Application.Current.Dispatcher.Invoke(() => new ExamSelectionDialog(message, title, text, exams));
+ }
+
public ISystemControl CreateKeyboardLayoutControl(IKeyboard keyboard, Location location)
{
if (location == Location.ActionCenter)
diff --git a/SafeExamBrowser.UserInterface.Desktop/Windows/ExamSelectionDialog.xaml b/SafeExamBrowser.UserInterface.Desktop/Windows/ExamSelectionDialog.xaml
new file mode 100644
index 00000000..9d869466
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Desktop/Windows/ExamSelectionDialog.xaml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Desktop/Windows/ExamSelectionDialog.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Windows/ExamSelectionDialog.xaml.cs
new file mode 100644
index 00000000..0fb386f8
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Desktop/Windows/ExamSelectionDialog.xaml.cs
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2020 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 System.Windows;
+using System.Windows.Controls;
+using SafeExamBrowser.I18n.Contracts;
+using SafeExamBrowser.Server.Contracts;
+using SafeExamBrowser.UserInterface.Contracts.Windows;
+using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
+
+namespace SafeExamBrowser.UserInterface.Desktop.Windows
+{
+ public partial class ExamSelectionDialog : Window, IExamSelectionDialog
+ {
+ private readonly IText text;
+
+ public ExamSelectionDialog(string message, string title, IText text, IEnumerable exams)
+ {
+ this.text = text;
+
+ InitializeComponent();
+ InitializeExamSelectionDialog(message, title, exams);
+ }
+
+ public ExamSelectionDialogResult Show(IWindow parent = null)
+ {
+ return Dispatcher.Invoke(() =>
+ {
+ var result = new ExamSelectionDialogResult { Success = false };
+
+ if (parent is Window)
+ {
+ Owner = parent as Window;
+ WindowStartupLocation = WindowStartupLocation.CenterOwner;
+ }
+
+ if (ShowDialog() is true)
+ {
+ result.SelectedExam = ExamList.SelectedItem as Exam;
+ result.Success = true;
+ }
+
+ return result;
+ });
+ }
+
+ private void InitializeExamSelectionDialog(string message, string title, IEnumerable exams)
+ {
+ Message.Text = message;
+ Title = title;
+ WindowStartupLocation = WindowStartupLocation.CenterScreen;
+
+ CancelButton.Content = text.Get(TextKey.ExamSelectionDialog_Cancel);
+ CancelButton.Click += CancelButton_Click;
+
+ SelectButton.Content = text.Get(TextKey.ExamSelectionDialog_Select);
+ SelectButton.Click += ConfirmButton_Click;
+
+ ExamList.ItemsSource = exams;
+ ExamList.SelectionChanged += ExamList_SelectionChanged;
+
+ Loaded += (o, args) => Activate();
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+
+ private void ConfirmButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = true;
+ Close();
+ }
+
+ private void ExamList_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ SelectButton.IsEnabled = ExamList.SelectedItem != null;
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj b/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj
index 03e32c12..08067dd3 100644
--- a/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj
+++ b/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj
@@ -208,6 +208,10 @@
{64ea30fb-11d4-436a-9c2b-88566285363e}
SafeExamBrowser.Logging.Contracts
+
+ {DB701E6F-BDDC-4CEC-B662-335A9DC11809}
+ SafeExamBrowser.Server.Contracts
+
{30b2d907-5861-4f39-abad-c4abf1b3470e}
SafeExamBrowser.Settings
diff --git a/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs
index 60a42b23..ba5d2060 100644
--- a/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs
+++ b/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs
@@ -16,6 +16,7 @@ using SafeExamBrowser.Client.Contracts;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
+using SafeExamBrowser.Server.Contracts;
using SafeExamBrowser.Settings.Browser;
using SafeExamBrowser.SystemComponents.Contracts.Audio;
using SafeExamBrowser.SystemComponents.Contracts.Keyboard;
@@ -81,6 +82,12 @@ namespace SafeExamBrowser.UserInterface.Mobile
return Application.Current.Dispatcher.Invoke(() => new BrowserWindow(control, settings, isMainWindow, text));
}
+ public IExamSelectionDialog CreateExamSelectionDialog(string message, string title, IEnumerable exams)
+ {
+ // TODO
+ throw new System.NotImplementedException();
+ }
+
public ISystemControl CreateKeyboardLayoutControl(IKeyboard keyboard, Location location)
{
if (location == Location.ActionCenter)