SEBWIN-820, #764: Implemented cross-window sharing of clipboard content for isolated clipboard policy.

This commit is contained in:
Damian Büchel 2024-03-04 14:27:49 +01:00
parent ff5b91c010
commit 9045b852d0
13 changed files with 199 additions and 32 deletions

View file

@ -39,6 +39,7 @@ namespace SafeExamBrowser.Browser
private int windowIdCounter = default; private int windowIdCounter = default;
private readonly AppConfig appConfig; private readonly AppConfig appConfig;
private readonly Clipboard clipboard;
private readonly IFileSystemDialog fileSystemDialog; private readonly IFileSystemDialog fileSystemDialog;
private readonly IHashAlgorithm hashAlgorithm; private readonly IHashAlgorithm hashAlgorithm;
private readonly IKeyGenerator keyGenerator; private readonly IKeyGenerator keyGenerator;
@ -77,6 +78,7 @@ namespace SafeExamBrowser.Browser
IUserInterfaceFactory uiFactory) IUserInterfaceFactory uiFactory)
{ {
this.appConfig = appConfig; this.appConfig = appConfig;
this.clipboard = new Clipboard(logger.CloneFor(nameof(Clipboard)), settings);
this.fileSystemDialog = fileSystemDialog; this.fileSystemDialog = fileSystemDialog;
this.hashAlgorithm = hashAlgorithm; this.hashAlgorithm = hashAlgorithm;
this.keyGenerator = keyGenerator; this.keyGenerator = keyGenerator;
@ -191,6 +193,7 @@ namespace SafeExamBrowser.Browser
var windowLogger = logger.CloneFor($"Browser Window #{id}"); var windowLogger = logger.CloneFor($"Browser Window #{id}");
var window = new BrowserWindow( var window = new BrowserWindow(
appConfig, appConfig,
clipboard,
fileSystemDialog, fileSystemDialog,
hashAlgorithm, hashAlgorithm,
id, id,

View file

@ -7,10 +7,12 @@
*/ */
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CefSharp; using CefSharp;
using SafeExamBrowser.Browser.Wrapper; using SafeExamBrowser.Browser.Wrapper;
using SafeExamBrowser.Browser.Wrapper.Events; using SafeExamBrowser.Browser.Wrapper.Events;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Browser; using SafeExamBrowser.UserInterface.Contracts.Browser;
using SafeExamBrowser.UserInterface.Contracts.Browser.Data; using SafeExamBrowser.UserInterface.Contracts.Browser.Data;
using SafeExamBrowser.UserInterface.Contracts.Browser.Events; using SafeExamBrowser.UserInterface.Contracts.Browser.Events;
@ -19,11 +21,13 @@ namespace SafeExamBrowser.Browser
{ {
internal class BrowserControl : IBrowserControl internal class BrowserControl : IBrowserControl
{ {
private readonly Clipboard clipboard;
private readonly ICefSharpControl control; private readonly ICefSharpControl control;
private readonly IDialogHandler dialogHandler; private readonly IDialogHandler dialogHandler;
private readonly IDisplayHandler displayHandler; private readonly IDisplayHandler displayHandler;
private readonly IDownloadHandler downloadHandler; private readonly IDownloadHandler downloadHandler;
private readonly IKeyboardHandler keyboardHandler; private readonly IKeyboardHandler keyboardHandler;
private readonly ILogger logger;
private readonly IRenderProcessMessageHandler renderProcessMessageHandler; private readonly IRenderProcessMessageHandler renderProcessMessageHandler;
private readonly IRequestHandler requestHandler; private readonly IRequestHandler requestHandler;
@ -38,19 +42,23 @@ namespace SafeExamBrowser.Browser
public event TitleChangedEventHandler TitleChanged; public event TitleChangedEventHandler TitleChanged;
public BrowserControl( public BrowserControl(
Clipboard clipboard,
ICefSharpControl control, ICefSharpControl control,
IDialogHandler dialogHandler, IDialogHandler dialogHandler,
IDisplayHandler displayHandler, IDisplayHandler displayHandler,
IDownloadHandler downloadHandler, IDownloadHandler downloadHandler,
IKeyboardHandler keyboardHandler, IKeyboardHandler keyboardHandler,
ILogger logger,
IRenderProcessMessageHandler renderProcessMessageHandler, IRenderProcessMessageHandler renderProcessMessageHandler,
IRequestHandler requestHandler) IRequestHandler requestHandler)
{ {
this.control = control; this.control = control;
this.clipboard = clipboard;
this.dialogHandler = dialogHandler; this.dialogHandler = dialogHandler;
this.displayHandler = displayHandler; this.displayHandler = displayHandler;
this.downloadHandler = downloadHandler; this.downloadHandler = downloadHandler;
this.keyboardHandler = keyboardHandler; this.keyboardHandler = keyboardHandler;
this.logger = logger;
this.renderProcessMessageHandler = renderProcessMessageHandler; this.renderProcessMessageHandler = renderProcessMessageHandler;
this.requestHandler = requestHandler; this.requestHandler = requestHandler;
} }
@ -63,25 +71,37 @@ namespace SafeExamBrowser.Browser
} }
} }
public void ExecuteJavascript(string javascript, Action<JavascriptResult> callback) public void ExecuteJavaScript(string code, Action<JavaScriptResult> 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, callback?.Invoke(new JavaScriptResult
Result = t.Result.Result, {
Success = t.Result.Success Message = t.Result.Message,
Result = t.Result.Result,
Success = t.Result.Success
});
}); });
}); }
} else
else
{
Task.Run(() => callback(new JavascriptResult
{ {
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 Success = false
})); }));
} }
@ -94,6 +114,8 @@ namespace SafeExamBrowser.Browser
public void Initialize() public void Initialize()
{ {
clipboard.Changed += Clipboard_Changed;
control.AddressChanged += (o, e) => AddressChanged?.Invoke(e.Address); 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.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); 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.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.TitleChanged += (o, e) => TitleChanged?.Invoke(e.Title);
control.UncaughtExceptionEvent += (w, b, f, e) => renderProcessMessageHandler.OnUncaughtException(w, b, f, e); control.UncaughtExceptionEvent += (w, b, f, e) => renderProcessMessageHandler.OnUncaughtException(w, b, f, e);
if (control is IWebBrowser webBrowser)
{
webBrowser.JavascriptMessageReceived += WebBrowser_JavascriptMessageReceived;
}
} }
public void NavigateBackwards() public void NavigateBackwards()
@ -147,6 +174,11 @@ namespace SafeExamBrowser.Browser
control.BrowserCore.SetZoomLevel(level); 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) private void Control_IsBrowserInitializedChanged(object sender, EventArgs e)
{ {
if (control.IsBrowserInitialized) if (control.IsBrowserInitialized)
@ -154,5 +186,10 @@ namespace SafeExamBrowser.Browser
control.BrowserCore.GetHost().SetFocus(true); control.BrowserCore.GetHost().SetFocus(true);
} }
} }
private void WebBrowser_JavascriptMessageReceived(object sender, JavascriptMessageReceivedEventArgs e)
{
clipboard.Process(e);
}
} }
} }

