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 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,

View file

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

View file

@ -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")
{

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 = {
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;

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

View file

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

View file

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

View file

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

View file

@ -56,7 +56,7 @@
</ItemGroup>
<ItemGroup>
<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\FindRequestedEventHandler.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);
}
}";
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}");
}
});
}

View file

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