Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[macOS, sandbox] Implement optional native file selection dialog support for sandboxed apps. #47499

Merged
merged 1 commit into from
Jul 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions core/core_bind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,10 @@ bool OS::has_feature(const String &p_feature) const {
}
}

bool OS::is_sandboxed() const {
return ::OS::get_singleton()->is_sandboxed();
}

uint64_t OS::get_static_memory_usage() const {
return ::OS::get_singleton()->get_static_memory_usage();
}
Expand Down Expand Up @@ -545,6 +549,10 @@ Vector<String> OS::get_granted_permissions() const {
return ::OS::get_singleton()->get_granted_permissions();
}

void OS::revoke_granted_permissions() {
::OS::get_singleton()->revoke_granted_permissions();
}

String OS::get_unique_id() const {
return ::OS::get_singleton()->get_unique_id();
}
Expand Down Expand Up @@ -636,10 +644,12 @@ void OS::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_main_thread_id"), &OS::get_main_thread_id);

ClassDB::bind_method(D_METHOD("has_feature", "tag_name"), &OS::has_feature);
ClassDB::bind_method(D_METHOD("is_sandboxed"), &OS::is_sandboxed);

ClassDB::bind_method(D_METHOD("request_permission", "name"), &OS::request_permission);
ClassDB::bind_method(D_METHOD("request_permissions"), &OS::request_permissions);
ClassDB::bind_method(D_METHOD("get_granted_permissions"), &OS::get_granted_permissions);
ClassDB::bind_method(D_METHOD("revoke_granted_permissions"), &OS::revoke_granted_permissions);

