Implement App-Scoped Security scoped bookmarks (#11711)
* implementation of security scoped bookmarks * option is now only available on mas builds
This commit is contained in:
parent
9f78ef0179
commit
d1d50a4c92
11 changed files with 226 additions and 42 deletions
|
@ -1262,6 +1262,10 @@ void App::BuildPrototype(
|
|||
.SetMethod("moveToApplicationsFolder", &App::MoveToApplicationsFolder)
|
||||
.SetMethod("isInApplicationsFolder", &App::IsInApplicationsFolder)
|
||||
#endif
|
||||
#if defined(MAS_BUILD)
|
||||
.SetMethod("startAccessingSecurityScopedResource",
|
||||
&App::StartAccessingSecurityScopedResource)
|
||||
#endif
|
||||
.SetMethod("getAppMemoryInfo", &App::GetAppMetrics);
|
||||
}
|
||||
|
||||
|
|
|
@ -209,6 +209,10 @@ class App : public AtomBrowserClient::Delegate,
|
|||
bool MoveToApplicationsFolder(mate::Arguments* args);
|
||||
bool IsInApplicationsFolder();
|
||||
#endif
|
||||
#if defined(MAS_BUILD)
|
||||
base::Callback<void()> StartAccessingSecurityScopedResource(
|
||||
mate::Arguments* args);
|
||||
#endif
|
||||
|
||||
#if defined(OS_WIN)
|
||||
// Get the current Jump List settings.
|
||||
|
|
59
atom/browser/api/atom_api_app_mas.mm
Normal file
59
atom/browser/api/atom_api_app_mas.mm
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) 2013 GitHub, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "atom/browser/api/atom_api_app.h"
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#include "base/strings/sys_string_conversions.h"
|
||||
|
||||
namespace atom {
|
||||
|
||||
namespace api {
|
||||
|
||||
// Callback passed to js which will stop accessing the given bookmark.
|
||||
void OnStopAccessingSecurityScopedResource(NSURL* bookmarkUrl) {
|
||||
[bookmarkUrl stopAccessingSecurityScopedResource];
|
||||
[bookmarkUrl release];
|
||||
}
|
||||
|
||||
// Get base64 encoded NSData, create a bookmark for it and start accessing it.
|
||||
base::Callback<void ()> App::StartAccessingSecurityScopedResource(mate::Arguments* args) {
|
||||
std::string data;
|
||||
args->GetNext(&data);
|
||||
NSString *base64str = base::SysUTF8ToNSString(data);
|
||||
NSData *bookmarkData = [[NSData alloc] initWithBase64EncodedString: base64str options: 0];
|
||||
|
||||
// Create bookmarkUrl from NSData.
|
||||
BOOL isStale = false;
|
||||
NSError *error = nil;
|
||||
NSURL *bookmarkUrl = [NSURL URLByResolvingBookmarkData: bookmarkData
|
||||
options: NSURLBookmarkResolutionWithSecurityScope
|
||||
relativeToURL: nil
|
||||
bookmarkDataIsStale: &isStale
|
||||
error: &error];
|
||||
|
||||
if (error != nil) {
|
||||
NSString *err = [NSString stringWithFormat: @"NSError: %@ %@", error, [error userInfo]];
|
||||
args->ThrowError(base::SysNSStringToUTF8(err));
|
||||
}
|
||||
|
||||
if (isStale) {
|
||||
args->ThrowError("bookmarkDataIsStale - try recreating the bookmark");
|
||||
}
|
||||
|
||||
if (error == nil && isStale == false) {
|
||||
[bookmarkUrl startAccessingSecurityScopedResource];
|
||||
}
|
||||
|
||||
// Stop the NSURL from being GC'd.
|
||||
[bookmarkUrl retain];
|
||||
|
||||
// Return a js callback which will close the bookmark.
|
||||
return base::Bind(&OnStopAccessingSecurityScopedResource, bookmarkUrl);
|
||||
}
|
||||
|
||||
} // namespace atom
|
||||
|
||||
} // namespace api
|
|
@ -54,6 +54,9 @@ struct Converter<file_dialog::DialogSettings> {
|
|||
dict.Get("filters", &(out->filters));
|
||||
dict.Get("properties", &(out->properties));
|
||||
dict.Get("showsTagField", &(out->shows_tag_field));
|
||||
#if defined(MAS_BUILD)
|
||||
dict.Get("securityScopedBookmarks", &(out->security_scoped_bookmarks));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -33,11 +33,25 @@ enum FileDialogProperty {
|
|||
FILE_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY = 1 << 7,
|
||||
};
|
||||
|
||||
typedef base::Callback<void(
|
||||
bool result, const std::vector<base::FilePath>& paths)> OpenDialogCallback;
|
||||
#if defined(MAS_BUILD)
|
||||
typedef base::Callback<void(
|
||||
bool result,
|
||||
const std::vector<base::FilePath>& paths,
|
||||
const std::vector<std::string>& bookmarkData)> OpenDialogCallback;
|
||||
|
||||
typedef base::Callback<void(
|
||||
bool result, const base::FilePath& path)> SaveDialogCallback;
|
||||
typedef base::Callback<void(
|
||||
bool result,
|
||||
const base::FilePath& path,
|
||||
const std::string& bookmarkData)> SaveDialogCallback;
|
||||
#else
|
||||
typedef base::Callback<void(
|
||||
bool result,
|
||||
const std::vector<base::FilePath>& paths)> OpenDialogCallback;
|
||||
|
||||
typedef base::Callback<void(
|
||||
bool result,
|
||||
const base::FilePath& path)> SaveDialogCallback;
|
||||
#endif
|
||||
|
||||
struct DialogSettings {
|
||||
atom::NativeWindow* parent_window = nullptr;
|
||||
|
@ -50,6 +64,7 @@ struct DialogSettings {
|
|||
int properties = 0;
|
||||
bool shows_tag_field = true;
|
||||
bool force_detached = false;
|
||||
bool security_scoped_bookmarks = false;
|
||||
};
|
||||
|
||||
bool ShowOpenDialog(const DialogSettings& settings,
|
||||
|
|
|
@ -185,11 +185,44 @@ int RunModalDialog(NSSavePanel* dialog, const DialogSettings& settings) {
|
|||
return chosen;
|
||||
}
|
||||
|
||||
void ReadDialogPaths(NSOpenPanel* dialog, std::vector<base::FilePath>* paths) {
|
||||
// Create bookmark data and serialise it into a base64 string.
|
||||
std::string GetBookmarkDataFromNSURL(NSURL* url) {
|
||||
// Create the file if it doesn't exist (necessary for NSSavePanel options).
|
||||
NSFileManager *defaultManager = [NSFileManager defaultManager];
|
||||
if (![defaultManager fileExistsAtPath: [url path]]) {
|
||||
[defaultManager createFileAtPath: [url path] contents: nil attributes: nil];
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
NSData *bookmarkData = [url bookmarkDataWithOptions: NSURLBookmarkCreationWithSecurityScope
|
||||
includingResourceValuesForKeys: nil
|
||||
relativeToURL: nil
|
||||
error: &error];
|
||||
if (error != nil) {
|
||||
// Send back an empty string if there was an error.
|
||||
return "";
|
||||
} else {
|
||||
// Encode NSData in base64 then convert to NSString.
|
||||
NSString *base64data = [[NSString alloc] initWithData: [bookmarkData base64EncodedDataWithOptions: 0]
|
||||
encoding: NSUTF8StringEncoding];
|
||||
return base::SysNSStringToUTF8(base64data);
|
||||
}
|
||||
}
|
||||
|
||||
void ReadDialogPathsWithBookmarks(NSOpenPanel* dialog,
|
||||
std::vector<base::FilePath>* paths,
|
||||
std::vector<std::string>* bookmarks) {
|
||||
NSArray* urls = [dialog URLs];
|
||||
for (NSURL* url in urls)
|
||||
if ([url isFileURL])
|
||||
if ([url isFileURL]) {
|
||||
paths->push_back(base::FilePath(base::SysNSStringToUTF8([url path])));
|
||||
bookmarks->push_back(GetBookmarkDataFromNSURL(url));
|
||||
}
|
||||
}
|
||||
|
||||
void ReadDialogPaths(NSOpenPanel* dialog, std::vector<base::FilePath>* paths) {
|
||||
std::vector<std::string> ignored_bookmarks;
|
||||
ReadDialogPathsWithBookmarks(dialog, paths, &ignored_bookmarks);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
@ -210,6 +243,33 @@ bool ShowOpenDialog(const DialogSettings& settings,
|
|||
return true;
|
||||
}
|
||||
|
||||
void OpenDialogCompletion(int chosen, NSOpenPanel* dialog,
|
||||
const DialogSettings& settings,
|
||||
const OpenDialogCallback& callback) {
|
||||
if (chosen == NSFileHandlingPanelCancelButton) {
|
||||
#if defined(MAS_BUILD)
|
||||
callback.Run(false, std::vector<base::FilePath>(),
|
||||
std::vector<std::string>());
|
||||
#else
|
||||
callback.Run(false, std::vector<base::FilePath>());
|
||||
#endif
|
||||
} else {
|
||||
std::vector<base::FilePath> paths;
|
||||
#if defined(MAS_BUILD)
|
||||
std::vector<std::string> bookmarks;
|
||||
if (settings.security_scoped_bookmarks) {
|
||||
ReadDialogPathsWithBookmarks(dialog, &paths, &bookmarks);
|
||||
} else {
|
||||
ReadDialogPaths(dialog, &paths);
|
||||
}
|
||||
callback.Run(true, paths, bookmarks);
|
||||
#else
|
||||
ReadDialogPaths(dialog, &paths);
|
||||
callback.Run(true, paths);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void ShowOpenDialog(const DialogSettings& settings,
|
||||
const OpenDialogCallback& c) {
|
||||
NSOpenPanel* dialog = [NSOpenPanel openPanel];
|
||||
|
@ -224,24 +284,12 @@ void ShowOpenDialog(const DialogSettings& settings,
|
|||
if (!settings.parent_window || !settings.parent_window->GetNativeWindow() ||
|
||||
settings.force_detached) {
|
||||
int chosen = [dialog runModal];
|
||||
if (chosen == NSFileHandlingPanelCancelButton) {
|
||||
callback.Run(false, std::vector<base::FilePath>());
|
||||
} else {
|
||||
std::vector<base::FilePath> paths;
|
||||
ReadDialogPaths(dialog, &paths);
|
||||
callback.Run(true, paths);
|
||||
}
|
||||
OpenDialogCompletion(chosen, dialog, settings, callback);
|
||||
} else {
|
||||
NSWindow* window = settings.parent_window->GetNativeWindow();
|
||||
[dialog beginSheetModalForWindow:window
|
||||
completionHandler:^(NSInteger chosen) {
|
||||
if (chosen == NSFileHandlingPanelCancelButton) {
|
||||
callback.Run(false, std::vector<base::FilePath>());
|
||||
} else {
|
||||
std::vector<base::FilePath> paths;
|
||||
ReadDialogPaths(dialog, &paths);
|
||||
callback.Run(true, paths);
|
||||
}
|
||||
OpenDialogCompletion(chosen, dialog, settings, callback);
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
@ -261,6 +309,29 @@ bool ShowSaveDialog(const DialogSettings& settings,
|
|||
return true;
|
||||
}
|
||||
|
||||
void SaveDialogCompletion(int chosen, NSSavePanel* dialog,
|
||||
const DialogSettings& settings,
|
||||
const SaveDialogCallback& callback) {
|
||||
if (chosen == NSFileHandlingPanelCancelButton) {
|
||||
#if defined(MAS_BUILD)
|
||||
callback.Run(false, base::FilePath(), "");
|
||||
#else
|
||||
callback.Run(false, base::FilePath());
|
||||
#endif
|
||||
} else {
|
||||
std::string path = base::SysNSStringToUTF8([[dialog URL] path]);
|
||||
#if defined(MAS_BUILD)
|
||||
std::string bookmark;
|
||||
if (settings.security_scoped_bookmarks) {
|
||||
bookmark = GetBookmarkDataFromNSURL([dialog URL]);
|
||||
}
|
||||
callback.Run(true, base::FilePath(path), bookmark);
|
||||
#else
|
||||
callback.Run(true, base::FilePath(path));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void ShowSaveDialog(const DialogSettings& settings,
|
||||
const SaveDialogCallback& c) {
|
||||
NSSavePanel* dialog = [NSSavePanel savePanel];
|
||||
|
@ -273,22 +344,12 @@ void ShowSaveDialog(const DialogSettings& settings,
|
|||
if (!settings.parent_window || !settings.parent_window->GetNativeWindow() ||
|
||||
settings.force_detached) {
|
||||
int chosen = [dialog runModal];
|
||||
if (chosen == NSFileHandlingPanelCancelButton) {
|
||||
callback.Run(false, base::FilePath());
|
||||
} else {
|
||||
std::string path = base::SysNSStringToUTF8([[dialog URL] path]);
|
||||
callback.Run(true, base::FilePath(path));
|
||||
}
|
||||
SaveDialogCompletion(chosen, dialog, settings, callback);
|
||||
} else {
|
||||
NSWindow* window = settings.parent_window->GetNativeWindow();
|
||||
[dialog beginSheetModalForWindow:window
|
||||
completionHandler:^(NSInteger chosen) {
|
||||
if (chosen == NSFileHandlingPanelCancelButton) {
|
||||
callback.Run(false, base::FilePath());
|
||||
} else {
|
||||
std::string path = base::SysNSStringToUTF8([[dialog URL] path]);
|
||||
callback.Run(true, base::FilePath(path));
|
||||
}
|
||||
SaveDialogCompletion(chosen, dialog, settings, callback);
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,13 @@ class FileSelectHelper : public base::RefCounted<FileSelectHelper>,
|
|||
|
||||
~FileSelectHelper() override {}
|
||||
|
||||
void OnOpenDialogDone(bool result, const std::vector<base::FilePath>& paths) {
|
||||
#if defined(MAS_BUILD)
|
||||
void OnOpenDialogDone(bool result, const std::vector<base::FilePath>& paths,
|
||||
const std::vector<std::string>& bookmarks)
|
||||
#else
|
||||
void OnOpenDialogDone(bool result, const std::vector<base::FilePath>& paths)
|
||||
#endif
|
||||
{
|
||||
std::vector<content::FileChooserFileInfo> file_info;
|
||||
if (result) {
|
||||
for (auto& path : paths) {
|
||||
|
@ -73,7 +79,13 @@ class FileSelectHelper : public base::RefCounted<FileSelectHelper>,
|
|||
OnFilesSelected(file_info);
|
||||
}
|
||||
|
||||
void OnSaveDialogDone(bool result, const base::FilePath& path) {
|
||||
#if defined(MAS_BUILD)
|
||||
void OnSaveDialogDone(bool result, const base::FilePath& path,
|
||||
const std::string& bookmark)
|
||||
#else
|
||||
void OnSaveDialogDone(bool result, const base::FilePath& path)
|
||||
#endif
|
||||
{
|
||||
std::vector<content::FileChooserFileInfo> file_info;
|
||||
if (result) {
|
||||
content::FileChooserFileInfo info;
|
||||
|
|
|
@ -970,6 +970,23 @@ details. Disabled by default.
|
|||
Set the about panel options. This will override the values defined in the app's
|
||||
`.plist` file. See the [Apple docs][about-panel-options] for more details.
|
||||
|
||||
### `app.startAccessingSecurityScopedResource(bookmarkData)` _macOS (mas)_
|
||||
|
||||
* `bookmarkData` String - The base64 encoded security scoped bookmark data returned by the `dialog.showOpenDialog` or `dialog.showSaveDialog` methods.
|
||||
|
||||
Returns `Function` - This function **must** be called once you have finished accessing the security scoped file. If you do not remember to stop accessing the bookmark, [kernel resources will be leaked](https://developer.apple.com/reference/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc) and your app will lose its ability to reach outside the sandbox completely, until your app is restarted.
|
||||
|
||||
```js
|
||||
// Start accessing the file.
|
||||
const stopAccessingSecurityScopedResource = app.startAccessingSecurityScopedResource(data)
|
||||
// You can now access the file outside of the sandbox 🎉
|
||||
|
||||
// Remember to stop accessing the file once you've finished with it.
|
||||
stopAccessingSecurityScopedResource()
|
||||
```
|
||||
|
||||
Start accessing a security scoped resource. With this method electron applications that are packaged for the Mac App Store may reach outside their sandbox to access files chosen by the user. See [Apple's documentation](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) for a description of how this system works.
|
||||
|
||||
### `app.commandLine.appendSwitch(switch[, value])`
|
||||
|
||||
* `switch` String - A command-line switch
|
||||
|
|
|
@ -50,8 +50,10 @@ The `dialog` module has the following methods:
|
|||
as a directory instead of a file.
|
||||
* `message` String (optional) _macOS_ - Message to display above input
|
||||
boxes.
|
||||
* `securityScopedBookmarks` _masOS_ _mas_ - Create [security scoped bookmarks](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) when packaged for the Mac App Store.
|
||||
* `callback` Function (optional)
|
||||
* `filePaths` String[] - An array of file paths chosen by the user
|
||||
* `bookmarks` String[] _macOS_ _mas_ - An array matching the `filePaths` array of base64 encoded strings which contains security scoped bookmark data. `securityScopedBookmarks` must be enabled for this to be populated.
|
||||
|
||||
Returns `String[]`, an array of file paths chosen by the user,
|
||||
if the callback is provided it returns `undefined`.
|
||||
|
@ -99,8 +101,10 @@ shown.
|
|||
displayed in front of the filename text field.
|
||||
* `showsTagField` Boolean (optional) _macOS_ - Show the tags input box,
|
||||
defaults to `true`.
|
||||
* `securityScopedBookmarks` Boolean (optional) _masOS_ _mas_ - Create a [security scoped bookmark](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) when packaged for the Mac App Store. If this option is enabled and the file doesn't already exist a blank file will be created at the chosen path.
|
||||
* `callback` Function (optional)
|
||||
* `filename` String
|
||||
* `bookmark` String _macOS_ _mas_ - Base64 encoded string which contains the security scoped bookmark data for the saved file. `securityScopedBookmarks` must be enabled for this to be present.
|
||||
|
||||
Returns `String`, the path of the file chosen by the user,
|
||||
if a callback is provided it returns `undefined`.
|
||||
|
|
|
@ -740,6 +740,11 @@
|
|||
'atom/app/node_main.h',
|
||||
],
|
||||
}], # enable_run_as_node
|
||||
['mas_build==1', {
|
||||
'lib_sources': [
|
||||
'atom/browser/api/atom_api_app_mas.mm',
|
||||
],
|
||||
}], # mas_build==1
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
|
||||
let {buttonLabel, defaultPath, filters, properties, title, message} = options
|
||||
let {buttonLabel, defaultPath, filters, properties, title, message, securityScopedBookmarks = false} = options
|
||||
|
||||
if (properties == null) {
|
||||
properties = ['openFile']
|
||||
|
@ -126,10 +126,10 @@ module.exports = {
|
|||
throw new TypeError('Message must be a string')
|
||||
}
|
||||
|
||||
const wrappedCallback = typeof callback === 'function' ? function (success, result) {
|
||||
return callback(success ? result : void 0)
|
||||
const wrappedCallback = typeof callback === 'function' ? function (success, result, bookmarkData) {
|
||||
return success ? callback(result, bookmarkData) : callback()
|
||||
} : null
|
||||
const settings = {title, buttonLabel, defaultPath, filters, message, window}
|
||||
const settings = {title, buttonLabel, defaultPath, filters, message, securityScopedBookmarks, window}
|
||||
settings.properties = dialogProperties
|
||||
return binding.showOpenDialog(settings, wrappedCallback)
|
||||
},
|
||||
|
@ -145,7 +145,7 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
|
||||
let {buttonLabel, defaultPath, filters, title, message, nameFieldLabel, showsTagField} = options
|
||||
let {buttonLabel, defaultPath, filters, title, message, securityScopedBookmarks = false, nameFieldLabel, showsTagField} = options
|
||||
|
||||
if (title == null) {
|
||||
title = ''
|
||||
|
@ -185,10 +185,10 @@ module.exports = {
|
|||
showsTagField = true
|
||||
}
|
||||
|
||||
const wrappedCallback = typeof callback === 'function' ? function (success, result) {
|
||||
return callback(success ? result : void 0)
|
||||
const wrappedCallback = typeof callback === 'function' ? function (success, result, bookmarkData) {
|
||||
return success ? callback(result, bookmarkData) : callback()
|
||||
} : null
|
||||
const settings = {title, buttonLabel, defaultPath, filters, message, nameFieldLabel, showsTagField, window}
|
||||
const settings = {title, buttonLabel, defaultPath, filters, message, securityScopedBookmarks, nameFieldLabel, showsTagField, window}
|
||||
return binding.showSaveDialog(settings, wrappedCallback)
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in a new issue