/* * Copyright (c) 2025 ETH Zürich, IT Services * * 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; using System.Linq; using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Core.Contracts.OperationModel; using SafeExamBrowser.Core.Contracts.OperationModel.Events; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Server.Contracts; using SafeExamBrowser.Server.Contracts.Data; using SafeExamBrowser.Settings; using SafeExamBrowser.SystemComponents.Contracts; using SafeExamBrowser.UserInterface.Contracts; namespace SafeExamBrowser.Runtime.Operations.Session { internal class ServerOperation : ConfigurationBaseOperation { private readonly IFileSystem fileSystem; private readonly IServerProxy server; public override event StatusChangedEventHandler StatusChanged; public ServerOperation( Dependencies dependencies, IFileSystem fileSystem, IConfigurationRepository repository, IServerProxy server, IUserInterfaceFactory uiFactory) : base(dependencies, repository, uiFactory) { this.fileSystem = fileSystem; this.server = server; } public override OperationResult Perform() { var result = OperationResult.Success; if (Context.Next.Settings.SessionMode == SessionMode.Server) { var browserExamKey = default(string); var exam = default(Exam); var exams = default(IEnumerable); var uri = default(Uri); Logger.Info("Initializing server..."); StatusChanged?.Invoke(TextKey.OperationStatus_InitializeServer); server.Initialize(Context.Next.Settings.Server); var (abort, fallback, success) = TryPerformWithFallback(() => server.Connect()); if (success) { (abort, fallback, success) = TryPerformWithFallback(() => server.GetAvailableExams(Context.Next.Settings.Server.ExamId), out exams); } if (success) { success = TrySelectExam(exams, out exam); } if (success) { (abort, fallback, success) = TryPerformWithFallback(() => server.GetConfigurationFor(exam), out uri); } if (success) { result = TryLoadServerSettings(exam, uri); } if (success && result == OperationResult.Success) { (abort, fallback, success) = TryPerformWithFallback(() => server.SendSelectedExam(exam), out browserExamKey); } if (browserExamKey != default) { Context.Next.Settings.Browser.CustomBrowserExamKey = browserExamKey; } if (abort) { result = OperationResult.Aborted; Logger.Info("The user aborted the server operation."); } if (fallback) { Context.Next.Settings.SessionMode = SessionMode.Normal; result = OperationResult.Success; Logger.Info("The user chose to fallback and start a normal session."); } if (result == OperationResult.Success) { Logger.Info("Successfully initialized server."); } else if (result == OperationResult.Failed) { Logger.Error("Failed to initialize server!"); } } return result; } public override OperationResult Repeat() { var result = OperationResult.Success; if (Context.Current.Settings.SessionMode == SessionMode.Server && Context.Next.Settings.SessionMode == SessionMode.Server) { result = Revert(); if (result == OperationResult.Success) { result = Perform(); } else { Logger.Error($"Cannot start new server session due to failed finalization of current server session! Terminating..."); } } else if (Context.Current.Settings.SessionMode == SessionMode.Server) { result = Revert(); } else if (Context.Next.Settings.SessionMode == SessionMode.Server) { result = Perform(); } return result; } public override OperationResult Revert() { var result = OperationResult.Success; if (Context.Current?.Settings.SessionMode == SessionMode.Server || Context.Next?.Settings.SessionMode == SessionMode.Server) { Logger.Info("Finalizing server..."); StatusChanged?.Invoke(TextKey.OperationStatus_FinalizeServer); var disconnect = server.Disconnect(); if (disconnect.Success) { result = OperationResult.Success; Logger.Info("Successfully finalized server."); } else { result = OperationResult.Failed; Logger.Error("Failed to finalize server!"); } } return result; } private OperationResult TryLoadServerSettings(Exam exam, Uri uri) { var info = server.GetConnectionInfo(); var result = OperationResult.Failed; var status = TryLoadSettings(uri, UriSource.Server, out _, out var settings); fileSystem.Delete(uri.LocalPath); if (status == LoadStatus.Success) { var browserSettings = Context.Next.Settings.Browser; var invigilationSettings = settings.Server.Invigilation; var serverSettings = Context.Next.Settings.Server; Context.Next.AppConfig.ServerApi = info.Api; Context.Next.AppConfig.ServerConnectionToken = info.ConnectionToken; Context.Next.AppConfig.ServerExamId = exam.Id; Context.Next.AppConfig.ServerOauth2Token = info.Oauth2Token; Context.Next.Settings = settings; Context.Next.Settings.Browser.StartUrl = exam.Url; Context.Next.Settings.Browser.StartUrlQuery = browserSettings.StartUrlQuery; Context.Next.Settings.Server = serverSettings; Context.Next.Settings.Server.Invigilation = invigilationSettings; Context.Next.Settings.SessionMode = SessionMode.Server; result = OperationResult.Success; } return result; } private (bool abort, bool fallback, bool success) TryPerformWithFallback(Func request) { var abort = false; var fallback = false; var success = false; while (!success) { var response = request(); success = response.Success; if (!success && !Retry(response.Message, out abort, out fallback)) { break; } } return (abort, fallback, success); } private (bool abort, bool fallback, bool success) TryPerformWithFallback(Func> request, out T value) { var abort = false; var fallback = false; var success = false; value = default; while (!success) { var response = request(); success = response.Success; value = response.Value; if (!success && !Retry(response.Message, out abort, out fallback)) { break; } } return (abort, fallback, success); } private bool Retry(string message, out bool abort, out bool fallback) { AskForServerFailureAction(message, out abort, out fallback, out var retry); if (retry) { Logger.Debug("The user chose to retry the current server request."); } return retry; } private void AskForServerFailureAction(string message, out bool abort, out bool fallback, out bool retry) { var showFallback = Context.Next.Settings.Server.PerformFallback; if (ClientBridge.IsRequired()) { ClientBridge.TryAskForServerFailureAction(message, showFallback, out abort, out fallback, out retry); } else { var dialog = uiFactory.CreateServerFailureDialog(message, showFallback); var result = dialog.Show(RuntimeWindow); abort = result.Abort; fallback = result.Fallback; retry = result.Retry; } } private bool TrySelectExam(IEnumerable exams, out Exam exam) { var success = true; if (string.IsNullOrWhiteSpace(Context.Next.Settings.Server.ExamId)) { success = TryAskForExamSelection(exams, out exam); } else { exam = exams.First(); Logger.Info("Automatically selected exam as defined in configuration."); } return success; } private bool TryAskForExamSelection(IEnumerable exams, out Exam exam) { var success = false; if (ClientBridge.IsRequired()) { success = ClientBridge.TryAskForExamSelection(exams, out exam); } else { var dialog = uiFactory.CreateExamSelectionDialog(exams); var result = dialog.Show(RuntimeWindow); exam = result.SelectedExam; success = result.Success; } return success; } } }