SEBWIN-820, #764: Implemented cross-window sharing of clipboard content for isolated clipboard policy.
This commit is contained in:
parent
ff5b91c010
commit
9045b852d0
13 changed files with 199 additions and 32 deletions
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
{
|
||||
|
|
72
SafeExamBrowser.Browser/Clipboard.cs
Normal file
72
SafeExamBrowser.Browser/Clipboard.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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}!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue