Browse Source

WASM - Check permissions before reading or writing to clipboard (#21573)

* check permissions before reading or writing to clipboard

* throw UnauthorizedAccessException when permission is not granted for clipboard

* handle unauthorized access exceptions for clipboard in textbox

* check only for denied errors on clipboard

* addressed review

* wasm - use marshal methods instead when reading clipboard

* log unauthorized read clipboard  error

* Improve logging

* Also log in MaskedTextBox

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/21634/head
Emmanuel Hansen 3 months ago
committed by GitHub
parent
commit
b84f5db167
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      src/Avalonia.Controls/MaskedTextBox.cs
  2. 16
      src/Avalonia.Controls/TextBox.cs
  3. 27
      src/Browser/Avalonia.Browser/ClipboardImpl.cs
  4. 7
      src/Browser/Avalonia.Browser/Interop/InputHelper.cs
  5. 92
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts

6
src/Avalonia.Controls/MaskedTextBox.cs

@ -5,7 +5,7 @@ using System.Globalization;
using System.Linq;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Logging;
using Avalonia.VisualTree;
namespace Avalonia.Controls
@ -224,6 +224,10 @@ namespace Avalonia.Controls
{
// Silently ignore.
}
catch (UnauthorizedAccessException uex)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, "Failed to read text from clipboard: {Error}", uex);
}
if (text == null)
return;

16
src/Avalonia.Controls/TextBox.cs

@ -13,6 +13,7 @@ using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
@ -1302,8 +1303,15 @@ namespace Avalonia.Controls
{
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
if (clipboard != null)
await clipboard.SetTextAsync(text);
try
{
if (clipboard != null)
await clipboard.SetTextAsync(text);
}
catch (UnauthorizedAccessException uex)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, "Failed to write text to clipboard: {Error}", uex);
}
}
}
@ -1333,6 +1341,10 @@ namespace Avalonia.Controls
{
// Silently ignore.
}
catch (UnauthorizedAccessException uex)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, "Failed to read text from clipboard: {Error}", uex);
}
}
if (string.IsNullOrEmpty(text))

27
src/Browser/Avalonia.Browser/ClipboardImpl.cs

@ -2,6 +2,7 @@
using System.IO;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Logging;
@ -14,8 +15,13 @@ internal sealed class ClipboardImpl : IClipboardImpl
{
public async Task<IAsyncDataTransfer?> TryGetDataAsync()
{
var jsItems = await ReadClipboardAsync(BrowserWindowingPlatform.GlobalThis).ConfigureAwait(false);
return jsItems.GetPropertyAsInt32("length") == 0 ? null : new BrowserClipboardDataTransfer(jsItems);
var result = await ReadClipboardAsync(BrowserWindowingPlatform.GlobalThis).ConfigureAwait(false);
if (result.GetPropertyAsString("error") == "denied")
{
throw new UnauthorizedAccessException("Read permission is not granted for clipboard");
}
var items = result.GetPropertyAsJSObject("result");
return items == null ? null : items.GetPropertyAsInt32("length") == 0 ? null : new BrowserClipboardDataTransfer(items);
}
public async Task SetDataAsync(IAsyncDataTransfer dataTransfer)
@ -29,7 +35,18 @@ internal sealed class ClipboardImpl : IClipboardImpl
}
// However, ConfigureAwait(false) is fine here: we're not doing anything after.
await WriteClipboardAsync(BrowserWindowingPlatform.GlobalThis, source).ConfigureAwait(false);
await WriteClipboardAsync(source).ConfigureAwait(false);
}
private async Task WriteClipboardAsync(JSObject? source)
{
// However, ConfigureAwait(false) is fine here: we're not doing anything after.
var error = await InputHelper.WriteClipboardAsync(BrowserWindowingPlatform.GlobalThis, source).ConfigureAwait(false);
if (error == "denied")
{
throw new UnauthorizedAccessException("Write permission is not granted for clipboard");
}
}
private async Task TryAddItemAsync(IAsyncDataTransferItem dataTransferItem, JSObject source)
@ -55,7 +72,7 @@ internal sealed class ClipboardImpl : IClipboardImpl
continue;
}
if(DataFormat.Bitmap.Equals(format))
if (DataFormat.Bitmap.Equals(format))
{
var bitmap = await dataTransferItem.TryGetValueAsync(DataFormat.Bitmap);
if (bitmap != null)
@ -106,5 +123,5 @@ internal sealed class ClipboardImpl : IClipboardImpl
}
public Task ClearAsync()
=> WriteClipboardAsync(BrowserWindowingPlatform.GlobalThis, null);
=> WriteClipboardAsync(null);
}

7
src/Browser/Avalonia.Browser/Interop/InputHelper.cs

