453 lines
11 KiB
Objective-C
453 lines
11 KiB
Objective-C
//
|
|
// SandboxBroker.m
|
|
// Cog
|
|
//
|
|
// Created by Christopher Snowhill on 6/20/22.
|
|
//
|
|
|
|
#import <Foundation/Foundation.h>
|
|
|
|
#import <Cocoa/Cocoa.h>
|
|
|
|
#import "SandboxBroker.h"
|
|
|
|
#import "Logging.h"
|
|
|
|
#import "Cog-Swift.h"
|
|
|
|
#import "PlaylistController.h"
|
|
|
|
static SandboxBroker *kSharedSandboxBroker = nil;
|
|
|
|
@interface SandboxEntry : NSObject {
|
|
SandboxToken *_token;
|
|
NSInteger _refCount;
|
|
NSURL *_secureUrl;
|
|
NSString *_path;
|
|
BOOL _isFolder;
|
|
};
|
|
|
|
@property(readonly) SandboxToken *token;
|
|
|
|
@property NSURL *secureUrl;
|
|
|
|
@property(readonly) NSString *path;
|
|
|
|
@property NSInteger refCount;
|
|
|
|
@property(readonly) BOOL isFolder;
|
|
|
|
- (id)initWithToken:(SandboxToken *)token;
|
|
@end
|
|
|
|
@implementation SandboxEntry
|
|
- (id)initWithToken:(SandboxToken *)token {
|
|
SandboxEntry *obj = [super init];
|
|
if(obj) {
|
|
obj->_refCount = 1;
|
|
obj->_secureUrl = nil;
|
|
obj->_token = token;
|
|
obj->_path = token.path;
|
|
obj->_isFolder = token.folder;
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
- (NSInteger)refCount {
|
|
return _refCount;
|
|
}
|
|
|
|
- (void)setRefCount:(NSInteger)refCount {
|
|
_refCount = refCount;
|
|
}
|
|
|
|
- (NSURL *)secureUrl {
|
|
return _secureUrl;
|
|
}
|
|
|
|
- (void)setSecureUrl:(NSURL *)url {
|
|
_secureUrl = url;
|
|
}
|
|
|
|
- (SandboxToken *)token {
|
|
return _token;
|
|
}
|
|
|
|
- (NSString *)path {
|
|
return _path;
|
|
}
|
|
|
|
- (BOOL)isFolder {
|
|
return _isFolder;
|
|
}
|
|
@end
|
|
|
|
@implementation SandboxBroker
|
|
|
|
+ (id)sharedSandboxBroker {
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
kSharedSandboxBroker = [[self alloc] init];
|
|
});
|
|
return kSharedSandboxBroker;
|
|
}
|
|
|
|
+ (NSLock *)sharedPersistentContainerLock {
|
|
return [NSClassFromString(@"PlaylistController") sharedPersistentContainerLock];
|
|
}
|
|
|
|
+ (NSPersistentContainer *)sharedPersistentContainer {
|
|
return [NSClassFromString(@"PlaylistController") sharedPersistentContainer];
|
|
}
|
|
|
|
+ (NSURL *)urlWithoutFragment:(NSURL *)url {
|
|
if(![url isFileURL]) return url;
|
|
|
|
NSString *s = [url path];
|
|
|
|
NSRange fragmentRange = [s rangeOfString:@"#"
|
|
options:NSBackwardsSearch];
|
|
|
|
if(fragmentRange.location != NSNotFound) {
|
|
// Chop the fragment.
|
|
NSString *newURLString = [s substringToIndex:fragmentRange.location];
|
|
|
|
return [NSURL fileURLWithPath:newURLString];
|
|
} else {
|
|
return url;
|
|
}
|
|
}
|
|
|
|
- (id)init {
|
|
id _self = [super init];
|
|
if(_self) {
|
|
storage = [[NSMutableArray alloc] init];
|
|
}
|
|
|
|
return _self;
|
|
}
|
|
|
|
- (void)shutdown {
|
|
for(SandboxEntry *obj in storage) {
|
|
if([obj secureUrl]) {
|
|
[[obj secureUrl] stopAccessingSecurityScopedResource];
|
|
}
|
|
}
|
|
}
|
|
|
|
+ (BOOL)isPath:(NSURL *)path aSubdirectoryOf:(NSURL *)directory {
|
|
NSArray *pathComponents = [path pathComponents];
|
|
NSArray *directoryComponents = [directory pathComponents];
|
|
|
|
if([pathComponents count] < [directoryComponents count])
|
|
return NO;
|
|
|
|
for(size_t i = 0; i < [directoryComponents count]; ++i) {
|
|
if(![pathComponents[i] isEqualToString:directoryComponents[i]])
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (SandboxEntry *)recursivePathTest:(NSURL *)url {
|
|
SandboxEntry *ret = nil;
|
|
|
|
NSLock *lock = [SandboxBroker sharedPersistentContainerLock];
|
|
NSPersistentContainer *pc = [SandboxBroker sharedPersistentContainer];
|
|
|
|
NSPredicate *folderPredicate = [NSPredicate predicateWithFormat:@"folder == NO"];
|
|
NSPredicate *filePredicate = [NSPredicate predicateWithFormat:@"path == %@", [url path]];
|
|
NSPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:@[folderPredicate, filePredicate]];
|
|
|
|
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"SandboxToken"];
|
|
request.predicate = predicate;
|
|
|
|
NSError *error = nil;
|
|
[lock lock];
|
|
NSArray *results = [pc.viewContext executeFetchRequest:request error:&error];
|
|
[lock unlock];
|
|
if(results && [results count] > 0) {
|
|
ret = [[SandboxEntry alloc] initWithToken:results[0]];
|
|
}
|
|
|
|
if(!ret) {
|
|
predicate = [NSPredicate predicateWithFormat:@"folder == YES"];
|
|
|
|
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"path.length" ascending:NO];
|
|
|
|
request = [NSFetchRequest fetchRequestWithEntityName:@"SandboxToken"];
|
|
request.sortDescriptors = @[sortDescriptor];
|
|
request.predicate = predicate;
|
|
|
|
error = nil;
|
|
[lock lock];
|
|
results = [pc.viewContext executeFetchRequest:request error:&error];
|
|
[lock unlock];
|
|
|
|
if(results && [results count] > 0) {
|
|
for(SandboxToken *token in results) {
|
|
if(token.path && [SandboxBroker isPath:url aSubdirectoryOf:[NSURL fileURLWithPath:token.path]]) {
|
|
SandboxEntry *entry = [[SandboxEntry alloc] initWithToken:token];
|
|
|
|
ret = entry;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(ret) {
|
|
BOOL isStale;
|
|
NSError *err = nil;
|
|
NSURL *secureUrl = [NSURL URLByResolvingBookmarkData:ret.token.bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&err];
|
|
if(!secureUrl && err) {
|
|
ALog(@"Failed to access bookmark for URL: %@, error: %@", ret.token.path, [err localizedDescription]);
|
|
return nil;
|
|
}
|
|
|
|
ret.secureUrl = secureUrl;
|
|
|
|
return ret;
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_block_t block) {
|
|
if(dispatch_queue_get_label(queue) == dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)) {
|
|
block();
|
|
} else {
|
|
dispatch_sync(queue, block);
|
|
}
|
|
}
|
|
|
|
- (void)addFolderIfMissing:(NSURL *)folderUrl {
|
|
if(![folderUrl isFileURL]) return;
|
|
|
|
@synchronized (self) {
|
|
SandboxEntry *_entry = nil;
|
|
|
|
for(SandboxEntry *entry in storage) {
|
|
if(entry.path && entry.isFolder && [SandboxBroker isPath:folderUrl aSubdirectoryOf:[NSURL fileURLWithPath:entry.path]]) {
|
|
_entry = entry;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!_entry) {
|
|
_entry = [self recursivePathTest:folderUrl];
|
|
}
|
|
|
|
if(!_entry) {
|
|
NSError *err = nil;
|
|
NSData *bookmark = [folderUrl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&err];
|
|
if(!bookmark && err) {
|
|
ALog(@"Failed to add bookmark for URL: %@, with error: %@", folderUrl, [err localizedDescription]);
|
|
return;
|
|
}
|
|
|
|
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
|
|
NSLock *lock = [SandboxBroker sharedPersistentContainerLock];
|
|
NSPersistentContainer *pc = [SandboxBroker sharedPersistentContainer];
|
|
|
|
[lock lock];
|
|
SandboxToken *token = [NSEntityDescription insertNewObjectForEntityForName:@"SandboxToken" inManagedObjectContext:pc.viewContext];
|
|
[lock unlock];
|
|
|
|
if(token) {
|
|
token.path = [folderUrl path];
|
|
token.bookmark = bookmark;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)addFileIfMissing:(NSURL *)fileUrl {
|
|
if(![fileUrl isFileURL]) return;
|
|
|
|
NSURL *url = [SandboxBroker urlWithoutFragment:fileUrl];
|
|
|
|
@synchronized (self) {
|
|
SandboxEntry *_entry = nil;
|
|
|
|
for(SandboxEntry *entry in storage) {
|
|
if(entry.path) {
|
|
if((entry.isFolder && [SandboxBroker isPath:url aSubdirectoryOf:[NSURL fileURLWithPath:entry.path]]) ||
|
|
(!entry.isFolder && [url isEqualTo:[NSURL fileURLWithPath:entry.path]])) {
|
|
_entry = entry;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!_entry) {
|
|
_entry = [self recursivePathTest:url];
|
|
}
|
|
|
|
if(!_entry) {
|
|
NSError *err = nil;
|
|
NSData *bookmark = [fileUrl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&err];
|
|
if(!bookmark && err) {
|
|
ALog(@"Failed to add bookmark for URL: %@, with error: %@", url, [err localizedDescription]);
|
|
return;
|
|
}
|
|
|
|
NSLock *lock = [SandboxBroker sharedPersistentContainerLock];
|
|
NSPersistentContainer *pc = [SandboxBroker sharedPersistentContainer];
|
|
|
|
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
|
|
[lock lock];
|
|
SandboxToken *token = [NSEntityDescription insertNewObjectForEntityForName:@"SandboxToken" inManagedObjectContext:pc.viewContext];
|
|
[lock unlock];
|
|
|
|
if(token) {
|
|
token.path = [url path];
|
|
token.bookmark = bookmark;
|
|
token.folder = NO;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)requestFolderForFile:(NSURL *)fileUrl {
|
|
if(![fileUrl isFileURL]) return;
|
|
NSURL *folderUrl = [fileUrl URLByDeletingLastPathComponent];
|
|
|
|
@synchronized(self) {
|
|
SandboxEntry *_entry = nil;
|
|
|
|
for(SandboxEntry *entry in storage) {
|
|
if(entry.path && entry.isFolder && [SandboxBroker isPath:folderUrl aSubdirectoryOf:[NSURL fileURLWithPath:entry.path]]) {
|
|
_entry = entry;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!_entry) {
|
|
_entry = [self recursivePathTest:folderUrl];
|
|
}
|
|
|
|
if(!_entry) {
|
|
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
|
|
static BOOL warnedYet = NO;
|
|
|
|
if(!warnedYet) {
|
|
NSAlert *alert = [[NSAlert alloc] init];
|
|
[alert setMessageText:NSLocalizedString(@"GrantPathTitle", @"Title of file dialog for granting folder access")];
|
|
[alert setInformativeText:NSLocalizedString(@"GrantPathMessage", @"Message to new users regarding file permissions")];
|
|
[alert addButtonWithTitle:NSLocalizedString(@"GrantPathOK", @"OK button text")];
|
|
[alert addButtonWithTitle:NSLocalizedString(@"GrantPathStopWarning", @"Button to stop warnings for session")];
|
|
|
|
if([alert runModal] == NSAlertSecondButtonReturn) {
|
|
warnedYet = YES;
|
|
}
|
|
}
|
|
|
|
NSOpenPanel *panel = [NSOpenPanel openPanel];
|
|
[panel setAllowsMultipleSelection:NO];
|
|
[panel setCanChooseDirectories:YES];
|
|
[panel setCanChooseFiles:NO];
|
|
[panel setFloatingPanel:YES];
|
|
[panel setDirectoryURL:folderUrl];
|
|
[panel setTitle:NSLocalizedString(@"GrantPathTitle", @"Title of file dialog for granting folder access")];
|
|
NSInteger result = [panel runModal];
|
|
if(result == NSModalResponseOK) {
|
|
NSURL *folderUrl = [panel URL];
|
|
NSError *err = nil;
|
|
NSData *bookmark = [folderUrl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&err];
|
|
if(!bookmark && err) {
|
|
ALog(@"Failed to add bookmark for URL: %@, with error: %@", folderUrl, [err localizedDescription]);
|
|
return;
|
|
}
|
|
|
|
NSLock *lock = [SandboxBroker sharedPersistentContainerLock];
|
|
NSPersistentContainer *pc = [SandboxBroker sharedPersistentContainer];
|
|
|
|
[lock lock];
|
|
SandboxToken *token = [NSEntityDescription insertNewObjectForEntityForName:@"SandboxToken" inManagedObjectContext:pc.viewContext];
|
|
[lock unlock];
|
|
|
|
if(token) {
|
|
token.path = [folderUrl path];
|
|
token.bookmark = bookmark;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
- (const void *)beginFolderAccess:(NSURL *)fileUrl {
|
|
NSURL *folderUrl = [SandboxBroker urlWithoutFragment:fileUrl];
|
|
if(![folderUrl isFileURL]) return NULL;
|
|
|
|
SandboxEntry *_entry = nil;
|
|
|
|
NSString *sandboxPath = [folderUrl path];
|
|
|
|
@synchronized(self) {
|
|
for(SandboxEntry *entry in storage) {
|
|
if(entry.path) {
|
|
if((entry.isFolder && [SandboxBroker isPath:folderUrl aSubdirectoryOf:[NSURL fileURLWithPath:entry.path]]) ||
|
|
(!entry.isFolder && [entry.path isEqualToString:sandboxPath])) {
|
|
entry.refCount += 1;
|
|
_entry = entry;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!_entry) {
|
|
_entry = [self recursivePathTest:folderUrl];
|
|
}
|
|
|
|
if(_entry) {
|
|
[storage addObject:_entry];
|
|
|
|
if(_entry.secureUrl) {
|
|
[_entry.secureUrl startAccessingSecurityScopedResource];
|
|
}
|
|
|
|
return CFBridgingRetain(_entry);
|
|
} else {
|
|
return NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)endFolderAccess:(const void *)handle {
|
|
if(!handle) return;
|
|
SandboxEntry *entry = CFBridgingRelease(handle);
|
|
if(!entry) return;
|
|
|
|
@synchronized(self) {
|
|
if(entry.refCount > 1) {
|
|
entry.refCount -= 1;
|
|
return;
|
|
} else {
|
|
if(entry.secureUrl) {
|
|
[entry.secureUrl stopAccessingSecurityScopedResource];
|
|
entry.secureUrl = nil;
|
|
}
|
|
entry.refCount = 0;
|
|
|
|
[storage removeObject:entry];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (BOOL)areAllPathsSafe:(NSArray *)urls {
|
|
for(NSURL *url in urls) {
|
|
if(![url isFileURL]) continue;
|
|
if(![self recursivePathTest:url]) {
|
|
return NO;
|
|
}
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
@end
|