diff --git a/src/Avalonia.Controls/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs index cfbd072a88..22c05e7159 100644 --- a/src/Avalonia.Controls/MaskedTextBox.cs +++ b/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; diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index dac2dd98c7..eb72638744 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/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)) diff --git a/src/Browser/Avalonia.Browser/ClipboardImpl.cs b/src/Browser/Avalonia.Browser/ClipboardImpl.cs index 3a4d44c278..50cec8721c 100644 --- a/src/Browser/Avalonia.Browser/ClipboardImpl.cs +++ b/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 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); } diff --git a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs index bb2884b064..4e104b0968 100644 --- a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs @@ -8,11 +8,12 @@ internal static partial class InputHelper { public static Task RedirectInputAsync(int topLevelId, Action handler) { - if (BrowserTopLevelImpl.TryGetTopLevel(topLevelId) is { } topLevelImpl) handler(topLevelImpl); + if (BrowserTopLevelImpl.TryGetTopLevel(topLevelId) is { } topLevelImpl) + handler(topLevelImpl); return Task.CompletedTask; } - public static Task RedirectInputRetunAsync(int topLevelId, Func handler, T @default) + public static Task RedirectInputRetunAsync(int topLevelId, Func 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 ReadClipboardAsync(JSObject window); [JSImport("InputHelper.writeClipboard", AvaloniaModule.MainModuleName)] - public static partial Task WriteClipboardAsync(JSObject globalThis, JSObject? source); + public static partial Task WriteClipboardAsync(JSObject globalThis, JSObject? source); [JSImport("InputHelper.getReadableDataItemFormats", AvaloniaModule.MainModuleName)] public static partial string[] GetReadableDataItemFormats(JSObject item); diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts index e70c43ebed..3c2ef49048 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts +++ b/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 { + public static async readClipboard(window: Window): Promise { 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((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((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 { - const items = source?.items ?? []; - if (items.length === 0) { - await window.navigator.clipboard.writeText(""); - return; - } + public static async writeClipboard(window: Window, source?: WriteableClipboardSource | null): Promise { + 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 { @@ -505,7 +531,7 @@ export class InputHelper { }; } - return () => {}; + return () => { }; } public static clearInput(inputElement: HTMLInputElement) {