AppSandboxFileAccess.m 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. //
  2. // AppSandboxFileAccess.m
  3. // AppSandboxFileAccess
  4. //
  5. // Created by Leigh McCulloch on 23/11/2013.
  6. //
  7. // Copyright (c) 2013, Leigh McCulloch
  8. // All rights reserved.
  9. //
  10. // BSD-2-Clause License: http://opensource.org/licenses/BSD-2-Clause
  11. //
  12. // Redistribution and use in source and binary forms, with or without
  13. // modification, are permitted provided that the following conditions are
  14. // met:
  15. //
  16. // 1. Redistributions of source code must retain the above copyright
  17. // notice, this list of conditions and the following disclaimer.
  18. //
  19. // 2. Redistributions in binary form must reproduce the above copyright
  20. // notice, this list of conditions and the following disclaimer in the
  21. // documentation and/or other materials provided with the distribution.
  22. //
  23. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  24. // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  25. // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
  26. // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  27. // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  28. // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
  29. // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  30. // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  31. // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  32. // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  33. // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  34. //
  35. #import "AppSandboxFileAccess.h"
  36. #import "AppSandboxFileAccessPersist.h"
  37. #import "AppSandboxFileAccessOpenSavePanelDelegate.h"
  38. #if !__has_feature(objc_arc)
  39. #error ARC must be enabled!
  40. #endif
  41. #define CFBundleDisplayName @"CFBundleDisplayName"
  42. #define CFBundleName @"CFBundleName"
  43. @interface AppSandboxFileAccess ()
  44. @property (nonatomic, strong) AppSandboxFileAccessPersist *defaultDelegate;
  45. @end
  46. @implementation AppSandboxFileAccess
  47. + (AppSandboxFileAccess *)fileAccess {
  48. return [[AppSandboxFileAccess alloc] init];
  49. }
  50. - (instancetype)init {
  51. self = [super init];
  52. if (self) {
  53. NSString *applicationName = [[NSBundle mainBundle] objectForInfoDictionaryKey:CFBundleDisplayName];
  54. if (!applicationName) {
  55. applicationName = [[NSBundle mainBundle] objectForInfoDictionaryKey:CFBundleName];
  56. }
  57. self.title = NSLocalizedString(@"Allow Access",nil);
  58. applicationName = [applicationName stringByAppendingString:@" "];
  59. self.message = [applicationName stringByAppendingString:NSLocalizedString(@"needs to access this path to continue. Click Allow to continue.", nil)];
  60. self.prompt = NSLocalizedString(@"Allow",nil);
  61. // create default delegate object that persists bookmarks to user defaults
  62. self.defaultDelegate = [[AppSandboxFileAccessPersist alloc] init];
  63. self.bookmarkPersistanceDelegate = _defaultDelegate;
  64. }
  65. return self;
  66. }
  67. - (NSURL *)askPermissionForURL:(NSURL *)url {
  68. NSParameterAssert(url);
  69. // this url will be the url allowed, it might be a parent url of the url passed in
  70. __block NSURL *allowedURL = nil;
  71. // create delegate that will limit which files in the open panel can be selected, to ensure only a folder
  72. // or file giving permission to the file requested can be selected
  73. AppSandboxFileAccessOpenSavePanelDelegate *openPanelDelegate = [[AppSandboxFileAccessOpenSavePanelDelegate alloc] initWithFileURL:url];
  74. // check that the url exists, if it doesn't, find the parent path of the url that does exist and ask permission for that
  75. NSFileManager *fileManager = [NSFileManager defaultManager];
  76. NSString *path = [url path];
  77. while (path.length > 1) { // give up when only '/' is left in the path or if we get to a path that exists
  78. if ([fileManager fileExistsAtPath:path isDirectory:NULL]) {
  79. break;
  80. }
  81. path = [path stringByDeletingLastPathComponent];
  82. }
  83. url = [NSURL fileURLWithPath:path];
  84. // display the open panel
  85. dispatch_block_t displayOpenPanelBlock = ^{
  86. NSOpenPanel *openPanel = [NSOpenPanel openPanel];
  87. [openPanel setMessage:self.message];
  88. [openPanel setCanCreateDirectories:NO];
  89. [openPanel setCanChooseFiles:YES];
  90. [openPanel setCanChooseDirectories:YES];
  91. [openPanel setAllowsMultipleSelection:NO];
  92. [openPanel setPrompt:self.prompt];
  93. [openPanel setTitle:self.title];
  94. [openPanel setShowsHiddenFiles:NO];
  95. [openPanel setExtensionHidden:NO];
  96. [openPanel setDirectoryURL:url];
  97. [openPanel setDelegate:openPanelDelegate];
  98. [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
  99. NSInteger openPanelButtonPressed = [openPanel runModal];
  100. if (openPanelButtonPressed == NSFileHandlingPanelOKButton) {
  101. allowedURL = [openPanel URL];
  102. }
  103. };
  104. if ([NSThread isMainThread]) {
  105. displayOpenPanelBlock();
  106. } else {
  107. dispatch_sync(dispatch_get_main_queue(), displayOpenPanelBlock);
  108. }
  109. return allowedURL;
  110. }
  111. - (NSData *)persistPermissionPath:(NSString *)path {
  112. NSParameterAssert(path);
  113. return [self persistPermissionURL:[NSURL fileURLWithPath:path]];
  114. }
  115. - (NSData *)persistPermissionURL:(NSURL *)url {
  116. NSParameterAssert(url);
  117. // store the sandbox permissions
  118. NSData *bookmarkData = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:NULL];
  119. if (bookmarkData) {
  120. [self.bookmarkPersistanceDelegate setBookmarkData:bookmarkData forURL:url];
  121. }
  122. return bookmarkData;
  123. }
  124. - (BOOL)accessFilePath:(NSString *)path withBlock:(AppSandboxFileAccessBlock)block persistPermission:(BOOL)persist {
  125. // Deprecated. Use 'accessFilePath:persistPermission:withBlock:' instead.
  126. return [self accessFilePath:path persistPermission:persist withBlock:block];
  127. }
  128. - (BOOL)accessFileURL:(NSURL *)fileURL withBlock:(AppSandboxFileAccessBlock)block persistPermission:(BOOL)persist {
  129. // Deprecated. Use 'accessFileURL:persistPermission:withBlock:' instead.
  130. return [self accessFileURL:fileURL persistPermission:persist withBlock:block];
  131. }
  132. - (BOOL)accessFilePath:(NSString *)path persistPermission:(BOOL)persist withBlock:(AppSandboxFileAccessBlock)block {
  133. return [self accessFileURL:[NSURL fileURLWithPath:path] persistPermission:persist withBlock:block];
  134. }
  135. - (BOOL)accessFileURL:(NSURL *)fileURL persistPermission:(BOOL)persist withBlock:(AppSandboxFileAccessBlock)block {
  136. NSParameterAssert(fileURL);
  137. NSParameterAssert(block);
  138. BOOL success = [self requestAccessPermissionsForFileURL:fileURL persistPermission:persist withBlock:^(NSURL *securityScopedFileURL, NSData *bookmarkData) {
  139. // execute the block with the file access permissions
  140. @try {
  141. [securityScopedFileURL startAccessingSecurityScopedResource];
  142. block();
  143. } @finally {
  144. //[securityScopedFileURL stopAccessingSecurityScopedResource];
  145. }
  146. }];
  147. return success;
  148. }
  149. - (BOOL)requestAccessPermissionsForFilePath:(NSString *)filePath persistPermission:(BOOL)persist withBlock:(AppSandboxFileSecurityScopeBlock)block {
  150. NSParameterAssert(filePath);
  151. NSURL *fileURL = [NSURL fileURLWithPath:filePath];
  152. return [self requestAccessPermissionsForFileURL:fileURL persistPermission:persist withBlock:block];
  153. }
  154. - (BOOL)requestAccessPermissionsForFileURL:(NSURL *)fileURL persistPermission:(BOOL)persist withBlock:(AppSandboxFileSecurityScopeBlock)block {
  155. NSParameterAssert(fileURL);
  156. NSURL *allowedURL = nil;
  157. // standardize the file url and remove any symlinks so that the url we lookup in bookmark data would match a url given by the askPermissionForURL method
  158. fileURL = [[fileURL URLByStandardizingPath] URLByResolvingSymlinksInPath];
  159. // lookup bookmark data for this url, this will automatically load bookmark data for a parent path if we have it
  160. NSData *bookmarkData = [self.bookmarkPersistanceDelegate bookmarkDataForURL:fileURL];
  161. if (bookmarkData) {
  162. // resolve the bookmark data into an NSURL object that will allow us to use the file
  163. BOOL bookmarkDataIsStale;
  164. allowedURL = [NSURL URLByResolvingBookmarkData:bookmarkData options:NSURLBookmarkResolutionWithSecurityScope|NSURLBookmarkResolutionWithoutUI relativeToURL:nil bookmarkDataIsStale:&bookmarkDataIsStale error:NULL];
  165. // if the bookmark data is stale we'll attempt to recreate it with the existing url object if possible (not guaranteed)
  166. if (bookmarkDataIsStale) {
  167. bookmarkData = nil;
  168. [self.bookmarkPersistanceDelegate clearBookmarkDataForURL:fileURL];
  169. if (allowedURL) {
  170. bookmarkData = [self persistPermissionURL:allowedURL];
  171. if (!bookmarkData) {
  172. allowedURL = nil;
  173. }
  174. }
  175. }
  176. }
  177. // if allowed url is nil, we need to ask the user for permission
  178. if (!allowedURL) {
  179. allowedURL = [self askPermissionForURL:fileURL];
  180. if (!allowedURL) {
  181. // if the user did not give permission, exit out here
  182. return NO;
  183. }
  184. }
  185. // if we have no bookmark data and we want to persist, we need to create it
  186. if (persist && !bookmarkData) {
  187. bookmarkData = [self persistPermissionURL:allowedURL];
  188. }
  189. if (block) {
  190. block(allowedURL, bookmarkData);
  191. }
  192. return YES;
  193. }
  194. - (void)setBookmarkPersistanceDelegate:(NSObject<AppSandboxFileAccessProtocol> *)bookmarkPersistanceDelegate
  195. {
  196. // revert to default delegate object if no delegate provided
  197. if (bookmarkPersistanceDelegate == nil) {
  198. _bookmarkPersistanceDelegate = self.defaultDelegate;
  199. } else {
  200. _bookmarkPersistanceDelegate = bookmarkPersistanceDelegate;
  201. }
  202. }
  203. @end