View file

@ -48,6 +48,7 @@ namespace SafeExamBrowser.Browser
private const double ZOOM_FACTOR = 0.2; private const double ZOOM_FACTOR = 0.2;
private readonly AppConfig appConfig; private readonly AppConfig appConfig;
private readonly Clipboard clipboard;
private readonly IFileSystemDialog fileSystemDialog; private readonly IFileSystemDialog fileSystemDialog;
private readonly IHashAlgorithm hashAlgorithm; private readonly IHashAlgorithm hashAlgorithm;
private readonly HttpClient httpClient; private readonly HttpClient httpClient;
@ -92,6 +93,7 @@ namespace SafeExamBrowser.Browser
public BrowserWindow( public BrowserWindow(
AppConfig appConfig, AppConfig appConfig,
Clipboard clipboard,
IFileSystemDialog fileSystemDialog, IFileSystemDialog fileSystemDialog,
IHashAlgorithm hashAlgorithm, IHashAlgorithm hashAlgorithm,
int id, int id,
@ -106,6 +108,7 @@ namespace SafeExamBrowser.Browser
IUserInterfaceFactory uiFactory) IUserInterfaceFactory uiFactory)
{ {
this.appConfig = appConfig; this.appConfig = appConfig;
this.clipboard = clipboard;
this.fileSystemDialog = fileSystemDialog; this.fileSystemDialog = fileSystemDialog;
this.hashAlgorithm = hashAlgorithm; this.hashAlgorithm = hashAlgorithm;
this.httpClient = new HttpClient(); this.httpClient = new HttpClient();
@ -149,6 +152,7 @@ namespace SafeExamBrowser.Browser
internal void InitializeControl() internal void InitializeControl()
{ {
var cefSharpControl = default(ICefSharpControl); var cefSharpControl = default(ICefSharpControl);
var controlLogger = logger.CloneFor($"{nameof(BrowserControl)} #{Id}");
var dialogHandler = new DialogHandler(); var dialogHandler = new DialogHandler();
var displayHandler = new DisplayHandler(); var displayHandler = new DisplayHandler();
var downloadLogger = logger.CloneFor($"{nameof(DownloadHandler)} #{Id}"); var downloadLogger = logger.CloneFor($"{nameof(DownloadHandler)} #{Id}");
@ -191,7 +195,7 @@ namespace SafeExamBrowser.Browser
InitializeRequestFilter(requestFilter); 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.AddressChanged += Control_AddressChanged;
Control.LoadFailed += Control_LoadFailed; Control.LoadFailed += Control_LoadFailed;
Control.LoadingStateChanged += Control_LoadingStateChanged; Control.LoadingStateChanged += Control_LoadingStateChanged;
@ -499,7 +503,7 @@ namespace SafeExamBrowser.Browser
private void KeyboardHandler_TabPressed(bool shiftPressed) 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") if (result.Result is string tagName && tagName?.ToUpper() == "BODY")
{ {

View file

@ -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<Data>();
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; }
}
}
}

View file

@ -9,12 +9,31 @@
*/ */
SafeExamBrowser.clipboard = { SafeExamBrowser.clipboard = {
clear: function () { id: Math.round((Date.now() + Math.random()) * 1000),
ranges = [];
text = "";
},
ranges: [], 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) { function copySelectedData(e) {
@ -134,6 +153,8 @@ function onCopy(e) {
try { try {
copySelectedData(e); copySelectedData(e);
CefSharp.PostMessage({ Type: "Clipboard", Id: SafeExamBrowser.clipboard.id, Content: SafeExamBrowser.clipboard.getContentEncoded() });
} finally { } finally {
e.preventDefault(); e.preventDefault();
e.returnValue = false; e.returnValue = false;
@ -148,6 +169,8 @@ function onCut(e) {
try { try {
copySelectedData(e); copySelectedData(e);
cutSelectedData(e); cutSelectedData(e);
CefSharp.PostMessage({ Type: "Clipboard", Id: SafeExamBrowser.clipboard.id, Content: SafeExamBrowser.clipboard.getContentEncoded() });
} finally { } finally {
e.preventDefault(); e.preventDefault();
e.returnValue = false; e.returnValue = false;

View file

@ -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);
}

View file

@ -80,6 +80,8 @@
<ItemGroup> <ItemGroup>
<Compile Include="BrowserApplication.cs" /> <Compile Include="BrowserApplication.cs" />
<Compile Include="BrowserWindow.cs" /> <Compile Include="BrowserWindow.cs" />
<Compile Include="Clipboard.cs" />
<Compile Include="Events\ClipboardChangedEventHandler.cs" />
<Compile Include="Events\DialogRequestedEventArgs.cs" /> <Compile Include="Events\DialogRequestedEventArgs.cs" />
<Compile Include="Events\DialogRequestedEventHandler.cs" /> <Compile Include="Events\DialogRequestedEventHandler.cs" />
<Compile Include="Events\DownloadAbortedEventHandler.cs" /> <Compile Include="Events\DownloadAbortedEventHandler.cs" />
@ -192,6 +194,7 @@
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Content\Clipboard.js" /> <EmbeddedResource Include="Content\Clipboard.js" />
</ItemGroup> </ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup> <PropertyGroup>
<PostBuildEvent> <PostBuildEvent>

View file

@ -27,6 +27,19 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
InitializeClipboardSettings(settings); InitializeClipboardSettings(settings);
InitializeProctoringSettings(settings); InitializeProctoringSettings(settings);
RemoveLegacyBrowsers(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) private void AllowBrowserToolbarForReloading(AppSettings settings)

View file

@ -11,7 +11,7 @@ namespace SafeExamBrowser.UserInterface.Contracts.Browser.Data
/// <summary> /// <summary>
/// The data resulting from a JavaScript expression evaluation. /// The data resulting from a JavaScript expression evaluation.
/// </summary> /// </summary>
public class JavascriptResult public class JavaScriptResult
{ {
/// <summary> /// <summary>
/// Indicates if the JavaScript was evaluated successfully or not. /// Indicates if the JavaScript was evaluated successfully or not.

View file

@ -64,9 +64,9 @@ namespace SafeExamBrowser.UserInterface.Contracts.Browser
void Destroy(); void Destroy();
/// <summary> /// <summary>
/// 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 <see cref="JavaScriptResult"/>.
/// </summary> /// </summary>
void ExecuteJavascript(string code, Action<JavascriptResult> callback); void ExecuteJavaScript(string code, Action<JavaScriptResult> callback = default);
/// <summary> /// <summary>
/// Attempts to find the given term on the current page according to the specified parameters. /// Attempts to find the given term on the current page according to the specified parameters.

View file

@ -56,7 +56,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Browser\Data\DownloadItemState.cs" /> <Compile Include="Browser\Data\DownloadItemState.cs" />
<Compile Include="Browser\Data\JavascriptResult.cs" /> <Compile Include="Browser\Data\JavaScriptResult.cs" />
<Compile Include="Browser\Events\AddressChangedEventHandler.cs" /> <Compile Include="Browser\Events\AddressChangedEventHandler.cs" />
<Compile Include="Browser\Events\FindRequestedEventHandler.cs" /> <Compile Include="Browser\Events\FindRequestedEventHandler.cs" />
<Compile Include="Browser\Events\LoadFailedEventHandler.cs" /> <Compile Include="Browser\Events\LoadFailedEventHandler.cs" />

View file

@ -523,19 +523,19 @@ if (typeof __SEB_focusElement === 'undefined') {
setTimeout(function () { item && item.focus && item.focus(); }, 20); setTimeout(function () { item && item.focus && item.focus(); }, 20);
} }
}"; }";
browserControl.ExecuteJavascript(javascript, result => browserControl.ExecuteJavaScript(javascript, result =>
{ {
if (!result.Success) 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) if (!result.Success)
{ {
logger.Error($"Failed to execute JavaScript: {result.Message}"); logger.Warn($"Failed to execute JavaScript: {result.Message}");
} }
}); });
} }

View file

@ -518,19 +518,19 @@ if (typeof __SEB_focusElement === 'undefined') {
setTimeout(function () { item && item.focus && item.focus(); }, 20); setTimeout(function () { item && item.focus && item.focus(); }, 20);
} }
}"; }";
browserControl.ExecuteJavascript(javascript, result => browserControl.ExecuteJavaScript(javascript, result =>
{ {
if (!result.Success) 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) if (!result.Success)
{ {
logger.Error($"Failed to execute JavaScript: {result.Message}!"); logger.Warn($"Failed to execute JavaScript: {result.Message}!");
} }
}); });
} }