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:
acheronfail 2018-02-13 05:25:06 +11:00 committed by shelley vohr
parent 9f78ef0179
commit d1d50a4c92
11 changed files with 226 additions and 42 deletions

View file

@ -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);
}

View file

@ -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.

View 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

View file

@ -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;
}
};

View file

@ -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,

View file

@ -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);
}];
}
}

View file

@ -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;

View file

@ -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

View file

@ -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`.

View file

@ -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
],
},
}

View file

@ -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)
},