ADD_PROPERTY(PropertyInfo(Variant::BOOL, "low_processor_usage_mode"), "set_low_processor_usage_mode", "is_in_low_processor_usage_mode");
ADD_PROPERTY(PropertyInfo(Variant::INT, "low_processor_usage_mode_sleep_usec"), "set_low_processor_usage_mode_sleep_usec", "get_low_processor_usage_mode_sleep_usec");
Expand Down
2 changes: 2 additions & 0 deletions core/core_bind.h
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,12 @@ class OS : public Object {
Thread::ID get_main_thread_id() const;

bool has_feature(const String &p_feature) const;
bool is_sandboxed() const;

bool request_permission(const String &p_name);
bool request_permissions();
Vector<String> get_granted_permissions() const;
void revoke_granted_permissions();

static OS *get_singleton() { return singleton; }

Expand Down
4 changes: 4 additions & 0 deletions core/os/os.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,10 @@ bool OS::has_feature(const String &p_feature) {
return false;
}

bool OS::is_sandboxed() const {
return false;
}

void OS::set_restart_on_exit(bool p_restart, const List<String> &p_restart_arguments) {
restart_on_exit = p_restart;
restart_commandline = p_restart_arguments;
Expand Down
3 changes: 3 additions & 0 deletions core/os/os.h
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ class OS {

bool has_feature(const String &p_feature);

virtual bool is_sandboxed() const;

void set_has_server_feature_callback(HasServerFeatureCallback p_callback);

void set_restart_on_exit(bool p_restart, const List<String> &p_restart_arguments);
Expand All @@ -304,6 +306,7 @@ class OS {
virtual bool request_permission(const String &p_name) { return true; }
virtual bool request_permissions() { return true; }
virtual Vector<String> get_granted_permissions() const { return Vector<String>(); }
virtual void revoke_granted_permissions() {}

// For recording / measuring benchmark data. Only enabled with tools
void set_use_benchmark(bool p_use_benchmark);
Expand Down
33 changes: 33 additions & 0 deletions doc/classes/DisplayServer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,24 @@
[b]Note:[/b] This method is implemented only on Windows.
</description>
</method>
<method name="file_dialog_show">
<return type="int" enum="Error" />
<param index="0" name="title" type="String" />
<param index="1" name="current_directory" type="String" />
<param index="2" name="filename" type="String" />
<param index="3" name="show_hidden" type="bool" />
<param index="4" name="mode" type="int" enum="DisplayServer.FileDialogMode" />
<param index="5" name="filters" type="PackedStringArray" />
<param index="6" name="callback" type="Callable" />
<description>
Displays OS native dialog for selecting files or directories in the file system.
Callbacks have the following arguments: [code]bool status, PackedStringArray selected_paths[/code].
[b]Note:[/b] This method is implemented if the display server has the [code]FEATURE_NATIVE_DIALOG[/code] feature.
[b]Note:[/b] This method is implemented on macOS.
[b]Note:[/b] On macOS, native file dialogs have no title.
[b]Note:[/b] On macOS, sandboxed apps will save security-scoped bookmarks to retain access to the opened folders across multiple sessions. Use [method OS.get_granted_permissions] to get a list of saved bookmarks.
</description>
</method>
<method name="force_process_and_drop_events">
<return type="void" />
<description>
Expand Down Expand Up @@ -1729,6 +1747,21 @@
<constant name="CURSOR_MAX" value="17" enum="CursorShape">
Represents the size of the [enum CursorShape] enum.
</constant>
<constant name="FILE_DIALOG_MODE_OPEN_FILE" value="0" enum="FileDialogMode">
The native file dialog allows selecting one, and only one file.
</constant>
<constant name="FILE_DIALOG_MODE_OPEN_FILES" value="1" enum="FileDialogMode">
The native file dialog allows selecting multiple files.
</constant>
<constant name="FILE_DIALOG_MODE_OPEN_DIR" value="2" enum="FileDialogMode">
The native file dialog only allows selecting a directory, disallowing the selection of any file.
</constant>
<constant name="FILE_DIALOG_MODE_OPEN_ANY" value="3" enum="FileDialogMode">
The native file dialog allows selecting one file or directory.
</constant>
<constant name="FILE_DIALOG_MODE_SAVE_FILE" value="4" enum="FileDialogMode">
The native file dialog will warn when a file exists.
</constant>
<constant name="WINDOW_MODE_WINDOWED" value="0" enum="WindowMode">
Windowed mode, i.e. [Window] doesn't occupy the whole screen (unless set to the size of the screen).
</constant>
Expand Down
4 changes: 4 additions & 0 deletions doc/classes/FileDialog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@
If [code]true[/code], the dialog will show hidden files.
</member>
<member name="title" type="String" setter="set_title" getter="get_title" overrides="Window" default="&quot;Save a File&quot;" />
<member name="use_native_dialog" type="bool" setter="set_use_native_dialog" getter="get_use_native_dialog" default="false">
If [code]true[/code], [member access] is set to [constant ACCESS_FILESYSTEM], and it is supported by the current [DisplayServer], OS native dialog will be used instead of custom one.
[b]Note:[/b] On macOS, sandboxed apps always use native dialogs to access host filesystem.
</member>
</members>
<signals>
<signal name="dir_selected">
Expand Down
17 changes: 15 additions & 2 deletions doc/classes/OS.xml
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@
<method name="get_granted_permissions" qualifiers="const">
<return type="PackedStringArray" />
<description>
With this function, you can get the list of dangerous permissions that have been granted to the Android application.
[b]Note:[/b] This method is implemented only on Android.
On Android devices: With this function, you can get the list of dangerous permissions that have been granted.
On macOS (sandboxed applications only): This function returns the list of user selected folders accessible to the application. Use native file dialog to request folder access permission.
</description>
</method>
<method name="get_keycode_string" qualifiers="const">
Expand Down Expand Up @@ -534,6 +534,13 @@
Returns [code]true[/code] if the project will automatically restart when it exits for any reason, [code]false[/code] otherwise. See also [method set_restart_on_exit] and [method get_restart_on_exit_arguments].
</description>
</method>
<method name="is_sandboxed" qualifiers="const">
<return type="bool" />
<description>
Returns [code]true[/code] if application is running in the sandbox.
[b]Note:[/b] This method is implemented on macOS.
</description>
</method>
<method name="is_stdout_verbose" qualifiers="const">
<return type="bool" />
<description>
Expand Down Expand Up @@ -602,6 +609,12 @@
[b]Note:[/b] This method is implemented only on Android.
</description>
</method>
<method name="revoke_granted_permissions">
<return type="void" />
<description>
On macOS (sandboxed applications only), this function clears list of user selected folders accessible to the application.
</description>
</method>
<method name="set_environment" qualifiers="const">
<return type="void" />
<param index="0" name="variable" type="String" />
Expand Down
2 changes: 2 additions & 0 deletions platform/macos/display_server_macos.h
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ class DisplayServerMacOS : public DisplayServer {
virtual Error dialog_show(String p_title, String p_description, Vector<String> p_buttons, const Callable &p_callback) override;
virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) override;

virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) override;

virtual void mouse_set_mode(MouseMode p_mode) override;
virtual MouseMode mouse_get_mode() const override;

Expand Down
170 changes: 170 additions & 0 deletions platform/macos/display_server_macos.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1847,6 +1847,176 @@
return OK;
}

Error DisplayServerMacOS::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) {
_THREAD_SAFE_METHOD_

NSString *url = [NSString stringWithUTF8String:p_current_directory.utf8().get_data()];
NSMutableArray *allowed_types = [[NSMutableArray alloc] init];
bool allow_other = false;
for (int i = 0; i < p_filters.size(); i++) {
Vector<String> tokens = p_filters[i].split(";");
if (tokens.size() > 0) {
if (tokens[0].strip_edges() == "*.*") {
allow_other = true;
} else {
[allowed_types addObject:[NSString stringWithUTF8String:tokens[0].replace("*.", "").strip_edges().utf8().get_data()]];
}
}
}

Callable callback = p_callback; // Make a copy for async completion handler.
switch (p_mode) {
case FILE_DIALOG_MODE_SAVE_FILE: {
NSSavePanel *panel = [NSSavePanel savePanel];

[panel setDirectoryURL:[NSURL fileURLWithPath:url]];
if ([allowed_types count]) {
[panel setAllowedFileTypes:allowed_types];
}
[panel setAllowsOtherFileTypes:allow_other];
[panel setExtensionHidden:YES];
[panel setCanSelectHiddenExtension:YES];
[panel setCanCreateDirectories:YES];
[panel setShowsHiddenFiles:p_show_hidden];
if (p_filename != "") {
NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()];
[panel setNameFieldStringValue:fileurl];
}

[panel beginSheetModalForWindow:[[NSApplication sharedApplication] mainWindow]
completionHandler:^(NSInteger ret) {
if (ret == NSModalResponseOK) {
// Save bookmark for folder.
if (OS::get_singleton()->is_sandboxed()) {
NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"];
bool skip = false;
for (id bookmark in bookmarks) {
NSError *error = nil;
BOOL isStale = NO;
NSURL *exurl = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error];
if (!error && !isStale && ([[exurl path] compare:[[panel directoryURL] path]] == NSOrderedSame)) {
skip = true;
break;
}
}
if (!skip) {
NSError *error = nil;
NSData *bookmark = [[panel directoryURL] bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error];
if (!error) {
NSArray *new_bookmarks = [bookmarks arrayByAddingObject:bookmark];
[[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"];
}
}
}
// Callback.
Vector<String> files;
String url;
url.parse_utf8([[[panel URL] path] UTF8String]);
files.push_back(url);
if (!callback.is_null()) {
Variant v_status = true;
Variant v_files = files;
Variant *v_args[2] = { &v_status, &v_files };
Variant ret;
Callable::CallError ce;
callback.callp((const Variant **)&v_args, 2, ret, ce);
}
} else {
if (!callback.is_null()) {
Variant v_status = false;
Variant v_files = Vector<String>();
Variant *v_args[2] = { &v_status, &v_files };
Variant ret;
Callable::CallError ce;
callback.callp((const Variant **)&v_args, 2, ret, ce);
}
}
}];
} break;
case FILE_DIALOG_MODE_OPEN_ANY:
case FILE_DIALOG_MODE_OPEN_FILE:
case FILE_DIALOG_MODE_OPEN_FILES:
case FILE_DIALOG_MODE_OPEN_DIR: {
NSOpenPanel *panel = [NSOpenPanel openPanel];

[panel setDirectoryURL:[NSURL fileURLWithPath:url]];
if ([allowed_types count]) {
[panel setAllowedFileTypes:allowed_types];
}
[panel setAllowsOtherFileTypes:allow_other];
[panel setExtensionHidden:YES];
[panel setCanSelectHiddenExtension:YES];
[panel setCanCreateDirectories:YES];
[panel setCanChooseFiles:(p_mode != FILE_DIALOG_MODE_OPEN_DIR)];
[panel setCanChooseDirectories:(p_mode == FILE_DIALOG_MODE_OPEN_DIR || p_mode == FILE_DIALOG_MODE_OPEN_ANY)];
[panel setShowsHiddenFiles:p_show_hidden];
if (p_filename != "") {
NSString *fileurl = [NSString stringWithUTF8String:p_filename.utf8().get_data()];
[panel setNameFieldStringValue:fileurl];
}
[panel setAllowsMultipleSelection:(p_mode == FILE_DIALOG_MODE_OPEN_FILES)];

[panel beginSheetModalForWindow:[[NSApplication sharedApplication] mainWindow]
completionHandler:^(NSInteger ret) {
if (ret == NSModalResponseOK) {
// Save bookmark for folder.
NSArray *urls = [(NSOpenPanel *)panel URLs];
if (OS::get_singleton()->is_sandboxed()) {
NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"];
NSMutableArray *new_bookmarks = [bookmarks mutableCopy];
for (NSUInteger i = 0; i != [urls count]; ++i) {
bool skip = false;
for (id bookmark in bookmarks) {
NSError *error = nil;
BOOL isStale = NO;
NSURL *exurl = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error];
if (!error && !isStale && ([[exurl path] compare:[[urls objectAtIndex:i] path]] == NSOrderedSame)) {
skip = true;
break;
}
}
if (!skip) {
NSError *error = nil;
NSData *bookmark = [[urls objectAtIndex:i] bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error];
if (!error) {
[new_bookmarks addObject:bookmark];
}
}
}
[[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"];
}
// Callback.
Vector<String> files;
for (NSUInteger i = 0; i != [urls count]; ++i) {
String url;
url.parse_utf8([[[urls objectAtIndex:i] path] UTF8String]);
files.push_back(url);
}
if (!callback.is_null()) {
Variant v_status = true;
Variant v_files = files;
Variant *v_args[2] = { &v_status, &v_files };
Variant ret;
Callable::CallError ce;
callback.callp((const Variant **)&v_args, 2, ret, ce);
}
} else {
if (!callback.is_null()) {
Variant v_status = false;
Variant v_files = Vector<String>();
Variant *v_args[2] = { &v_status, &v_files };
Variant ret;
Callable::CallError ce;
callback.callp((const Variant **)&v_args, 2, ret, ce);
}
}
}];
} break;
}

return OK;
}

Error DisplayServerMacOS::dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) {
_THREAD_SAFE_METHOD_

Expand Down
3 changes: 3 additions & 0 deletions platform/macos/doc_classes/EditorExportPlatformMacOS.xml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@
<member name="codesign/entitlements/app_sandbox/files_pictures" type="int" setter="" getter="">
Allows read or write access to the user's "Pictures" folder. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_assets_pictures_read-write]com.apple.security.files.pictures.read-write[/url].
</member>
<member name="codesign/entitlements/app_sandbox/files_user_selected" type="int" setter="" getter="">
Allows read or write access to the locations the user has selected using a native file dialog. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_files_user-selected_read-write]com.apple.security.files.user-selected.read-write[/url].
</member>
<member name="codesign/entitlements/app_sandbox/helper_executables" type="Array" setter="" getter="">
List of helper executables to embedded to the app bundle. Sandboxed app are limited to execute only these executable. See [url=https://developer.apple.com/documentation/xcode/embedding-a-helper-tool-in-a-sandboxed-app]Embedding a command-line tool in a sandboxed app[/url].
</member>
Expand Down
9 changes: 9 additions & 0 deletions platform/macos/export/export_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_pictures", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_music", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_movies", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_user_selected", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0));
r_options->push_back(ExportOption(PropertyInfo(Variant::ARRAY, "codesign/entitlements/app_sandbox/helper_executables", PROPERTY_HINT_ARRAY_TYPE, itos(Variant::STRING) + "/" + itos(PROPERTY_HINT_GLOBAL_FILE) + ":"), Array()));
r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray()));

Expand Down Expand Up @@ -1922,6 +1923,14 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p
ent_f->store_line("<key>com.apple.security.files.movies.read-write</key>");
ent_f->store_line("<true/>");
}
if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_user_selected") == 1) {
ent_f->store_line("<key>com.apple.security.files.user-selected.read-only</key>");
ent_f->store_line("<true/>");
}
if ((int)p_preset->get("codesign/entitlements/app_sandbox/files_user_selected") == 2) {
ent_f->store_line("<key>com.apple.security.files.user-selected.read-write</key>");
ent_f->store_line("<true/>");
}
}

ent_f->store_line("</dict>");
Expand Down
Loading