From 9045b852d0e685d45160e536a890847c8ef94c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20B=C3=BCchel?= Date: Mon, 4 Mar 2024 14:27:49 +0100 Subject: [PATCH] SEBWIN-820, #764: Implemented cross-window sharing of clipboard content for isolated clipboard policy. --- SafeExamBrowser.Browser/BrowserApplication.cs | 3 + SafeExamBrowser.Browser/BrowserControl.cs | 63 ++++++++++++---- SafeExamBrowser.Browser/BrowserWindow.cs | 8 ++- SafeExamBrowser.Browser/Clipboard.cs | 72 +++++++++++++++++++ SafeExamBrowser.Browser/Content/Clipboard.js | 33 +++++++-- .../Events/ClipboardChangedEventHandler.cs | 12 ++++ .../SafeExamBrowser.Browser.csproj | 3 + .../ConfigurationData/DataProcessor.cs | 13 ++++ .../Browser/Data/JavascriptResult.cs | 2 +- .../Browser/IBrowserControl.cs | 4 +- ...ExamBrowser.UserInterface.Contracts.csproj | 2 +- .../Windows/BrowserWindow.xaml.cs | 8 +-- .../Windows/BrowserWindow.xaml.cs | 8 +-- 13 files changed, 199 insertions(+), 32 deletions(-) create mode 100644 SafeExamBrowser.Browser/Clipboard.cs create mode 100644 SafeExamBrowser.Browser/Events/ClipboardChangedEventHandler.cs diff --git a/SafeExamBrowser.Browser/BrowserApplication.cs b/SafeExamBrowser.Browser/BrowserApplication.cs index 107cdc5c..2aa4e7f8 100644 --- a/SafeExamBrowser.Browser/BrowserApplication.cs +++ b/SafeExamBrowser.Browser/BrowserApplication.cs @@ -39,6 +39,7 @@ namespace SafeExamBrowser.Browser private int windowIdCounter = default; private readonly AppConfig appConfig; + private readonly Clipboard clipboard; private readonly IFileSystemDialog fileSystemDialog; private readonly IHashAlgorithm hashAlgorithm; private readonly IKeyGenerator keyGenerator; @@ -77,6 +78,7 @@ namespace SafeExamBrowser.Browser IUserInterfaceFactory uiFactory) { this.appConfig = appConfig; + this.clipboard = new Clipboard(logger.CloneFor(nameof(Clipboard)), settings); this.fileSystemDialog = fileSystemDialog; this.hashAlgorithm = hashAlgorithm; this.keyGenerator = keyGenerator; @@ -191,6 +193,7 @@ namespace SafeExamBrowser.Browser var windowLogger = logger.CloneFor($"Browser Window #{id}"); var window = new BrowserWindow( appConfig, + clipboard, fileSystemDialog, hashAlgorithm, id, diff --git a/SafeExamBrowser.Browser/BrowserControl.cs b/SafeExamBrowser.Browser/BrowserControl.cs index f9a70ecd..5514a420 100644 --- a/SafeExamBrowser.Browser/BrowserControl.cs +++ b/SafeExamBrowser.Browser/BrowserControl.cs @@ -7,10 +7,12 @@ */ using System; +using System.Linq; using System.Threading.Tasks; using CefSharp; using SafeExamBrowser.Browser.Wrapper; using SafeExamBrowser.Browser.Wrapper.Events; +using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.UserInterface.Contracts.Browser; using SafeExamBrowser.UserInterface.Contracts.Browser.Data; using SafeExamBrowser.UserInterface.Contracts.Browser.Events; @@ -19,11 +21,13 @@ namespace SafeExamBrowser.Browser { internal class BrowserControl : IBrowserControl { + private readonly Clipboard clipboard; private readonly ICefSharpControl control; private readonly IDialogHandler dialogHandler; private readonly IDisplayHandler displayHandler; private readonly IDownloadHandler downloadHandler; private readonly IKeyboardHandler keyboardHandler; + private readonly ILogger logger; private readonly IRenderProcessMessageHandler renderProcessMessageHandler; private readonly IRequestHandler requestHandler; @@ -38,19 +42,23 @@ namespace SafeExamBrowser.Browser public event TitleChangedEventHandler TitleChanged; public BrowserControl( + Clipboard clipboard, ICefSharpControl control, IDialogHandler dialogHandler, IDisplayHandler displayHandler, IDownloadHandler downloadHandler, IKeyboardHandler keyboardHandler, + ILogger logger, IRenderProcessMessageHandler renderProcessMessageHandler, IRequestHandler requestHandler) { this.control = control; + this.clipboard = clipboard; this.dialogHandler = dialogHandler; this.displayHandler = displayHandler; this.downloadHandler = downloadHandler; this.keyboardHandler = keyboardHandler; + this.logger = logger; this.renderProcessMessageHandler = renderProcessMessageHandler; this.requestHandler = requestHandler; } @@ -63,25 +71,37 @@ namespace SafeExamBrowser.Browser } } - public void ExecuteJavascript(string javascript, Action callback) + public void ExecuteJavaScript(string code, Action callback = default) { - if ((control as IWebBrowser)?.CanExecuteJavascriptInMainFrame == true) + try { - control.EvaluateScriptAsync(javascript).ContinueWith(t => + if (control.BrowserCore != default && control.BrowserCore.MainFrame != default) { - callback(new JavascriptResult + control.BrowserCore.EvaluateScriptAsync(code).ContinueWith(t => { - Message = t.Result.Message, - Result = t.Result.Result, - Success = t.Result.Success + callback?.Invoke(new JavaScriptResult + { + Message = t.Result.Message, + Result = t.Result.Result, + Success = t.Result.Success + }); }); - }); - } - else - { - Task.Run(() => callback(new JavascriptResult + } + else { - Message = "JavaScript can't be executed in main frame!", + Task.Run(() => callback?.Invoke(new JavaScriptResult + { + Message = "JavaScript can't be executed in main frame!", + Success = false + })); + } + } + catch (Exception e) + { + logger.Error($"Failed to execute JavaScript '{(code.Length > 50 ? code.Take(50) : code)}'!", e); + Task.Run(() => callback?.Invoke(new JavaScriptResult + { + Message = $"Failed to execute JavaScript '{(code.Length > 50 ? code.Take(50) : code)}'! Reason: {e.Message}", Success = false })); } @@ -94,6 +114,8 @@ namespace SafeExamBrowser.Browser public void Initialize() { + clipboard.Changed += Clipboard_Changed; + control.AddressChanged += (o, e) => AddressChanged?.Invoke(e.Address); control.AuthCredentialsRequired += (w, b, o, i, h, p, r, s, c, a) => a.Value = requestHandler.GetAuthCredentials(w, b, o, i, h, p, r, s, c); control.BeforeBrowse += (w, b, f, r, u, i, a) => a.Value = requestHandler.OnBeforeBrowse(w, b, f, r, u, i); @@ -115,6 +137,11 @@ namespace SafeExamBrowser.Browser control.ResourceRequestHandlerRequired += (IWebBrowser w, IBrowser b, IFrame f, IRequest r, bool n, bool d, string i, ref bool h, ResourceRequestEventArgs a) => a.Handler = requestHandler.GetResourceRequestHandler(w, b, f, r, n, d, i, ref h); control.TitleChanged += (o, e) => TitleChanged?.Invoke(e.Title); control.UncaughtExceptionEvent += (w, b, f, e) => renderProcessMessageHandler.OnUncaughtException(w, b, f, e); + + if (control is IWebBrowser webBrowser) + { + webBrowser.JavascriptMessageReceived += WebBrowser_JavascriptMessageReceived; + } } public void NavigateBackwards() @@ -147,6 +174,11 @@ namespace SafeExamBrowser.Browser control.BrowserCore.SetZoomLevel(level); } + private void Clipboard_Changed(long id) + { + ExecuteJavaScript($"SafeExamBrowser.clipboard.update({id}, '{clipboard.Content}');"); + } + private void Control_IsBrowserInitializedChanged(object sender, EventArgs e) { if (control.IsBrowserInitialized) @@ -154,5 +186,10 @@ namespace SafeExamBrowser.Browser control.BrowserCore.GetHost().SetFocus(true); } } + + private void WebBrowser_JavascriptMessageReceived(object sender, JavascriptMessageReceivedEventArgs e) + { + clipboard.Process(e); + } } } diff --git a/SafeExamBrowser.Browser/BrowserWindow.cs b/SafeExamBrowser.Browser/BrowserWindow.cs index da25e4e1..4086edad 100644 --- a/SafeExamBrowser.Browser/BrowserWindow.cs +++ b/SafeExamBrowser.Browser/BrowserWindow.cs @@ -48,6 +48,7 @@ namespace SafeExamBrowser.Browser private const double ZOOM_FACTOR = 0.2; private readonly AppConfig appConfig; + private readonly Clipboard clipboard; private readonly IFileSystemDialog fileSystemDialog; private readonly IHashAlgorithm hashAlgorithm; private readonly HttpClient httpClient; @@ -92,6 +93,7 @@ namespace SafeExamBrowser.Browser public BrowserWindow( AppConfig appConfig, + Clipboard clipboard, IFileSystemDialog fileSystemDialog, IHashAlgorithm hashAlgorithm, int id, @@ -106,6 +108,7 @@ namespace SafeExamBrowser.Browser IUserInterfaceFactory uiFactory) { this.appConfig = appConfig; + this.clipboard = clipboard; this.fileSystemDialog = fileSystemDialog; this.hashAlgorithm = hashAlgorithm; this.httpClient = new HttpClient(); @@ -149,6 +152,7 @@ namespace SafeExamBrowser.Browser internal void InitializeControl() { var cefSharpControl = default(ICefSharpControl); + var controlLogger = logger.CloneFor($"{nameof(BrowserControl)} #{Id}"); var dialogHandler = new DialogHandler(); var displayHandler = new DisplayHandler(); var downloadLogger = logger.CloneFor($"{nameof(DownloadHandler)} #{Id}"); @@ -191,7 +195,7 @@ namespace SafeExamBrowser.Browser InitializeRequestFilter(requestFilter); - Control = new BrowserControl(cefSharpControl, dialogHandler, displayHandler, downloadHandler, keyboardHandler, renderHandler, requestHandler); + Control = new BrowserControl(clipboard, cefSharpControl, dialogHandler, displayHandler, downloadHandler, keyboardHandler, controlLogger, renderHandler, requestHandler); Control.AddressChanged += Control_AddressChanged; Control.LoadFailed += Control_LoadFailed; Control.LoadingStateChanged += Control_LoadingStateChanged; @@ -499,7 +503,7 @@ namespace SafeExamBrowser.Browser private void KeyboardHandler_TabPressed(bool shiftPressed) { - Control.ExecuteJavascript("document.activeElement.tagName", result => + Control.ExecuteJavaScript("document.activeElement.tagName", result => { if (result.Result is string tagName && tagName?.ToUpper() == "BODY") { diff --git a/SafeExamBrowser.Browser/Clipboard.cs b/SafeExamBrowser.Browser/Clipboard.cs new file mode 100644 index 00000000..82629524 --- /dev/null +++ b/SafeExamBrowser.Browser/Clipboard.cs @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 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.Threading.Tasks; +using CefSharp; +using SafeExamBrowser.Browser.Events; +using SafeExamBrowser.Logging.Contracts; +using BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings; + +namespace SafeExamBrowser.Browser +{ + internal class Clipboard + { + private readonly ILogger logger; + private readonly BrowserSettings settings; + + internal string Content { get; private set; } + + internal event ClipboardChangedEventHandler Changed; + + internal Clipboard(ILogger logger, BrowserSettings settings) + { + this.logger = logger; + this.settings = settings; + } + + internal void Process(JavascriptMessageReceivedEventArgs message) + { + if (settings.UseIsolatedClipboard) + { + try + { + var data = message.ConvertMessageTo(); + + if (data != default && data.Type == "Clipboard" && TrySetContent(data.Content)) + { + Task.Run(() => Changed?.Invoke(data.Id)); + } + } + catch (Exception e) + { + logger.Error($"Failed to process browser message '{message}'!", e); + } + } + } + + private bool TrySetContent(object value) + { + var text = value as string; + + if (text != default) + { + Content = text; + } + + return text != default; + } + + private class Data + { + public string Content { get; set; } + public long Id { get; set; } + public string Type { get; set; } + } + } +} diff --git a/SafeExamBrowser.Browser/Content/Clipboard.js b/SafeExamBrowser.Browser/Content/Clipboard.js index 59c54251..6f66aa24 100644 --- a/SafeExamBrowser.Browser/Content/Clipboard.js +++ b/SafeExamBrowser.Browser/Content/Clipboard.js @@ -9,12 +9,31 @@ */ SafeExamBrowser.clipboard = { - clear: function () { - ranges = []; - text = ""; - }, + id: Math.round((Date.now() + Math.random()) * 1000), ranges: [], - text: "" + text: "", + + clear: function () { + this.ranges = []; + this.text = ""; + }, + + getContentEncoded: function () { + var bytes = new TextEncoder().encode(this.text); + var base64 = btoa(String.fromCodePoint(...bytes)); + + return base64; + }, + + update: function (id, base64) { + if (this.id != id) { + var bytes = Uint8Array.from(atob(base64), (m) => m.codePointAt(0)); + var content = new TextDecoder().decode(bytes); + + this.ranges = []; + this.text = content; + } + } } function copySelectedData(e) { @@ -134,6 +153,8 @@ function onCopy(e) { try { copySelectedData(e); + + CefSharp.PostMessage({ Type: "Clipboard", Id: SafeExamBrowser.clipboard.id, Content: SafeExamBrowser.clipboard.getContentEncoded() }); } finally { e.preventDefault(); e.returnValue = false; @@ -148,6 +169,8 @@ function onCut(e) { try { copySelectedData(e); cutSelectedData(e); + + CefSharp.PostMessage({ Type: "Clipboard", Id: SafeExamBrowser.clipboard.id, Content: SafeExamBrowser.clipboard.getContentEncoded() }); } finally { e.preventDefault(); e.returnValue = false; diff --git a/SafeExamBrowser.Browser/Events/ClipboardChangedEventHandler.cs b/SafeExamBrowser.Browser/Events/ClipboardChangedEventHandler.cs new file mode 100644 index 00000000..035114ff --- /dev/null +++ b/SafeExamBrowser.Browser/Events/ClipboardChangedEventHandler.cs @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2023 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.Browser.Events +{ + internal delegate void ClipboardChangedEventHandler(long id); +} diff --git a/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj b/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj index d4492386..189deceb 100644 --- a/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj +++ b/SafeExamBrowser.Browser/SafeExamBrowser.Browser.csproj @@ -80,6 +80,8 @@ + + @@ -192,6 +194,7 @@ + diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs index 9ba7d7a5..1a8149e7 100644 --- a/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs +++ b/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs @@ -27,6 +27,19 @@ namespace SafeExamBrowser.Configuration.ConfigurationData InitializeClipboardSettings(settings); InitializeProctoringSettings(settings); RemoveLegacyBrowsers(settings); + + settings.Applications.Blacklist.Clear(); + settings.Applications.Whitelist.Clear(); + settings.Browser.MainWindow.AllowAddressBar = true; + settings.Browser.MainWindow.AllowBackwardNavigation = true; + settings.Browser.MainWindow.AllowForwardNavigation = true; + settings.Browser.AllowPageZoom = true; + settings.Browser.AllowPdfReader = true; + settings.Browser.MainWindow.ShowToolbar = true; + settings.LogLevel = Settings.Logging.LogLevel.Debug; + settings.Security.AllowApplicationLogAccess = true; + settings.Security.AllowReconfiguration = true; + settings.Taskbar.ShowApplicationLog = true; } private void AllowBrowserToolbarForReloading(AppSettings settings) diff --git a/SafeExamBrowser.UserInterface.Contracts/Browser/Data/JavascriptResult.cs b/SafeExamBrowser.UserInterface.Contracts/Browser/Data/JavascriptResult.cs index 209006fa..47306acd 100644 --- a/SafeExamBrowser.UserInterface.Contracts/Browser/Data/JavascriptResult.cs +++ b/SafeExamBrowser.UserInterface.Contracts/Browser/Data/JavascriptResult.cs @@ -11,7 +11,7 @@ namespace SafeExamBrowser.UserInterface.Contracts.Browser.Data /// /// The data resulting from a JavaScript expression evaluation. /// - public class JavascriptResult + public class JavaScriptResult { /// /// Indicates if the JavaScript was evaluated successfully or not. diff --git a/SafeExamBrowser.UserInterface.Contracts/Browser/IBrowserControl.cs b/SafeExamBrowser.UserInterface.Contracts/Browser/IBrowserControl.cs index 15146894..5337837a 100644 --- a/SafeExamBrowser.UserInterface.Contracts/Browser/IBrowserControl.cs +++ b/SafeExamBrowser.UserInterface.Contracts/Browser/IBrowserControl.cs @@ -64,9 +64,9 @@ namespace SafeExamBrowser.UserInterface.Contracts.Browser void Destroy(); /// - /// Executes the given JavaScript code in the browser. + /// Executes the given JavaScript code in the browser. An optional callback may be used to process a potential . /// - void ExecuteJavascript(string code, Action callback); + void ExecuteJavaScript(string code, Action callback = default); /// /// Attempts to find the given term on the current page according to the specified parameters. diff --git a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj index b75ee7b2..24306ed0 100644 --- a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj +++ b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj @@ -56,7 +56,7 @@ - + diff --git a/SafeExamBrowser.UserInterface.Desktop/Windows/BrowserWindow.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Windows/BrowserWindow.xaml.cs index a7353e40..c87e8269 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Windows/BrowserWindow.xaml.cs +++ b/SafeExamBrowser.UserInterface.Desktop/Windows/BrowserWindow.xaml.cs @@ -523,19 +523,19 @@ if (typeof __SEB_focusElement === 'undefined') { setTimeout(function () { item && item.focus && item.focus(); }, 20); } }"; - browserControl.ExecuteJavascript(javascript, result => + browserControl.ExecuteJavaScript(javascript, result => { if (!result.Success) { - logger.Error($"Failed to initialize JavaScript: {result.Message}"); + logger.Warn($"Failed to initialize JavaScript: {result.Message}"); } }); - browserControl.ExecuteJavascript("__SEB_focusElement(" + forward.ToString().ToLower() + ")", result => + browserControl.ExecuteJavaScript("__SEB_focusElement(" + forward.ToString().ToLower() + ")", result => { if (!result.Success) { - logger.Error($"Failed to execute JavaScript: {result.Message}"); + logger.Warn($"Failed to execute JavaScript: {result.Message}"); } }); } diff --git a/SafeExamBrowser.UserInterface.Mobile/Windows/BrowserWindow.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Windows/BrowserWindow.xaml.cs index 09807005..3feecaa9 100644 --- a/SafeExamBrowser.UserInterface.Mobile/Windows/BrowserWindow.xaml.cs +++ b/SafeExamBrowser.UserInterface.Mobile/Windows/BrowserWindow.xaml.cs @@ -518,19 +518,19 @@ if (typeof __SEB_focusElement === 'undefined') { setTimeout(function () { item && item.focus && item.focus(); }, 20); } }"; - browserControl.ExecuteJavascript(javascript, result => + browserControl.ExecuteJavaScript(javascript, result => { if (!result.Success) { - logger.Error($"Failed to initialize JavaScript: {result.Message}!"); + logger.Warn($"Failed to initialize JavaScript: {result.Message}!"); } }); - browserControl.ExecuteJavascript("__SEB_focusElement(" + forward.ToString().ToLower() + ")", result => + browserControl.ExecuteJavaScript("__SEB_focusElement(" + forward.ToString().ToLower() + ")", result => { if (!result.Success) { - logger.Error($"Failed to execute JavaScript: {result.Message}!"); + logger.Warn($"Failed to execute JavaScript: {result.Message}!"); } }); }