diff --git a/SafeExamBrowser.Client/ClientController.cs b/SafeExamBrowser.Client/ClientController.cs index 08b61d45..6448eed0 100644 --- a/SafeExamBrowser.Client/ClientController.cs +++ b/SafeExamBrowser.Client/ClientController.cs @@ -28,6 +28,7 @@ using SafeExamBrowser.Monitoring.Contracts.Applications; using SafeExamBrowser.Monitoring.Contracts.Display; using SafeExamBrowser.Monitoring.Contracts.System; using SafeExamBrowser.Server.Contracts; +using SafeExamBrowser.Server.Contracts.Data; using SafeExamBrowser.Settings; using SafeExamBrowser.UserInterface.Contracts; using SafeExamBrowser.UserInterface.Contracts.FileSystemDialog; @@ -186,6 +187,7 @@ namespace SafeExamBrowser.Client Browser.ConfigurationDownloadRequested += Browser_ConfigurationDownloadRequested; Browser.SessionIdentifierDetected += Browser_SessionIdentifierDetected; Browser.TerminationRequested += Browser_TerminationRequested; + ClientHost.ExamSelectionRequested += ClientHost_ExamSelectionRequested; ClientHost.MessageBoxRequested += ClientHost_MessageBoxRequested; ClientHost.PasswordRequested += ClientHost_PasswordRequested; ClientHost.ReconfigurationAborted += ClientHost_ReconfigurationAborted; @@ -226,6 +228,7 @@ namespace SafeExamBrowser.Client if (ClientHost != null) { + ClientHost.ExamSelectionRequested -= ClientHost_ExamSelectionRequested; ClientHost.MessageBoxRequested -= ClientHost_MessageBoxRequested; ClientHost.PasswordRequested -= ClientHost_PasswordRequested; ClientHost.ReconfigurationAborted -= ClientHost_ReconfigurationAborted; @@ -393,6 +396,18 @@ namespace SafeExamBrowser.Client } } + private void ClientHost_ExamSelectionRequested(ExamSelectionRequestEventArgs args) + { + logger.Info($"Received exam selection request with id '{args.RequestId}'."); + + var exams = args.Exams.Select(e => new Exam { Id = e.id, LmsName = e.lms, Name = e.name, Url = e.url }); + var dialog = uiFactory.CreateExamSelectionDialog(exams); + var result = dialog.Show(splashScreen); + + runtime.SubmitExamSelectionResult(args.RequestId, result.Success, result.SelectedExam?.Id); + logger.Info($"Exam selection request with id '{args.RequestId}' is complete."); + } + private void ClientHost_MessageBoxRequested(MessageBoxRequestEventArgs args) { logger.Info($"Received message box request with id '{args.RequestId}'."); diff --git a/SafeExamBrowser.Client/Communication/ClientHost.cs b/SafeExamBrowser.Client/Communication/ClientHost.cs index 7df4aa6a..c9051b26 100644 --- a/SafeExamBrowser.Client/Communication/ClientHost.cs +++ b/SafeExamBrowser.Client/Communication/ClientHost.cs @@ -24,6 +24,7 @@ namespace SafeExamBrowser.Client.Communication public Guid AuthenticationToken { private get; set; } public bool IsConnected { get; private set; } + public event CommunicationEventHandler ExamSelectionRequested; public event CommunicationEventHandler MessageBoxRequested; public event CommunicationEventHandler PasswordRequested; public event CommunicationEventHandler ReconfigurationAborted; @@ -69,6 +70,9 @@ namespace SafeExamBrowser.Client.Communication { switch (message) { + case ExamSelectionRequestMessage m: + ExamSelectionRequested?.InvokeAsync(new ExamSelectionRequestEventArgs { Exams = m.Exams, RequestId = m.RequestId }); + return new SimpleResponse(SimpleResponsePurport.Acknowledged); case MessageBoxRequestMessage m: MessageBoxRequested?.InvokeAsync(new MessageBoxRequestEventArgs { Action = m.Action, Icon = m.Icon, Message = m.Message, RequestId = m.RequestId, Title = m.Title }); return new SimpleResponse(SimpleResponsePurport.Acknowledged); diff --git a/SafeExamBrowser.Communication.Contracts/Data/ExamSelectionReplyMessage.cs b/SafeExamBrowser.Communication.Contracts/Data/ExamSelectionReplyMessage.cs new file mode 100644 index 00000000..4c70aa06 --- /dev/null +++ b/SafeExamBrowser.Communication.Contracts/Data/ExamSelectionReplyMessage.cs @@ -0,0 +1,41 @@ +/* + * 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; + +namespace SafeExamBrowser.Communication.Contracts.Data +{ + /// + /// The reply to a . + /// + [Serializable] + public class ExamSelectionReplyMessage : Message + { + /// + /// The unique identifier for the exam selection request. + /// + public Guid RequestId { get; } + + /// + /// The identifier of the exam selected by the user. + /// + public string SelectedExamId { get; } + + /// + /// Determines whether the user interaction was successful or not. + /// + public bool Success { get; } + + public ExamSelectionReplyMessage(Guid requestId, bool success, string selectedExamId) + { + RequestId = requestId; + Success = success; + SelectedExamId = selectedExamId; + } + } +} diff --git a/SafeExamBrowser.Communication.Contracts/Data/ExamSelectionRequestMessage.cs b/SafeExamBrowser.Communication.Contracts/Data/ExamSelectionRequestMessage.cs new file mode 100644 index 00000000..d1038cd6 --- /dev/null +++ b/SafeExamBrowser.Communication.Contracts/Data/ExamSelectionRequestMessage.cs @@ -0,0 +1,36 @@ +/* + * 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; +using System.Collections.Generic; + +namespace SafeExamBrowser.Communication.Contracts.Data +{ + /// + /// This message is transmitted to the client to request a server exam selection by the user. + /// + [Serializable] + public class ExamSelectionRequestMessage : Message + { + /// + /// The exams from which the user needs to make a selection. + /// + public IEnumerable<(string id, string lms, string name, string url)> Exams { get; } + + /// + /// The unique identifier for the server exam selection request. + /// + public Guid RequestId { get; } + + public ExamSelectionRequestMessage(IEnumerable<(string id, string lms, string name, string url)> exams, Guid requestId) + { + Exams = exams; + RequestId = requestId; + } + } +} diff --git a/SafeExamBrowser.Communication.Contracts/Events/ExamSelectionReplyEventArgs.cs b/SafeExamBrowser.Communication.Contracts/Events/ExamSelectionReplyEventArgs.cs new file mode 100644 index 00000000..e9b65ce5 --- /dev/null +++ b/SafeExamBrowser.Communication.Contracts/Events/ExamSelectionReplyEventArgs.cs @@ -0,0 +1,33 @@ +/* + * 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; + +namespace SafeExamBrowser.Communication.Contracts.Events +{ + /// + /// The event arguments used for the exam selection event fired by the . + /// + public class ExamSelectionReplyEventArgs : CommunicationEventArgs + { + /// + /// Identifies the exam selection request. + /// + public Guid RequestId { get; set; } + + /// + /// The identifier of the exam selected by the user. + /// + public string SelectedExamId { get; set; } + + /// + /// Indicates whether an exam has been successfully selected by the user. + /// + public bool Success { get; set; } + } +} diff --git a/SafeExamBrowser.Communication.Contracts/Events/ExamSelectionRequestEventArgs.cs b/SafeExamBrowser.Communication.Contracts/Events/ExamSelectionRequestEventArgs.cs new file mode 100644 index 00000000..2bb1e5c8 --- /dev/null +++ b/SafeExamBrowser.Communication.Contracts/Events/ExamSelectionRequestEventArgs.cs @@ -0,0 +1,29 @@ +/* + * 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; +using System.Collections.Generic; + +namespace SafeExamBrowser.Communication.Contracts.Events +{ + /// + /// The event arguments used for the server exam selection request event fired by the . + /// + public class ExamSelectionRequestEventArgs : CommunicationEventArgs + { + /// + /// The exams from which the user needs to make a selection. + /// + public IEnumerable<(string id, string lms, string name, string url)> Exams { get; set; } + + /// + /// Identifies the server exam selection request. + /// + public Guid RequestId { get; set; } + } +} diff --git a/SafeExamBrowser.Communication.Contracts/Hosts/IClientHost.cs b/SafeExamBrowser.Communication.Contracts/Hosts/IClientHost.cs index dda8e2ef..f6d58f90 100644 --- a/SafeExamBrowser.Communication.Contracts/Hosts/IClientHost.cs +++ b/SafeExamBrowser.Communication.Contracts/Hosts/IClientHost.cs @@ -26,6 +26,11 @@ namespace SafeExamBrowser.Communication.Contracts.Hosts /// bool IsConnected { get; } + /// + /// Event fired when the runtime requests a server exam selection from the user. + /// + event CommunicationEventHandler ExamSelectionRequested; + /// /// Event fired when the runtime requests a message box input from the user. /// diff --git a/SafeExamBrowser.Communication.Contracts/Hosts/IRuntimeHost.cs b/SafeExamBrowser.Communication.Contracts/Hosts/IRuntimeHost.cs index 3500f00e..0325b85d 100644 --- a/SafeExamBrowser.Communication.Contracts/Hosts/IRuntimeHost.cs +++ b/SafeExamBrowser.Communication.Contracts/Hosts/IRuntimeHost.cs @@ -41,6 +41,11 @@ namespace SafeExamBrowser.Communication.Contracts.Hosts /// event CommunicationEventHandler ClientConfigurationNeeded; + /// + /// Event fired when the client submitted a server exam selection made by the user. + /// + event CommunicationEventHandler ExamSelectionReceived; + /// /// Event fired when the client submitted a message box result chosen by the user. /// diff --git a/SafeExamBrowser.Communication.Contracts/ICommunication.cs b/SafeExamBrowser.Communication.Contracts/ICommunication.cs index afaa4c39..6babcfce 100644 --- a/SafeExamBrowser.Communication.Contracts/ICommunication.cs +++ b/SafeExamBrowser.Communication.Contracts/ICommunication.cs @@ -21,6 +21,8 @@ namespace SafeExamBrowser.Communication.Contracts [ServiceKnownType(typeof(AuthenticationResponse))] [ServiceKnownType(typeof(ClientConfiguration))] [ServiceKnownType(typeof(ConfigurationResponse))] + [ServiceKnownType(typeof(ExamSelectionReplyMessage))] + [ServiceKnownType(typeof(ExamSelectionRequestMessage))] [ServiceKnownType(typeof(MessageBoxReplyMessage))] [ServiceKnownType(typeof(MessageBoxRequestMessage))] [ServiceKnownType(typeof(PasswordReplyMessage))] diff --git a/SafeExamBrowser.Communication.Contracts/Proxies/IClientProxy.cs b/SafeExamBrowser.Communication.Contracts/Proxies/IClientProxy.cs index 57193cd4..5f1e26bf 100644 --- a/SafeExamBrowser.Communication.Contracts/Proxies/IClientProxy.cs +++ b/SafeExamBrowser.Communication.Contracts/Proxies/IClientProxy.cs @@ -7,6 +7,7 @@ */ using System; +using System.Collections.Generic; using SafeExamBrowser.Communication.Contracts.Data; namespace SafeExamBrowser.Communication.Contracts.Proxies @@ -36,6 +37,11 @@ namespace SafeExamBrowser.Communication.Contracts.Proxies /// CommunicationResult RequestAuthentication(); + /// + /// Requests the client to render a server exam selection dialog and subsequently return the interaction result as separate message. + /// + CommunicationResult RequestExamSelection(IEnumerable<(string id, string lms, string name, string url)> exams, Guid requestId); + /// /// Requests the client to render a password dialog and subsequently return the interaction result as separate message. /// diff --git a/SafeExamBrowser.Communication.Contracts/Proxies/IRuntimeProxy.cs b/SafeExamBrowser.Communication.Contracts/Proxies/IRuntimeProxy.cs index 1fc18364..17335730 100644 --- a/SafeExamBrowser.Communication.Contracts/Proxies/IRuntimeProxy.cs +++ b/SafeExamBrowser.Communication.Contracts/Proxies/IRuntimeProxy.cs @@ -36,6 +36,12 @@ namespace SafeExamBrowser.Communication.Contracts.Proxies /// CommunicationResult RequestReconfiguration(string filePath); + /// + /// Submits the result of a server exam selection previously requested by the runtime. If the procedure was aborted by the user, + /// the selected exam identifier will be ! + /// + CommunicationResult SubmitExamSelectionResult(Guid requestId, bool success, string selectedExamId = default(string)); + /// /// Submits the result of a message box input previously requested by the runtime. /// @@ -43,8 +49,8 @@ namespace SafeExamBrowser.Communication.Contracts.Proxies /// /// Submits the result of a password input previously requested by the runtime. If the procedure was aborted by the user, - /// the password parameter will be null! + /// the password parameter will be ! /// - CommunicationResult SubmitPassword(Guid requestId, bool success, string password = null); + CommunicationResult SubmitPassword(Guid requestId, bool success, string password = default(string)); } } diff --git a/SafeExamBrowser.Communication.Contracts/SafeExamBrowser.Communication.Contracts.csproj b/SafeExamBrowser.Communication.Contracts/SafeExamBrowser.Communication.Contracts.csproj index 55996f59..e50b4626 100644 --- a/SafeExamBrowser.Communication.Contracts/SafeExamBrowser.Communication.Contracts.csproj +++ b/SafeExamBrowser.Communication.Contracts/SafeExamBrowser.Communication.Contracts.csproj @@ -60,6 +60,8 @@ + + @@ -78,6 +80,8 @@ + + diff --git a/SafeExamBrowser.Communication/Proxies/ClientProxy.cs b/SafeExamBrowser.Communication/Proxies/ClientProxy.cs index 9406cc08..e82d67af 100644 --- a/SafeExamBrowser.Communication/Proxies/ClientProxy.cs +++ b/SafeExamBrowser.Communication/Proxies/ClientProxy.cs @@ -7,6 +7,7 @@ */ using System; +using System.Collections.Generic; using SafeExamBrowser.Communication.Contracts; using SafeExamBrowser.Communication.Contracts.Data; using SafeExamBrowser.Communication.Contracts.Proxies; @@ -127,6 +128,32 @@ namespace SafeExamBrowser.Communication.Proxies } } + public CommunicationResult RequestExamSelection(IEnumerable<(string id, string lms, string name, string url)> exams, Guid requestId) + { + try + { + var response = Send(new ExamSelectionRequestMessage(exams, requestId)); + var success = IsAcknowledged(response); + + if (success) + { + Logger.Debug("Client acknowledged server exam selection request."); + } + else + { + Logger.Error($"Client did not acknowledge server exam selection request! Received: {ToString(response)}."); + } + + return new CommunicationResult(success); + } + catch (Exception e) + { + Logger.Error($"Failed to perform '{nameof(RequestExamSelection)}'", e); + + return new CommunicationResult(false); + } + } + public CommunicationResult RequestPassword(PasswordRequestPurpose purpose, Guid requestId) { try diff --git a/SafeExamBrowser.Communication/Proxies/RuntimeProxy.cs b/SafeExamBrowser.Communication/Proxies/RuntimeProxy.cs index ac346864..5bf94ac6 100644 --- a/SafeExamBrowser.Communication/Proxies/RuntimeProxy.cs +++ b/SafeExamBrowser.Communication/Proxies/RuntimeProxy.cs @@ -127,6 +127,32 @@ namespace SafeExamBrowser.Communication.Proxies } } + public CommunicationResult SubmitExamSelectionResult(Guid requestId, bool success, string selectedExamId = null) + { + try + { + var response = Send(new ExamSelectionReplyMessage(requestId, success, selectedExamId)); + var acknowledged = IsAcknowledged(response); + + if (acknowledged) + { + Logger.Debug("Runtime acknowledged server exam selection transmission."); + } + else + { + Logger.Error($"Runtime did not acknowledge server exam selection transmission! Response: {ToString(response)}."); + } + + return new CommunicationResult(acknowledged); + } + catch (Exception e) + { + Logger.Error($"Failed to perform '{nameof(SubmitExamSelectionResult)}'", e); + + return new CommunicationResult(false); + } + } + public CommunicationResult SubmitMessageBoxResult(Guid requestId, int result) { try diff --git a/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs b/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs index 00c11d2e..af27eb22 100644 --- a/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs +++ b/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs @@ -24,6 +24,7 @@ namespace SafeExamBrowser.Runtime.Communication public event CommunicationEventHandler ClientDisconnected; public event CommunicationEventHandler ClientReady; public event CommunicationEventHandler ClientConfigurationNeeded; + public event CommunicationEventHandler ExamSelectionReceived; public event CommunicationEventHandler MessageBoxReplyReceived; public event CommunicationEventHandler PasswordReceived; public event CommunicationEventHandler ReconfigurationRequested; @@ -58,6 +59,9 @@ namespace SafeExamBrowser.Runtime.Communication { switch (message) { + case ExamSelectionReplyMessage m: + ExamSelectionReceived?.InvokeAsync(new ExamSelectionReplyEventArgs { RequestId = m.RequestId, SelectedExamId = m.SelectedExamId, Success = m.Success }); + return new SimpleResponse(SimpleResponsePurport.Acknowledged); case MessageBoxReplyMessage m: MessageBoxReplyReceived?.InvokeAsync(new MessageBoxReplyEventArgs { RequestId = m.RequestId, Result = m.Result }); return new SimpleResponse(SimpleResponsePurport.Acknowledged); diff --git a/SafeExamBrowser.Runtime/RuntimeController.cs b/SafeExamBrowser.Runtime/RuntimeController.cs index 0b1a5592..d4abdcc8 100644 --- a/SafeExamBrowser.Runtime/RuntimeController.cs +++ b/SafeExamBrowser.Runtime/RuntimeController.cs @@ -7,6 +7,7 @@ */ using System; +using System.Linq; using System.Threading; using SafeExamBrowser.Communication.Contracts.Data; using SafeExamBrowser.Communication.Contracts.Events; @@ -18,6 +19,7 @@ using SafeExamBrowser.Core.Contracts.OperationModel.Events; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Runtime.Operations.Events; +using SafeExamBrowser.Server.Contracts.Data; using SafeExamBrowser.Settings.Security; using SafeExamBrowser.Settings.Service; using SafeExamBrowser.UserInterface.Contracts; @@ -410,8 +412,7 @@ namespace SafeExamBrowser.Runtime } else { - // TODO: Also implement mechanism to retrieve selection via client!! - // TryAskForExamSelectionViaClient(args); + TryAskForExamSelectionViaClient(args); } } @@ -426,8 +427,7 @@ namespace SafeExamBrowser.Runtime } else { - // TODO: Also implement mechanism to retrieve selection via client!! - // TryAskForServerFailureActionViaClient(args); + TryAskForServerFailureActionViaClient(args); } } @@ -521,6 +521,40 @@ namespace SafeExamBrowser.Runtime args.Success = result.Success; } + private void TryAskForExamSelectionViaClient(ExamSelectionEventArgs args) + { + var exams = args.Exams.Select(e => (e.Id, e.LmsName, e.Name, e.Url)); + var requestId = Guid.NewGuid(); + var response = default(ExamSelectionReplyEventArgs); + var responseEvent = new AutoResetEvent(false); + var responseEventHandler = new CommunicationEventHandler((a) => + { + if (a.RequestId == requestId) + { + response = a; + responseEvent.Set(); + } + }); + + runtimeHost.ExamSelectionReceived += responseEventHandler; + + var communication = sessionContext.ClientProxy.RequestExamSelection(exams, requestId); + + if (communication.Success) + { + responseEvent.WaitOne(); + args.SelectedExam = args.Exams.First(e => e.Id == response.SelectedExamId); + args.Success = response.Success; + } + else + { + args.SelectedExam = default(Exam); + args.Success = false; + } + + runtimeHost.ExamSelectionReceived -= responseEventHandler; + } + private void TryAskForServerFailureActionViaDialog(ServerFailureEventArgs args) { var dialog = uiFactory.CreateServerFailureDialog(args.Message, args.ShowFallback); @@ -531,6 +565,11 @@ namespace SafeExamBrowser.Runtime args.Retry = result.Retry; } + private void TryAskForServerFailureActionViaClient(ServerFailureEventArgs args) + { + // TODO: Implement communication mechanism! + } + private void TryGetPasswordViaDialog(PasswordRequiredEventArgs args) { var message = default(TextKey); diff --git a/SafeExamBrowser.Server/ServerProxy.cs b/SafeExamBrowser.Server/ServerProxy.cs index 687e31a7..ff318746 100644 --- a/SafeExamBrowser.Server/ServerProxy.cs +++ b/SafeExamBrowser.Server/ServerProxy.cs @@ -285,23 +285,19 @@ namespace SafeExamBrowser.Server try { - for (var count = 0; count < 5; count--) + if (logContent.TryDequeue(out var c) && c is ILogMessage message) { - if (logContent.TryDequeue(out var c) && c is ILogMessage message) + var json = new JObject { - var json = new JObject - { - ["type"] = ToLogType(message.Severity), - ["timestamp"] = message.DateTime.Ticks, - ["text"] = message.Message - }; + ["type"] = ToLogType(message.Severity), + ["timestamp"] = message.DateTime.Ticks, + ["text"] = message.Message + }; - var content = json.ToString(); - var contentType = "application/json;charset=UTF-8"; - // TODO: Logging these requests spams the application log! - // TODO: Why can't we send multiple log messages in one request? - var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token); - } + var content = json.ToString(); + var contentType = "application/json;charset=UTF-8"; + // TODO: Why can't we send multiple log messages in one request? + var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token); } } catch (Exception e)