324 lines
10 KiB
C#
324 lines
10 KiB
C#
/*
|
|
* Copyright (c) 2022 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 System.Reflection;
|
|
using System.Threading;
|
|
using System.Windows;
|
|
using Microsoft.Web.WebView2.Wpf;
|
|
using SafeExamBrowser.Configuration.Contracts;
|
|
using SafeExamBrowser.Core.Contracts.Notifications;
|
|
using SafeExamBrowser.Core.Contracts.Notifications.Events;
|
|
using SafeExamBrowser.Core.Contracts.Resources.Icons;
|
|
using SafeExamBrowser.I18n.Contracts;
|
|
using SafeExamBrowser.Logging.Contracts;
|
|
using SafeExamBrowser.Proctoring.Contracts;
|
|
using SafeExamBrowser.Proctoring.Contracts.Events;
|
|
using SafeExamBrowser.Server.Contracts;
|
|
using SafeExamBrowser.Server.Contracts.Events;
|
|
using SafeExamBrowser.Settings.Proctoring;
|
|
using SafeExamBrowser.SystemComponents.Contracts;
|
|
using SafeExamBrowser.UserInterface.Contracts;
|
|
using SafeExamBrowser.UserInterface.Contracts.Proctoring;
|
|
|
|
namespace SafeExamBrowser.Proctoring
|
|
{
|
|
public class ProctoringController : IProctoringController, INotification
|
|
{
|
|
private readonly AppConfig appConfig;
|
|
private readonly IFileSystem fileSystem;
|
|
private readonly IModuleLogger logger;
|
|
private readonly IServerProxy server;
|
|
private readonly IText text;
|
|
private readonly IUserInterfaceFactory uiFactory;
|
|
|
|
private string filePath;
|
|
private ProctoringControl control;
|
|
private ProctoringSettings settings;
|
|
private IProctoringWindow window;
|
|
private WindowVisibility windowVisibility;
|
|
|
|
public IconResource IconResource { get; set; }
|
|
public bool IsHandRaised { get; private set; }
|
|
public string Tooltip { get; set; }
|
|
|
|
public event ProctoringEventHandler HandLowered;
|
|
public event ProctoringEventHandler HandRaised;
|
|
public event NotificationChangedEventHandler NotificationChanged;
|
|
|
|
public ProctoringController(
|
|
AppConfig appConfig,
|
|
IFileSystem fileSystem,
|
|
IModuleLogger logger,
|
|
IServerProxy server,
|
|
IText text,
|
|
IUserInterfaceFactory uiFactory)
|
|
{
|
|
this.appConfig = appConfig;
|
|
this.fileSystem = fileSystem;
|
|
this.logger = logger;
|
|
this.server = server;
|
|
this.text = text;
|
|
this.uiFactory = uiFactory;
|
|
|
|
IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ProctoringNotification_Inactive.xaml") };
|
|
Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip);
|
|
}
|
|
|
|
public void Activate()
|
|
{
|
|
if (settings.WindowVisibility == WindowVisibility.Visible)
|
|
{
|
|
window?.BringToForeground();
|
|
}
|
|
else if (settings.WindowVisibility == WindowVisibility.AllowToHide || settings.WindowVisibility == WindowVisibility.AllowToShow)
|
|
{
|
|
window?.Toggle();
|
|
}
|
|
}
|
|
|
|
public void Initialize(ProctoringSettings settings)
|
|
{
|
|
var start = false;
|
|
|
|
this.settings = settings;
|
|
this.windowVisibility = settings.WindowVisibility;
|
|
|
|
server.HandConfirmed += Server_HandConfirmed;
|
|
server.ProctoringConfigurationReceived += Server_ProctoringConfigurationReceived;
|
|
server.ProctoringInstructionReceived += Server_ProctoringInstructionReceived;
|
|
|
|
if (settings.JitsiMeet.Enabled)
|
|
{
|
|
this.settings.JitsiMeet.ServerUrl = Sanitize(settings.JitsiMeet.ServerUrl);
|
|
|
|
start = !string.IsNullOrWhiteSpace(settings.JitsiMeet.RoomName);
|
|
start &= !string.IsNullOrWhiteSpace(settings.JitsiMeet.ServerUrl);
|
|
}
|
|
else if (settings.Zoom.Enabled)
|
|
{
|
|
start = !string.IsNullOrWhiteSpace(settings.Zoom.ApiKey);
|
|
start &= !string.IsNullOrWhiteSpace(settings.Zoom.ApiSecret) || !string.IsNullOrWhiteSpace(settings.Zoom.Signature);
|
|
start &= !string.IsNullOrWhiteSpace(settings.Zoom.MeetingNumber);
|
|
start &= !string.IsNullOrWhiteSpace(settings.Zoom.UserName);
|
|
}
|
|
|
|
if (start)
|
|
{
|
|
StartProctoring();
|
|
}
|
|
}
|
|
|
|
public void LowerHand()
|
|
{
|
|
var response = server.LowerHand();
|
|
|
|
if (response.Success)
|
|
{
|
|
IsHandRaised = false;
|
|
HandLowered?.Invoke();
|
|
logger.Info("Hand lowered.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error($"Failed to send lower hand notification to server! Message: {response.Message}.");
|
|
}
|
|
}
|
|
|
|
public void RaiseHand(string message = null)
|
|
{
|
|
var response = server.RaiseHand(message);
|
|
|
|
if (response.Success)
|
|
{
|
|
IsHandRaised = true;
|
|
HandRaised?.Invoke();
|
|
logger.Info("Hand raised.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error($"Failed to send raise hand notification to server! Message: {response.Message}.");
|
|
}
|
|
}
|
|
|
|
public void Terminate()
|
|
{
|
|
StopProctoring();
|
|
}
|
|
|
|
private void Server_HandConfirmed()
|
|
{
|
|
logger.Info("Hand confirmation received.");
|
|
|
|
IsHandRaised = false;
|
|
HandLowered?.Invoke();
|
|
}
|
|
|
|
private void Server_ProctoringInstructionReceived(ProctoringInstructionEventArgs args)
|
|
{
|
|
logger.Info("Proctoring instruction received.");
|
|
|
|
settings.JitsiMeet.RoomName = args.JitsiMeetRoomName;
|
|
settings.JitsiMeet.ServerUrl = args.JitsiMeetServerUrl;
|
|
settings.JitsiMeet.Token = args.JitsiMeetToken;
|
|
|
|
settings.Zoom.ApiKey = args.ZoomApiKey;
|
|
settings.Zoom.MeetingNumber = args.ZoomMeetingNumber;
|
|
settings.Zoom.Password = args.ZoomPassword;
|
|
settings.Zoom.Signature = args.ZoomSignature;
|
|
settings.Zoom.Subject = args.ZoomSubject;
|
|
settings.Zoom.UserName = args.ZoomUserName;
|
|
|
|
StopProctoring();
|
|
StartProctoring();
|
|
}
|
|
|
|
private void Server_ProctoringConfigurationReceived(bool allowChat, bool receiveAudio, bool receiveVideo)
|
|
{
|
|
logger.Info("Proctoring configuration received.");
|
|
|
|
settings.JitsiMeet.AllowChat = allowChat;
|
|
settings.JitsiMeet.ReceiveAudio = receiveAudio;
|
|
settings.JitsiMeet.ReceiveVideo = receiveVideo;
|
|
|
|
settings.Zoom.AllowChat = allowChat;
|
|
settings.Zoom.ReceiveAudio = receiveAudio;
|
|
settings.Zoom.ReceiveVideo = receiveVideo;
|
|
|
|
if (allowChat || receiveVideo)
|
|
{
|
|
settings.WindowVisibility = WindowVisibility.AllowToHide;
|
|
}
|
|
else
|
|
{
|
|
settings.WindowVisibility = windowVisibility;
|
|
}
|
|
|
|
StopProctoring();
|
|
StartProctoring();
|
|
}
|
|
|
|
private void StartProctoring()
|
|
{
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
try
|
|
{
|
|
var content = LoadContent(settings);
|
|
|
|
filePath = Path.Combine(appConfig.TemporaryDirectory, $"{Path.GetRandomFileName()}_index.html");
|
|
fileSystem.Save(content, filePath);
|
|
|
|
control = new ProctoringControl(logger.CloneFor(nameof(ProctoringControl)), settings);
|
|
control.CreationProperties = new CoreWebView2CreationProperties { UserDataFolder = appConfig.TemporaryDirectory };
|
|
control.EnsureCoreWebView2Async().ContinueWith(_ =>
|
|
{
|
|
control.Dispatcher.Invoke(() =>
|
|
{
|
|
control.CoreWebView2.Navigate(filePath);
|
|
});
|
|
});
|
|
|
|
window = uiFactory.CreateProctoringWindow(control);
|
|
window.SetTitle(settings.JitsiMeet.Enabled ? settings.JitsiMeet.Subject : settings.Zoom.Subject);
|
|
window.Show();
|
|
|
|
if (settings.WindowVisibility == WindowVisibility.AllowToShow || settings.WindowVisibility == WindowVisibility.Hidden)
|
|
{
|
|
if (settings.Zoom.Enabled)
|
|
{
|
|
window.HideWithDelay();
|
|
}
|
|
else
|
|
{
|
|
window.Hide();
|
|
}
|
|
}
|
|
|
|
IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ProctoringNotification_Active.xaml") };
|
|
Tooltip = text.Get(TextKey.Notification_ProctoringActiveTooltip);
|
|
NotificationChanged?.Invoke();
|
|
|
|
logger.Info($"Started proctoring with {(settings.JitsiMeet.Enabled ? "Jitsi Meet" : "Zoom")}.");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Error($"Failed to start proctoring! Reason: {e.Message}", e);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void StopProctoring()
|
|
{
|
|
if (control != default(ProctoringControl) && window != default(IProctoringWindow))
|
|
{
|
|
control.Dispatcher.Invoke(() =>
|
|
{
|
|
if (settings.JitsiMeet.Enabled)
|
|
{
|
|
control.ExecuteScriptAsync("api.executeCommand('hangup'); api.dispose();");
|
|
}
|
|
else if (settings.Zoom.Enabled)
|
|
{
|
|
control.ExecuteScriptAsync("ZoomMtg.leaveMeeting({});");
|
|
}
|
|
|
|
Thread.Sleep(2000);
|
|
|
|
window.Close();
|
|
control = default(ProctoringControl);
|
|
window = default(IProctoringWindow);
|
|
fileSystem.Delete(filePath);
|
|
|
|
logger.Info("Stopped proctoring.");
|
|
});
|
|
}
|
|
}
|
|
|
|
private string LoadContent(ProctoringSettings settings)
|
|
{
|
|
var provider = settings.JitsiMeet.Enabled ? "JitsiMeet" : "Zoom";
|
|
var assembly = Assembly.GetAssembly(typeof(ProctoringController));
|
|
var path = $"{typeof(ProctoringController).Namespace}.{provider}.index.html";
|
|
|
|
using (var stream = assembly.GetManifestResourceStream(path))
|
|
using (var reader = new StreamReader(stream))
|
|
{
|
|
var html = reader.ReadToEnd();
|
|
|
|
if (settings.JitsiMeet.Enabled)
|
|
{
|
|
html = html.Replace("%%_ALLOW_CHAT_%%", settings.JitsiMeet.AllowChat ? "chat" : "");
|
|
html = html.Replace("%%_ALLOW_CLOSED_CAPTIONS_%%", settings.JitsiMeet.AllowClosedCaptions ? "closedcaptions" : "");
|
|
html = html.Replace("%%_ALLOW_RAISE_HAND_%%", settings.JitsiMeet.AllowRaiseHand ? "raisehand" : "");
|
|
html = html.Replace("%%_ALLOW_RECORDING_%%", settings.JitsiMeet.AllowRecording ? "recording" : "");
|
|
html = html.Replace("%%_ALLOW_TILE_VIEW", settings.JitsiMeet.AllowTileView ? "tileview" : "");
|
|
html = html.Replace("'%_AUDIO_MUTED_%'", settings.JitsiMeet.AudioMuted && settings.WindowVisibility != WindowVisibility.Hidden ? "true" : "false");
|
|
html = html.Replace("'%_AUDIO_ONLY_%'", settings.JitsiMeet.AudioOnly ? "true" : "false");
|
|
html = html.Replace("'%_VIDEO_MUTED_%'", settings.JitsiMeet.VideoMuted && settings.WindowVisibility != WindowVisibility.Hidden ? "true" : "false");
|
|
}
|
|
else if (settings.Zoom.Enabled)
|
|
{
|
|
html = html.Replace("'%_ALLOW_CHAT_%'", settings.Zoom.AllowChat ? "true" : "false");
|
|
html = html.Replace("'%_ALLOW_CLOSED_CAPTIONS_%'", settings.Zoom.AllowClosedCaptions ? "true" : "false");
|
|
html = html.Replace("'%_ALLOW_RAISE_HAND_%'", settings.Zoom.AllowRaiseHand ? "true" : "false");
|
|
html = html.Replace("'%_AUDIO_MUTED_%'", settings.Zoom.AudioMuted && settings.WindowVisibility != WindowVisibility.Hidden ? "true" : "false");
|
|
html = html.Replace("'%_VIDEO_MUTED_%'", settings.Zoom.VideoMuted && settings.WindowVisibility != WindowVisibility.Hidden ? "true" : "false");
|
|
}
|
|
|
|
return html;
|
|
}
|
|
}
|
|
|
|
private string Sanitize(string serverUrl)
|
|
{
|
|
return serverUrl?.Replace($"{Uri.UriSchemeHttp}{Uri.SchemeDelimiter}", "").Replace($"{Uri.UriSchemeHttps}{Uri.SchemeDelimiter}", "");
|
|
}
|
|
}
|
|
}
|