file_dialog_mac.mm 17 KB

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