From 32c2f082000d58e5fa7b75672ebb8da3611c666c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 18 Jul 2024 18:03:52 -0700 Subject: [PATCH] MacOS sandboxing feature (#16090) * Set isDirectory:true explicitly to help [NSURL fileURLWithPath] method Might solve some rare/random issues with initial directory not being applied * Fix dialogs page incorrectly setting parent folder * Move SecurityScopedStream out of iOS project and share it with macOS project * Refactor BclStorageItem to be more reusable across platforms * [Breaking] Set BclStorageItem.CanBookmark to false, as it never was supposed to be true. Plain BCL doesn't provide files bookmarking. * Reimplement storage provider support on macOS, support (optional) sandboxing * Fix build * Fix AppSandboxEnabled=false usage * Re-enable BCL bookmarks, keep them base64 * Fix nullable error * Prefix all bookmarks with a platform key * Fix devtools breaking sandboxed app * Try to read errors after saving bookmark * Don't crash sample app if has no access * Add internal IStorageItemWithFileSystemInfo abstraction * Log information if OpenSecurityScope returned false * Fix build * Prefix bookmarks with "ava.v1." * Support opening old-style bookmarks to avoid breaking changes --- .../project.pbxproj | 8 +- .../{SystemDialogs.mm => StorageProvider.mm} | 135 +++++--- native/Avalonia.Native/src/OSX/common.h | 2 +- native/Avalonia.Native/src/OSX/main.mm | 4 +- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 50 ++- .../Platform/Storage/AndroidStorageItem.cs | 4 +- .../Storage/AndroidStorageProvider.cs | 22 +- .../Platform/Storage/FileIO/BclStorageFile.cs | 113 +------ .../Storage/FileIO/BclStorageFolder.cs | 128 +------- .../Platform/Storage/FileIO/BclStorageItem.cs | 141 +++++++++ .../Storage/FileIO/BclStorageProvider.cs | 56 +++- .../Storage/FileIO/SecurityScopedStream.cs | 116 +++++++ .../Storage/FileIO/StorageBookmarkHelper.cs | 153 ++++++++++ .../Storage/FileIO/StorageProviderHelpers.cs | 30 +- .../Platform/Storage/IStorageBookmarkItem.cs | 8 +- .../Storage/StorageProviderExtensions.cs | 20 +- .../Diagnostics/Conventions.cs | 16 +- .../Screenshots/FilePickerHandler.cs | 15 +- .../AvaloniaNativeApplicationPlatform.cs | 12 +- src/Avalonia.Native/AvaloniaNativePlatform.cs | 6 +- .../AvaloniaNativePlatformExtensions.cs | 7 + src/Avalonia.Native/ClipboardImpl.cs | 11 +- src/Avalonia.Native/StorageItem.cs | 152 +++++++++ src/Avalonia.Native/StorageProviderApi.cs | 289 ++++++++++++++++++ src/Avalonia.Native/StorageProviderImpl.cs | 63 ++++ src/Avalonia.Native/SystemDialogs.cs | 181 ----------- src/Avalonia.Native/TopLevelImpl.cs | 7 - src/Avalonia.Native/avn.idl | 17 +- .../Storage/BrowserStorageProvider.cs | 38 ++- .../Storage/IOSSecurityScopedStream.cs | 68 ----- .../Avalonia.iOS/Storage/IOSStorageItem.cs | 29 +- .../Storage/IOSStorageProvider.cs | 42 ++- .../Utilities/UriExtensionsTests.cs | 2 +- .../Platform/StorageProviderHelperTests.cs | 74 +++++ 34 files changed, 1370 insertions(+), 649 deletions(-) rename native/Avalonia.Native/src/OSX/{SystemDialogs.mm => StorageProvider.mm} (85%) create mode 100644 src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs create mode 100644 src/Avalonia.Native/StorageItem.cs create mode 100644 src/Avalonia.Native/StorageProviderApi.cs create mode 100644 src/Avalonia.Native/StorageProviderImpl.cs delete mode 100644 src/Avalonia.Native/SystemDialogs.cs delete mode 100644 src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs create mode 100644 tests/Avalonia.Controls.UnitTests/Platform/StorageProviderHelperTests.cs diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 734f126a1b..9a67ee0161 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -34,7 +34,7 @@ 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AFD334023E03C4F0042899B /* controlhost.mm */; }; 37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; }; 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; - 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; + 37C09D8821580FE4006A6758 /* StorageProvider.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* StorageProvider.mm */; }; 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; }; 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; }; 520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; }; @@ -95,7 +95,7 @@ 379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; }; 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; 37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = ""; }; - 37C09D8721580FE4006A6758 /* SystemDialogs.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SystemDialogs.mm; sourceTree = ""; }; + 37C09D8721580FE4006A6758 /* StorageProvider.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = StorageProvider.mm; sourceTree = ""; }; 37DDA9AF219330F8002E132B /* AvnString.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnString.mm; sourceTree = ""; }; 37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = ""; }; 37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; }; @@ -202,7 +202,7 @@ 523484CB26EA68AA00EA0C2C /* trayicon.h */, 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */, 37A517B22159597E00FBA241 /* Screens.mm */, - 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, + 37C09D8721580FE4006A6758 /* StorageProvider.mm */, EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */, AB7A61F02147C815003C5833 /* Products */, AB661C1C2148230E00291242 /* Frameworks */, @@ -339,7 +339,7 @@ 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */, 1A465D10246AB61600C5858B /* dnd.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */, - 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, + 37C09D8821580FE4006A6758 /* StorageProvider.mm in Sources */, 1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */, F10084862BFF1FB40024303E /* TopLevelImpl.mm in Sources */, 1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/SystemDialogs.mm b/native/Avalonia.Native/src/OSX/StorageProvider.mm similarity index 85% rename from native/Avalonia.Native/src/OSX/SystemDialogs.mm rename to native/Avalonia.Native/src/OSX/StorageProvider.mm index 97c8108edc..0fd77c6789 100644 --- a/native/Avalonia.Native/src/OSX/SystemDialogs.mm +++ b/native/Avalonia.Native/src/OSX/StorageProvider.mm @@ -64,12 +64,91 @@ const int kFileTypePopupTag = 10975; @end -class SystemDialogs : public ComSingleObject +class StorageProvider : public ComSingleObject { ExtensionDropdownHandler* __strong _extension_dropdown_handler; public: FORWARD_IUNKNOWN() + + virtual HRESULT SaveBookmarkToBytes ( + IAvnString* fileUriStr, + void** err, + IAvnString** ppv + ) override + { + @autoreleasepool + { + if(ppv == nullptr) + return E_POINTER; + + NSError* error; + auto fileUri = [NSURL URLWithString: GetNSStringAndRelease(fileUriStr)]; + auto bookmarkData = [fileUri bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; + if (bookmarkData) + { + *ppv = CreateByteArray((void*)bookmarkData.bytes, (int)bookmarkData.length); + } + if (error != nil) + { + *err = CreateAvnString([error localizedDescription]); + } + return S_OK; + } + } + + virtual HRESULT ReadBookmarkFromBytes ( + void* ptr, + int len, + IAvnString** ppv + ) override { + @autoreleasepool + { + if(ppv == nullptr) + return E_POINTER; + + auto bookmarkData = [[NSData alloc] initWithBytes:ptr length:len]; + auto fileUri = [NSURL URLByResolvingBookmarkData: bookmarkData + options:NSURLBookmarkResolutionWithSecurityScope|NSURLBookmarkResolutionWithoutUI + relativeToURL:nil + bookmarkDataIsStale:nil + error:nil]; + + if (fileUri) + { + *ppv = CreateAvnString([fileUri absoluteString]); + } + return S_OK; + } + } + + virtual void ReleaseBookmark ( + IAvnString* fileUriStr + ) override { + // no-op + } + + virtual bool OpenSecurityScope ( + IAvnString* fileUriStr + ) override { + @autoreleasepool + { + auto fileUri = [NSURL URLWithString: GetNSStringAndRelease(fileUriStr)]; + auto success = [fileUri startAccessingSecurityScopedResource]; + return success; + } + } + + virtual void CloseSecurityScope ( + IAvnString* fileUriStr + ) override { + @autoreleasepool + { + auto fileUri = [NSURL URLWithString: GetNSStringAndRelease(fileUriStr)]; + [fileUri stopAccessingSecurityScopedResource]; + } + } + virtual void SelectFolderDialog (IAvnWindow* parentWindowHandle, IAvnSystemDialogEvents* events, bool allowMultiple, @@ -105,19 +184,9 @@ public: if(urls.count > 0) { - void* strings[urls.count]; - - for(int i = 0; i < urls.count; i++) - { - auto url = [urls objectAtIndex:i]; - - auto string = [url path]; - - strings[i] = (void*)[string UTF8String]; - } - - events->OnCompleted((int)urls.count, &strings[0]); - + auto uriStrings = CreateAvnStringArray(urls); + events->OnCompleted(uriStrings); + [panel orderOut:panel]; if(parentWindowHandle != nullptr) @@ -130,7 +199,7 @@ public: } } - events->OnCompleted(0, nullptr); + events->OnCompleted(nullptr); }; @@ -188,19 +257,9 @@ public: if(urls.count > 0) { - void* strings[urls.count]; - - for(int i = 0; i < urls.count; i++) - { - auto url = [urls objectAtIndex:i]; - - auto string = [url path]; - - strings[i] = (void*)[string UTF8String]; - } - - events->OnCompleted((int)urls.count, &strings[0]); - + auto uriStrings = CreateAvnStringArray(urls); + events->OnCompleted(uriStrings); + [panel orderOut:panel]; if(parentWindowHandle != nullptr) @@ -213,7 +272,7 @@ public: } } - events->OnCompleted(0, nullptr); + events->OnCompleted(nullptr); }; @@ -264,15 +323,11 @@ public: auto handler = ^(NSModalResponse result) { if(result == NSFileHandlingPanelOKButton) { - void* strings[1]; - auto url = [panel URL]; - - auto string = [url path]; - strings[0] = (void*)[string UTF8String]; - - events->OnCompleted(1, &strings[0]); - + auto urls = [NSArray arrayWithObject:url]; + auto uriStrings = CreateAvnStringArray(urls); + events->OnCompleted(uriStrings); + [panel orderOut:panel]; if(parentWindowHandle != nullptr) @@ -284,7 +339,7 @@ public: return; } - events->OnCompleted(0, nullptr); + events->OnCompleted(nullptr); }; @@ -519,7 +574,7 @@ private: }; }; -extern IAvnSystemDialogs* CreateSystemDialogs() +extern IAvnStorageProvider* CreateStorageProvider() { - return new SystemDialogs(); + return new StorageProvider(); } diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index fab11d6e4f..36c157704d 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -14,7 +14,7 @@ extern void PostDispatcherCallback(IAvnActionCallback* cb); extern IAvnTopLevel* CreateAvnTopLevel(IAvnTopLevelEvents* events); extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events); extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events); -extern IAvnSystemDialogs* CreateSystemDialogs(); +extern IAvnStorageProvider* CreateStorageProvider(); extern IAvnScreens* CreateScreens(IAvnScreenEvents* cb); extern IAvnClipboard* CreateClipboard(NSPasteboard*, NSPasteboardItem*); extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 47a5ba73c5..d1dbe9d186 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -276,13 +276,13 @@ public: } } - virtual HRESULT CreateSystemDialogs(IAvnSystemDialogs** ppv) override + virtual HRESULT CreateStorageProvider(IAvnStorageProvider** ppv) override { START_COM_CALL; @autoreleasepool { - *ppv = ::CreateSystemDialogs(); + *ppv = ::CreateStorageProvider(); return S_OK; } } diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index fc66f533c6..5b7814fb5d 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -254,17 +254,24 @@ namespace ControlCatalog.Pages if (file is not null) { - // Sync disposal of StreamWriter is not supported on WASM + try + { + // Sync disposal of StreamWriter is not supported on WASM #if NET6_0_OR_GREATER - await using var stream = await file.OpenWriteAsync(); - await using var writer = new System.IO.StreamWriter(stream); + await using var stream = await file.OpenWriteAsync(); + await using var writer = new System.IO.StreamWriter(stream); #else - using var stream = await file.OpenWriteAsync(); - using var writer = new System.IO.StreamWriter(stream); + using var stream = await file.OpenWriteAsync(); + using var writer = new System.IO.StreamWriter(stream); #endif - await writer.WriteLineAsync(openedFileContent.Text); + await writer.WriteLineAsync(openedFileContent.Text); - SetFolder(await file.GetParentAsync()); + SetFolder(await file.GetParentAsync()); + } + catch (Exception ex) + { + openedFileContent.Text = ex.ToString(); + } } await SetPickerResult(file is null ? null : new[] { file }); @@ -280,8 +287,6 @@ namespace ControlCatalog.Pages }); await SetPickerResult(folders); - - SetFolder(folders.FirstOrDefault()); }; this.Get