@ -8,11 +8,12 @@ internal static partial class InputHelper
{
public static Task RedirectInputAsync(int topLevelId, Action<BrowserTopLevelImpl> handler)
{
if (BrowserTopLevelImpl.TryGetTopLevel(topLevelId) is { } topLevelImpl) handler(topLevelImpl);
if (BrowserTopLevelImpl.TryGetTopLevel(topLevelId) is { } topLevelImpl)
handler(topLevelImpl);
return Task.CompletedTask;
}
public static Task<T> RedirectInputRetunAsync<T>(int topLevelId, Func<BrowserTopLevelImpl,T> handler, T @default)
public static Task<T> RedirectInputRetunAsync<T>(int topLevelId, Func<BrowserTopLevelImpl, T> handler, T @default)
{
if (BrowserTopLevelImpl.TryGetTopLevel(topLevelId) is { } topLevelImpl)
return Task.FromResult(handler(topLevelImpl));
@ -132,7 +133,7 @@ internal static partial class InputHelper
public static partial Task<JSObject> ReadClipboardAsync(JSObject window);
[JSImport("InputHelper.writeClipboard", AvaloniaModule.MainModuleName)]
public static partial Task WriteClipboardAsync(JSObject globalThis, JSObject? source);
public static partial Task<string> WriteClipboardAsync(JSObject globalThis, JSObject? source);
[JSImport("InputHelper.getReadableDataItemFormats", AvaloniaModule.MainModuleName)]
public static partial string[] GetReadableDataItemFormats(JSObject item);

92
src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts

@ -47,6 +47,11 @@ interface WriteableClipboardSource {
items: WriteableClipboardItem[];
}
interface ClipboardResult {
error: string;
result: ReadableDataItem[]
}
type ReadableDataItem = {
type: "clipboardItem";
value: ClipboardItem;
@ -71,7 +76,7 @@ type ReadableDataValue = {
export class InputHelper {
static clipboardState: ClipboardState = ClipboardState.None;
static resolveClipboard?: (value: readonly ReadableDataItem[]) => void;
static resolveClipboard?: (value: ClipboardResult) => void;
static rejectClipboard?: (reason?: any) => void;
public static initializeBackgroundHandlers() {
@ -85,7 +90,7 @@ export class InputHelper {
}
const items = this.getDataTransferItems(args.clipboardData);
this.resolveClipboard(items.map((item) => ({ type: "dataTransferItem", value: item })));
this.resolveClipboard({ result: items.map((item) => ({ type: "dataTransferItem", value: item }))} as ClipboardResult);
});
this.clipboardState = ClipboardState.Ready;
}
@ -122,43 +127,64 @@ export class InputHelper {
item.data[format] = new Blob([bytes], { type: format });
}
public static async readClipboard(window: Window): Promise<readonly ReadableDataItem[]> {
public static async readClipboard(window: Window): Promise<ClipboardResult> {
const clipboard = window.navigator.clipboard;
if (clipboard.read) {
const clipboardItems = await clipboard.read();
return clipboardItems.map((item) => ({ type: "clipboardItem", value: item }));
} else if (clipboard.readText) {
const item: ReadableDataItem = {
type: "string",
value: await clipboard.readText()
};
return [item];
} else {
try {
return await new Promise<readonly ReadableDataItem[]>((resolve, reject) => {
this.clipboardState = ClipboardState.Pending;
this.resolveClipboard = resolve;
this.rejectClipboard = reject;
});
} finally {
this.clipboardState = ClipboardState.Ready;
this.resolveClipboard = undefined;
this.rejectClipboard = undefined;
try {
if (clipboard.read) {
const clipboardItems = await clipboard.read();
return { result: clipboardItems.map((item) => ({ type: "clipboardItem", value: item })) } as ClipboardResult;
} else if (clipboard.readText) {
const item: ReadableDataItem = {
type: "string",
value: await clipboard.readText()
};
return { result: [item] } as ClipboardResult;
} else {
try {
return await new Promise<ClipboardResult>((resolve, reject) => {
this.clipboardState = ClipboardState.Pending;
this.resolveClipboard = resolve;
this.rejectClipboard = reject;
});
} finally {
this.clipboardState = ClipboardState.Ready;
this.resolveClipboard = undefined;
this.rejectClipboard = undefined;
}
}
}
catch (ex: unknown) {
if (ex instanceof Error && ex.name == "NotAllowedError") {
return { error: "denied" } as ClipboardResult;
}
throw ex;
}
}
public static async writeClipboard(window: Window, source?: WriteableClipboardSource | null): Promise<void> {
const items = source?.items ?? [];
if (items.length === 0) {
await window.navigator.clipboard.writeText("");
return;
}
public static async writeClipboard(window: Window, source?: WriteableClipboardSource | null): Promise<string> {
try {
const items = source?.items ?? [];
if (items.length === 0) {
await window.navigator.clipboard.writeText("");
return "";
}
if (window.navigator.clipboard.write) {
await window.navigator.clipboard.write(items.map(item => new ClipboardItem(item.data)))
}
else {
await this.writeFirstText(window, items)
}
return window.navigator.clipboard.write
? await window.navigator.clipboard.write(items.map(item => new ClipboardItem(item.data)))
: await this.writeFirstText(window, items);
return "";
}
catch (error: unknown) {
if (error instanceof Error && error.name == "NotAllowedError") {
return "denied";
}
throw error;
}
}
private static async writeFirstText(window: Window, items: WriteableClipboardItem[]): Promise<void> {
@ -505,7 +531,7 @@ export class InputHelper {
};
}
return () => {};
return () => { };
}
public static clearInput(inputElement: HTMLInputElement) {

Loading…
Cancel
Save