file_dialog_mac.mm 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. // Copyright (c) 2013 GitHub, Inc.
  2. // Use of this source code is governed by the MIT license that can be
  3. // found in the LICENSE file.
  4. #include "shell/browser/ui/file_dialog.h"
  5. #include <string>
  6. #include <utility>
  7. #include <vector>
  8. #import <Cocoa/Cocoa.h>
  9. #import <CoreServices/CoreServices.h>
  10. #include "base/apple/foundation_util.h"
  11. #include "base/apple/scoped_cftyperef.h"
  12. #include "base/files/file_util.h"
  13. #include "base/mac/mac_util.h"
  14. #include "base/strings/sys_string_conversions.h"
  15. #include "content/public/browser/browser_task_traits.h"
  16. #include "content/public/browser/browser_thread.h"
  17. #include "shell/browser/native_window.h"
  18. #include "shell/common/gin_converters/file_path_converter.h"
  19. #include "shell/common/thread_restrictions.h"
  20. @interface PopUpButtonHandler : NSObject
  21. @property(nonatomic, assign) NSSavePanel* savePanel;
  22. @property(nonatomic, strong) NSArray* fileTypesList;
  23. - (instancetype)initWithPanel:(NSSavePanel*)panel
  24. andTypesList:(NSArray*)typesList;
  25. - (void)selectFormat:(id)sender;
  26. @end
  27. @implementation PopUpButtonHandler
  28. @synthesize savePanel;
  29. @synthesize fileTypesList;
  30. - (instancetype)initWithPanel:(NSSavePanel*)panel
  31. andTypesList:(NSArray*)typesList {
  32. self = [super init];
  33. if (self) {
  34. [self setSavePanel:panel];
  35. [self setFileTypesList:typesList];
  36. }
  37. return self;
  38. }
  39. - (void)selectFormat:(id)sender {
  40. NSPopUpButton* button = (NSPopUpButton*)sender;
  41. NSInteger selectedItemIndex = [button indexOfSelectedItem];
  42. NSArray* list = [self fileTypesList];
  43. NSArray* fileTypes = [list objectAtIndex:selectedItemIndex];
  44. // If we meet a '*' file extension, we allow all the file types and no
  45. // need to set the specified file types.
  46. if ([fileTypes count] == 0 || [fileTypes containsObject:@"*"])
  47. [[self savePanel] setAllowedFileTypes:nil];
  48. else
  49. [[self savePanel] setAllowedFileTypes:fileTypes];
  50. }
  51. @end
  52. // Manages the PopUpButtonHandler.
  53. @interface ElectronAccessoryView : NSView
  54. @end
  55. @implementation ElectronAccessoryView
  56. - (void)dealloc {
  57. auto* popupButton =
  58. static_cast<NSPopUpButton*>([[self subviews] objectAtIndex:1]);
  59. popupButton.target = nil;
  60. }
  61. @end
  62. namespace file_dialog {
  63. DialogSettings::DialogSettings() = default;
  64. DialogSettings::DialogSettings(const DialogSettings&) = default;
  65. DialogSettings::~DialogSettings() = default;
  66. namespace {
  67. void SetAllowedFileTypes(NSSavePanel* dialog, const Filters& filters) {
  68. NSMutableArray* file_types_list = [NSMutableArray array];
  69. NSMutableArray* filter_names = [NSMutableArray array];
  70. // Create array to keep file types and their name.
  71. for (const Filter& filter : filters) {
  72. NSMutableOrderedSet* file_type_set =
  73. [NSMutableOrderedSet orderedSetWithCapacity:filters.size()];
  74. [filter_names addObject:@(filter.first.c_str())];
  75. for (std::string ext : filter.second) {
  76. // macOS is incapable of understanding multiple file extensions,
  77. // so we need to tokenize the extension that's been passed in.
  78. // We want to err on the side of allowing files, so we pass
  79. // along only the final extension ('tar.gz' => 'gz').
  80. auto pos = ext.rfind('.');
  81. if (pos != std::string::npos) {
  82. ext.erase(0, pos + 1);
  83. }
  84. [file_type_set addObject:@(ext.c_str())];
  85. }
  86. [file_types_list addObject:[file_type_set array]];
  87. }
  88. // Passing empty array to setAllowedFileTypes will cause exception.
  89. NSArray* file_types = nil;
  90. NSUInteger count = [file_types_list count];
  91. if (count > 0) {
  92. file_types = [[file_types_list objectAtIndex:0] allObjects];
  93. // If we meet a '*' file extension, we allow all the file types and no
  94. // need to set the specified file types.
  95. if ([file_types count] == 0 || [file_types containsObject:@"*"])
  96. file_types = nil;
  97. }
  98. [dialog setAllowedFileTypes:file_types];
  99. if (count <= 1)
  100. return; // don't add file format picker
  101. // Add file format picker.
  102. ElectronAccessoryView* accessoryView = [[ElectronAccessoryView alloc]
  103. initWithFrame:NSMakeRect(0.0, 0.0, 200, 32.0)];
  104. NSTextField* label =
  105. [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 60, 22)];
  106. [label setEditable:NO];
  107. [label setStringValue:@"Format:"];
  108. [label setBordered:NO];
  109. [label setBezeled:NO];
  110. [label setDrawsBackground:NO];
  111. NSPopUpButton* popupButton =
  112. [[NSPopUpButton alloc] initWithFrame:NSMakeRect(50.0, 2, 140, 22.0)
  113. pullsDown:NO];
  114. PopUpButtonHandler* popUpButtonHandler =
  115. [[PopUpButtonHandler alloc] initWithPanel:dialog
  116. andTypesList:file_types_list];
  117. [popupButton addItemsWithTitles:filter_names];
  118. [popupButton setTarget:popUpButtonHandler];
  119. [popupButton setAction:@selector(selectFormat:)];
  120. [accessoryView addSubview:label];
  121. [accessoryView addSubview:popupButton];
  122. [dialog setAccessoryView:accessoryView];
  123. }
  124. void SetupDialog(NSSavePanel* dialog, const DialogSettings& settings) {
  125. if (!settings.title.empty())
  126. [dialog setTitle:base::SysUTF8ToNSString(settings.title)];
  127. if (!settings.button_label.empty())
  128. [dialog setPrompt:base::SysUTF8ToNSString(settings.button_label)];
  129. if (!settings.message.empty())
  130. [dialog setMessage:base::SysUTF8ToNSString(settings.message)];
  131. if (!settings.name_field_label.empty())
  132. [dialog
  133. setNameFieldLabel:base::SysUTF8ToNSString(settings.name_field_label)];
  134. [dialog setShowsTagField:settings.shows_tag_field];
  135. NSString* default_dir = nil;
  136. NSString* default_filename = nil;
  137. if (!settings.default_path.empty()) {
  138. electron::ScopedAllowBlockingForElectron allow_blocking;
  139. if (base::DirectoryExists(settings.default_path)) {
  140. default_dir = base::SysUTF8ToNSString(settings.default_path.value());
  141. } else {
  142. if (settings.default_path.IsAbsolute()) {
  143. default_dir =
  144. base::SysUTF8ToNSString(settings.default_path.DirName().value());
  145. }
  146. default_filename =
  147. base::SysUTF8ToNSString(settings.default_path.BaseName().value());
  148. }
  149. }
  150. if (settings.filters.empty()) {
  151. [dialog setAllowsOtherFileTypes:YES];
  152. } else {
  153. // Set setAllowedFileTypes before setNameFieldStringValue as it might
  154. // override the extension set using setNameFieldStringValue
  155. SetAllowedFileTypes(dialog, settings.filters);
  156. }
  157. // Make sure the extension is always visible. Without this, the extension in
  158. // the default filename will not be used in the saved file.
  159. [dialog setExtensionHidden:NO];
  160. if (default_dir)
  161. [dialog setDirectoryURL:[NSURL fileURLWithPath:default_dir]];
  162. if (default_filename)
  163. [dialog setNameFieldStringValue:default_filename];
  164. }
  165. void SetupOpenDialogForProperties(NSOpenPanel* dialog, int properties) {
  166. [dialog setCanChooseFiles:(properties & OPEN_DIALOG_OPEN_FILE)];
  167. if (properties & OPEN_DIALOG_OPEN_DIRECTORY)
  168. [dialog setCanChooseDirectories:YES];
  169. if (properties & OPEN_DIALOG_CREATE_DIRECTORY)
  170. [dialog setCanCreateDirectories:YES];
  171. if (properties & OPEN_DIALOG_MULTI_SELECTIONS)
  172. [dialog setAllowsMultipleSelection:YES];
  173. if (properties & OPEN_DIALOG_SHOW_HIDDEN_FILES)
  174. [dialog setShowsHiddenFiles:YES];
  175. if (properties & OPEN_DIALOG_NO_RESOLVE_ALIASES)
  176. [dialog setResolvesAliases:NO];
  177. if (properties & OPEN_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY)
  178. [dialog setTreatsFilePackagesAsDirectories:YES];
  179. }
  180. void SetupSaveDialogForProperties(NSSavePanel* dialog, int properties) {
  181. if (properties & SAVE_DIALOG_CREATE_DIRECTORY)
  182. [dialog setCanCreateDirectories:YES];
  183. if (properties & SAVE_DIALOG_SHOW_HIDDEN_FILES)
  184. [dialog setShowsHiddenFiles:YES];
  185. if (properties & SAVE_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY)
  186. [dialog setTreatsFilePackagesAsDirectories:YES];
  187. }
  188. // Run modal dialog with parent window and return user's choice.
  189. int RunModalDialog(NSSavePanel* dialog, const DialogSettings& settings) {
  190. __block int chosen = NSModalResponseCancel;
  191. if (!settings.parent_window || !settings.parent_window->GetNativeWindow() ||
  192. settings.force_detached) {
  193. chosen = [dialog runModal];
  194. } else {
  195. NSWindow* window =
  196. settings.parent_window->GetNativeWindow().GetNativeNSWindow();
  197. [dialog beginSheetModalForWindow:window
  198. completionHandler:^(NSInteger c) {
  199. chosen = c;
  200. [NSApp stopModal];
  201. }];
  202. [NSApp runModalForWindow:window];
  203. }
  204. return chosen;
  205. }
  206. // Create bookmark data and serialise it into a base64 string.
  207. std::string GetBookmarkDataFromNSURL(NSURL* url) {
  208. // Create the file if it doesn't exist (necessary for NSSavePanel options).
  209. NSFileManager* defaultManager = [NSFileManager defaultManager];
  210. if (![defaultManager fileExistsAtPath:[url path]]) {
  211. [defaultManager createFileAtPath:[url path] contents:nil attributes:nil];
  212. }
  213. NSError* error = nil;
  214. NSData* bookmarkData =
  215. [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
  216. includingResourceValuesForKeys:nil
  217. relativeToURL:nil
  218. error:&error];
  219. if (error != nil) {
  220. // Send back an empty string if there was an error.
  221. return "";
  222. } else {
  223. // Encode NSData in base64 then convert to NSString.
  224. NSString* base64data = [[NSString alloc]
  225. initWithData:[bookmarkData base64EncodedDataWithOptions:0]
  226. encoding:NSUTF8StringEncoding];
  227. return base::SysNSStringToUTF8(base64data);
  228. }
  229. }
  230. void ReadDialogPathsWithBookmarks(NSOpenPanel* dialog,
  231. std::vector<base::FilePath>* paths,
  232. std::vector<std::string>* bookmarks) {
  233. NSArray* urls = [dialog URLs];
  234. for (NSURL* url in urls) {
  235. if (![url isFileURL])
  236. continue;
  237. NSString* path = [url path];
  238. // There's a bug in macOS where despite a request to disallow file
  239. // selection, files/packages can be selected. If file selection
  240. // was disallowed, drop any files selected. See crbug.com/1357523.
  241. if (![dialog canChooseFiles]) {
  242. BOOL is_directory;
  243. BOOL exists =
  244. [[NSFileManager defaultManager] fileExistsAtPath:path
  245. isDirectory:&is_directory];
  246. BOOL is_package =
  247. [[NSWorkspace sharedWorkspace] isFilePackageAtPath:path];
  248. if (!exists || !is_directory || is_package)
  249. continue;
  250. }
  251. paths->emplace_back(base::SysNSStringToUTF8(path));
  252. bookmarks->push_back(GetBookmarkDataFromNSURL(url));
  253. }
  254. }
  255. void ReadDialogPaths(NSOpenPanel* dialog, std::vector<base::FilePath>* paths) {
  256. std::vector<std::string> ignored_bookmarks;
  257. ReadDialogPathsWithBookmarks(dialog, paths, &ignored_bookmarks);
  258. }
  259. void ResolvePromiseInNextTick(gin_helper::Promise<v8::Local<v8::Value>> promise,
  260. v8::Local<v8::Value> value) {
  261. // The completionHandler runs inside a transaction commit, and we should
  262. // not do any runModal inside it. However since we can not control what
  263. // users will run in the microtask, we have to delay the resolution until
  264. // next tick, otherwise crash like this may happen:
  265. // https://github.com/electron/electron/issues/26884
  266. content::GetUIThreadTaskRunner({})->PostTask(
  267. FROM_HERE,
  268. base::BindOnce(
  269. [](gin_helper::Promise<v8::Local<v8::Value>> promise,
  270. v8::Global<v8::Value> global) {
  271. v8::Isolate* isolate = promise.isolate();
  272. v8::HandleScope handle_scope(isolate);
  273. v8::Local<v8::Value> value = global.Get(isolate);
  274. promise.Resolve(value);
  275. },
  276. std::move(promise), v8::Global<v8::Value>(promise.isolate(), value)));
  277. }
  278. } // namespace
  279. bool ShowOpenDialogSync(const DialogSettings& settings,
  280. std::vector<base::FilePath>* paths) {
  281. DCHECK(paths);
  282. NSOpenPanel* dialog = [NSOpenPanel openPanel];
  283. SetupDialog(dialog, settings);
  284. SetupOpenDialogForProperties(dialog, settings.properties);
  285. int chosen = RunModalDialog(dialog, settings);
  286. if (chosen == NSModalResponseCancel)
  287. return false;
  288. ReadDialogPaths(dialog, paths);
  289. return true;
  290. }
  291. void OpenDialogCompletion(int chosen,
  292. NSOpenPanel* dialog,
  293. bool security_scoped_bookmarks,
  294. gin_helper::Promise<gin_helper::Dictionary> promise) {
  295. v8::HandleScope scope(promise.isolate());
  296. gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(promise.isolate());
  297. if (chosen == NSModalResponseCancel) {
  298. dict.Set("canceled", true);
  299. dict.Set("filePaths", std::vector<base::FilePath>());
  300. #if IS_MAS_BUILD()
  301. dict.Set("bookmarks", std::vector<std::string>());
  302. #endif
  303. } else {
  304. std::vector<base::FilePath> paths;
  305. dict.Set("canceled", false);
  306. #if IS_MAS_BUILD()
  307. std::vector<std::string> bookmarks;
  308. if (security_scoped_bookmarks)
  309. ReadDialogPathsWithBookmarks(dialog, &paths, &bookmarks);
  310. else
  311. ReadDialogPaths(dialog, &paths);
  312. dict.Set("filePaths", paths);
  313. dict.Set("bookmarks", bookmarks);
  314. #else
  315. ReadDialogPaths(dialog, &paths);
  316. dict.Set("filePaths", paths);
  317. #endif
  318. }
  319. ResolvePromiseInNextTick(promise.As<v8::Local<v8::Value>>(),
  320. dict.GetHandle());
  321. }
  322. void ShowOpenDialog(const DialogSettings& settings,
  323. gin_helper::Promise<gin_helper::Dictionary> promise) {
  324. NSOpenPanel* dialog = [NSOpenPanel openPanel];
  325. SetupDialog(dialog, settings);
  326. SetupOpenDialogForProperties(dialog, settings.properties);
  327. // Capture the value of the security_scoped_bookmarks settings flag
  328. // and pass it to the completion handler.
  329. bool security_scoped_bookmarks = settings.security_scoped_bookmarks;
  330. __block gin_helper::Promise<gin_helper::Dictionary> p = std::move(promise);
  331. if (!settings.parent_window || !settings.parent_window->GetNativeWindow() ||
  332. settings.force_detached) {
  333. [dialog beginWithCompletionHandler:^(NSInteger chosen) {
  334. OpenDialogCompletion(chosen, dialog, security_scoped_bookmarks,
  335. std::move(p));
  336. }];
  337. } else {
  338. NSWindow* window =
  339. settings.parent_window->GetNativeWindow().GetNativeNSWindow();
  340. [dialog
  341. beginSheetModalForWindow:window
  342. completionHandler:^(NSInteger chosen) {
  343. OpenDialogCompletion(chosen, dialog, security_scoped_bookmarks,
  344. std::move(p));
  345. }];
  346. }
  347. }
  348. bool ShowSaveDialogSync(const DialogSettings& settings, base::FilePath* path) {
  349. DCHECK(path);
  350. NSSavePanel* dialog = [NSSavePanel savePanel];
  351. SetupDialog(dialog, settings);
  352. SetupSaveDialogForProperties(dialog, settings.properties);
  353. int chosen = RunModalDialog(dialog, settings);
  354. if (chosen == NSModalResponseCancel || ![[dialog URL] isFileURL])
  355. return false;
  356. *path = base::FilePath(base::SysNSStringToUTF8([[dialog URL] path]));
  357. return true;
  358. }
  359. void SaveDialogCompletion(int chosen,
  360. NSSavePanel* dialog,
  361. bool security_scoped_bookmarks,
  362. gin_helper::Promise<gin_helper::Dictionary> promise) {
  363. v8::HandleScope scope(promise.isolate());
  364. gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(promise.isolate());
  365. if (chosen == NSModalResponseCancel) {
  366. dict.Set("canceled", true);
  367. dict.Set("filePath", base::FilePath());
  368. #if IS_MAS_BUILD()
  369. dict.Set("bookmark", base::StringPiece());
  370. #endif
  371. } else {
  372. std::string path = base::SysNSStringToUTF8([[dialog URL] path]);
  373. dict.Set("filePath", base::FilePath(path));
  374. dict.Set("canceled", false);
  375. #if IS_MAS_BUILD()
  376. std::string bookmark;
  377. if (security_scoped_bookmarks) {
  378. bookmark = GetBookmarkDataFromNSURL([dialog URL]);
  379. dict.Set("bookmark", bookmark);
  380. }
  381. #endif
  382. }
  383. ResolvePromiseInNextTick(promise.As<v8::Local<v8::Value>>(),
  384. dict.GetHandle());
  385. }
  386. void ShowSaveDialog(const DialogSettings& settings,
  387. gin_helper::Promise<gin_helper::Dictionary> promise) {
  388. NSSavePanel* dialog = [NSSavePanel savePanel];
  389. SetupDialog(dialog, settings);
  390. SetupSaveDialogForProperties(dialog, settings.properties);
  391. [dialog setCanSelectHiddenExtension:YES];
  392. // Capture the value of the security_scoped_bookmarks settings flag
  393. // and pass it to the completion handler.
  394. bool security_scoped_bookmarks = settings.security_scoped_bookmarks;
  395. __block gin_helper::Promise<gin_helper::Dictionary> p = std::move(promise);
  396. if (!settings.parent_window || !settings.parent_window->GetNativeWindow() ||
  397. settings.force_detached) {
  398. [dialog beginWithCompletionHandler:^(NSInteger chosen) {
  399. SaveDialogCompletion(chosen, dialog, security_scoped_bookmarks,
  400. std::move(p));
  401. }];
  402. } else {
  403. NSWindow* window =
  404. settings.parent_window->GetNativeWindow().GetNativeNSWindow();
  405. [dialog
  406. beginSheetModalForWindow:window
  407. completionHandler:^(NSInteger chosen) {
  408. SaveDialogCompletion(chosen, dialog, security_scoped_bookmarks,
  409. std::move(p));
  410. }];
  411. }
  412. }
  413. } // namespace file_dialog