Sandbox not extended when dragging URL from Finder - macos

I'm implementing drag and drop from the Finder into a NSTableView.
It works fine, except that I can not (always) access the referenced file.
The (test) validation method is as follows:
-(NSDragOperation)tableView:(NSTableView *)tableView validateDrop:(id<NSDraggingInfo>)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)dropOperation {
NSArray *newURLs = [info.draggingPasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]]
options:#{NSPasteboardURLReadingFileURLsOnlyKey:#YES}];
for (NSURL *url in newURLs) {
if (![url startAccessingSecurityScopedResource])
NSLog(#"failed");
[url stopAccessingSecurityScopedResource];
}
}
This logs failed for the URLs received.
I receive the (file reference) URLs just fine from the Finder (their path is what I expect it to be).
The documentation states that dragging NSURL objects should work, so I'm a bit surprised that this doesn't work.
What's the correct way to drag URLs from the Finder to my app?

Related

MacOS sandboxed application: access files without NSOpenPanel

In a sandboxed NSDocument-based application, any compatible document can be accessed using the NSOpenPanel, no matter where the document is saved. Without NSOpenPanel, the application can only access files in the sandbox container.
As my application manages two types of subclassed NSdocument (Text as a reader/writer and Image as a reader only), I try to implement a separate "Open Recent" menu for images. I disabled the the ordinary behaviour for them as they are opened by the user, overriding the noteNewRecentDocumentURL: (NSURL *)url method of the NSDocumentController to return NO for image urls. So that only the text documents appear in the ordinary File -> Open Recent menu (and open normally when user select them). Images are listed in a custom menu.
The problem occurs with these image urls, because the application is sandboxed: the application cannot open directly any image file listed in the dedicated menu (any reading operation returns a -54 error. This behaviour can be checked using:
[[NSFileManager defaultManager] isReadableFileAtPath:[fileURL path]]
which always returns FALSE in this situation. There is only one exception to that: when I reopen, from the dedicated Open Recent menu, a file that has been previously opened with the NSOpenPanel in the same application session, then closed: in this case isReadableFileAtPath: returns TRUE and the file can be accessed. But when application quits and restarts, recent images files cannot be accessed this way.
I identified thre solutions to deal with this problem:
Moving the image file in the sandbox container as soon as it has been accessed "legally" by the user, through the NSOpenPanel. It works, of course, but prevent the user from deciding on his own the location of his files! In the same way, duplicating the file in the sandbox is not a solution.
Creating an alias to these files in the sandbox. As I couldn't find a way to do this, I couldn't test whether this is a solution or not.
Disable the application sandboxing. But this is the worse solution as there are many reasons to use sandboxing!
Is there a 4th solution, which would authorize a read-only access to any image file, wherever it is located, without disabling the sandbox?
You can't access any file no matter what.
Also I am not sure what your second solution means, that is probably the reason you couldn't follow it. You probably wanted to refer to 'security-scoped bookmarks' and not to 'aliases' and they work very well and that is the path that you should follow.
Well Ivan's suggestion was excellent. After a few readings (less than an hour), I could implement those security-scoped bookmark. For interested people, here are the main findings.
add the feature to your sandboxed application's entitlement file
set the com.apple.security.files.bookmarks.document-scope (or the com.apple.security.files.bookmarks.app-scope, or both) key to TRUE.
Modify your document opening method (which calls the NSOpenPanel) like this:
-(void) openMyDocument:(id)sender{
// ... do your stuff
[self.panel beginWithCompletionHandler:^(NSInteger result) {
if (result == NSModalResponseOK) {
NSURL* selectedURL = [[self.panel URLs] objectAtIndex:0];
NSData *bookmark = nil;
NSError *error = nil;
bookmark = [selectedURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
includingResourceValuesForKeys:nil
relativeToURL:nil // Make it app-scoped
error:&error];
if (error) {
NSLog(#"Error while creating bookmark for URL (%#): %#", selectedURL, error);
}
NSString *access = [NSString stringWithFormat:#"%#%#", #"Access:", [selectedURL path]];
[[NSUserDefaults standardUserDefaults] setObject:bookmark forKey:access];
[[NSUserDefaults standardUserDefaults] synchronize];
// ... then open the document your way
}
}
}
Modify the method you created to read the file without using NSOpenPanel
- (void) openDocumentForScopedURL: (NSURL *) fileURL
NSString *accessKey = [NSString stringWithFormat:#"%#%#", #"Access:", [fileURL path]];
NSData *bookmarkData = [[NSUserDefaults standardUserDefaults] objectForKey:accessKey];
NSURL *bookmarkFileURL = nil;
if (bookmarkData == nil){
// no secured-scoped bookmark found, alert the user
return;
} else {
NSError *error = nil;
BOOL bookmarkDataIsStale;
bookmarkFileURL = [NSURL
URLByResolvingBookmarkData:bookmarkData
options:NSURLBookmarkResolutionWithSecurityScope
relativeToURL:nil
bookmarkDataIsStale:&bookmarkDataIsStale
error:&error];
[bookmarkFileURL startAccessingSecurityScopedResource];
}
// ... Then open your file, using bookmarkFileURL
// ... and do your stuff
// IMPORTANT. You must notify that stopped to access
[bookmarkFileURL stopAccessingSecurityScopedResource];
}

How to store preferences for a HTML-based QuickLook generator on OSX?

I used to do it with a custom PrefPane that has to be installed separately, but that's not satisfactory.
I have a HTML-based QuickLook generator, that create thumbnails and previews of some inhomogeneous content (files that have a long ASCII header, and a various number of binary extensions each of them with some header).
Inside the QL method OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options), I tried to use [NSUserDefaults standardUserDefaults], but it has no effect, and writes no preference file, probably because we are inside a bundle, and not an app.
Any idea how to achieve this? I know that some excellent QL generators do it, such as BetterZip QL. I tried to reverse engineer the BetterZip QL, but with no success.
I directly contacted the author of the BetterZip QL, and together, we came up with a solution. Here it is.
In short:
First, create a small helper app, that will be bundled inside the
generator. This app will be responsible for writing the preference
file.
Make this app register a custom URL scheme and implement the handling associated with it.
Make your HTML-based QL open a specially-formatted URL, using that custom scheme, using Javascript.
Ok, now in details.
First create a small helper app target inside your Xcode workspace/project. My QL generator was named QLFits, I chose QLFitsConfig.
By default, there is a MainMenu.xib associated that app. Keep it. It is used by the Info.plist, and it can be useful for debugging. As a matter of fact, to debug the custom URL scheme, you can add a NSWindow to that xib, and put labels which could be used to display debug messages. I found no real other way to log or display debug messages when debugging this problem.
But at the end, you have a small windowless app. There are two configuration things that this app must have.
The flag indicating that this app is an agent (see picture). It prevents the app to appear in the doc when running.
The declaration of the custom URL scheme, with an Editor role. See also the picture for an example (here qlfitsconfig)
Next, the implementation of the app needs to register the URL scheme to tell the system there is an app that is capable of opening it. Below the implementation of my "appDidFinishLaunching" method in the AppDelegate of the app.
There is three parts: The registration of the handler of the custom URL scheme. The instantiation of a NSUserDefaults object with a suite name that is shared with the QL generator. And finally, the registration of the default values of the preferences (using a .plist file bundled with the app).
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager];
[appleEventManager setEventHandler:self andSelector:#selector(handleAppleEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
[[NSUserDefaults standardUserDefaults] addSuiteNamed:suiteName];
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName];
NSString *optionsPath = [[NSBundle mainBundle] pathForResource:#"defaultOptions" ofType:#"plist"];
NSDictionary *defaultOptions = [NSDictionary dictionaryWithContentsOfFile:optionsPath];
[defaults registerDefaults:defaultOptions];
}
The suiteName variable is a static NSString with a reverse-DNS format: static NSString *suiteName = #"com.onekiloparsec.qlfitsconfig.user-defaults-suite";
Then, the app needs to act upon the triggering of the event. Hence, one must do something with the event, and use that event to store the preference. Here is the implementation. Note that the signature of the method must be precisely that one, not because we declare it so above, but because that's the only one recognised by the system.
- (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
{
NSString *URLString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
if (URLString) {
NSURL *URL = [NSURL URLWithString:URLString];
if (URL) {
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName];
for (NSString *component in [URL pathComponents]) {
if ([component containsString:#"="]) {
NSArray *keyValue = [component componentsSeparatedByString:#"="];
[defaults setObject:keyValue.lastObject forKey:keyValue.firstObject];
}
}
[defaults synchronize];
}
}
}
The basic idea is that we will provide preferences through URL parameters as key-value pairs. Hence, we transformed here that URL string into pair of preferences, that are stored as strings.
That's all for the app. To test and debug it, you need to build and run it (check with the /Utilities/Activity Monitor.app that it is running, for instance). You can type the following commands into a Terminal to see what happens:
$ open qlfitsconfig://save/option1=value1/option2=value2
And if you have kept the window with labels mentioned above, you can use them to display/debug what event your app receives.
Now, back to the QL generator. Include the config app as a "Target Dependency" in the "Build Phases" of the generator. Moreover, add a new "Copy Files" Build Phase (after the Copy Bundle Resources build phase) to copy that helper app inside the QL bundle (see picture).
Now, in the code, more precisely, inside the method preview method: OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options).
At the beginning of it, I make sure the config help app is actually registered with the Launch Services of the system. To find it, one must use the bundle identifier of the QL generator. Note especially how the URL of the app is constructed, based on where it is copied in the Build phase (the Helpers directory).
NSBundle *bundle = [NSBundle bundleWithIdentifier:#"com.onekiloparsec.QLFits3"];
NSURL *urlConfig = [NSURL fileURLWithPath:[[bundle bundlePath] stringByAppendingPathComponent:#"Contents/Helpers/QLFitsConfig.app"]];
LSRegisterURL((__bridge CFURLRef) urlConfig, true);
The last line is using legacy APIs, but I couldn't make the new ones working. This is a weakness, and one should probably find a better way at some point.
Now, if some preferences were already saved, one can access them with an instance of NSUserDefaults assuming we initialise it with the same suite name as defined in the helper app. Example:
static NSString *suiteName = #"com.onekiloparsec.qlfitsconfig.user-defaults-suite";
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName];
BOOL alwaysShowHeaders = YES;
if ([defaults stringForKey:#"alwaysShowHeaders"]) {
alwaysShowHeaders = [[defaults stringForKey:#"alwaysShowHeaders"] isEqualToString:#"1"];
}
That's it for the Obj-C code.
The last part is the Javascript code. In my QL generator (whose code can be checked on GitHub), I use a template.html file containing all the html and JS code. You can organise yourself differently here.
I first intended to change the QL preferences when checkboxes were toggled. But it appeared to not work (no events are triggered). The only way I made it work is that once my checkboxes where set, the user is requested to "save" the preferences using a button. And I save the preferences upon the clicking of that button. Here is the JS code inside my template.html
<script>
function saveConfig (a) {
a.href = "qlfitsconfig://save";
a.href += "/alwaysShowHeaders=" + (document.getElementById("alwaysShowHeadersInput").checked ? "1" : "0");
a.href += "/showSummaryInThumbnails=" + (document.getElementById("showSummaryInThumbnailsInput").checked ? "1" : "0");
return true;
}
</script>
alwaysShowHeadersInput and showSummaryInThumbnailsInput are the 'id' of my checkboxes in the HTML code. And the save button is triggering the saveConfig function.
And the button must be declared inside an a tag:
<input id="save" type="button" value="Save">
Here is what the preferences look like in my QL window:
Et voilĂ !

Dragging URLs into my app

I posted this question about dragging content from OS X Finder into an NSTableView. This all works nicely now. However, if I want to drag URLs from a browser address bar into my app, I first need to drag them to the desktop (where they appear as a .webloc file) and then drag them into my app.
Is there a way to directly drag them from the browser address bar into my app, without having to drag them to the desktop first?
I tried registering kUTTypeURL but this doesn't seem to work as dragged URLs bounce back to their origin:
[[self sourcesTableView] registerForDraggedTypes:[NSArray arrayWithObjects: (NSString*)kUTTypeFileURL, (NSString*)kUTTypeURL, nil]];
In my accepted answer to your other question, the code I provided specifically restricts the URLs that your app can accept to file URLs:
NSArray* urls = [pb readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]]
options:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
forKey: NSPasteboardURLReadingFileURLsOnlyKey]];
Note the options dictionary containing a boolean YES for the NSPasteboardURLReadingFileURLsOnlyKey.
If you want to accept any URL, just do this:
NSArray* urls = [pb readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]]
options:nil];
Or even better, you can require that you'll accept any URL as long as it is of a particular type, in this case an image:
NSArray* acceptedTypes = [NSArray arrayWithObject:(NSString*)kUTTypeImage];
NSArray* urls = [pb readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]]
options:[NSDictionary dictionaryWithObject:acceptedTypes
forKey:NSPasteboardURLReadingContentsConformToTypesKey]];

Bring all NSDocument windows to front when opened

In most systems, the default behaviour for "open a new window" is that it appears at the front. This doesn't happen in Cocoa, and I'm trying to find the "correct" way to make this standard behaviour. Most things I've tried only work for a maximum of one window.
I need to open multiple windows on startup:
(N x NSDocuments (one window each)
1 x simple NSWindowController that opens a NIB file.
Things that DON'T work:
Iterate across all the NSDocuments I want to open, and open them.
What happens? ... only the "last" one that call open on comes to the front - the rest are hidden, invisible, nowhere on the screen, until you fast-switch or use the Window menu to find them.
Code:
...documents is an array of NSPersistentDocument's, loaded from CoreData...
[NSDocumentController sharedDocumentController];
[controller openDocumentWithContentsOfURL:[documents objectAtIndex:0] display:YES error:&error];
Manually invoking "makeKeyAndOrderFront" on each window, after it's opened
What happens? nothing different. But the only way I can find to get the NSWindow instance is so horribly hacky it seems totally wrong (but is mentioend in several blogs and mailing list posts)
Code:
[NSDocumentController sharedDocumentController];
NSDocument* openedDocument = [controller openDocumentWithContentsOfURL:[documents objectAtIndex:0] display:YES error:&error];
[[[[openedDocument windowControllers] objectAtIndex:0] window] makeKeyAndOrderFront:nil];
...I know I'm doing this wrong, but I can't find out why/what to do differently :(.
Something that works, usually, but not always:
As above, but just use "showWindow" instead (I took this from the NSDocument guide).
Bizarrely, this sometimes works ... even though it's the exact code that Apple claims they're calling internally. If they're calling it internally, why does it behave different if I re-invoke it after they've already done so?
[[[openedDocument windowControllers] objectAtIndex:0] showWindow:self];
You can just open all the documents without displaying and then tell the documents to show their windows:
NSArray* docs = [NSArray arrayWithObjects:#"doc1.rtf", #"doc2.rtf",#"doc3.rtf",#"doc4.rtf",nil];
for(NSString* doc in docs)
{
NSURL* url = [NSURL fileURLWithPath:[[NSHomeDirectory() stringByAppendingPathComponent:#"Documents"] stringByAppendingPathComponent:doc]];
NSError* err;
[[NSDocumentController sharedDocumentController] openDocumentWithContentsOfURL:url display:NO error:&err];
}
[[[NSDocumentController sharedDocumentController] documents] makeObjectsPerformSelector:#selector(showWindows)];
Won't this work?
For 10.6 or greater
[[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
This often has something to do with the app itself: your other windows are behind other apps (in particular, behind Xcode!), and would have appeared with a Hide Others command.
The solution to that problem would be that after you send showWindow to all of your windows (making sure you do the key one last), you tell the app to come forward, relative to other apps.
NSApp.activateIgnoringOtherApps(true) // Swift
or
[NSApp activateIgnoringOtherApps:YES]; // Objective-C
See also: How to bring NSWindow to front and to the current Space?

Finder-style UI for NSURL bookmarkData resolution with missing file?

I am using the new NSURL bookmark data API introduced in OS X 10.6 to store an "alias" to a file system resource. When I use
+[NSURL URLByResolvingBookmarkData:options:relativeToURL:bookmarkDataIsStale:error:]
to resolve the bookmark data, I get nil if the file no longer exists. Since I am not passing NSURLBookmarkResolutionWithoutUI in the bookmark resolution options, I expected to get a dialog, like that shown by the Finder when you open an alias file that no longer resolves (i.e. a dialog to cancel, fix the alias, etc...).
Is there a way to have NSURL automatically prompt to reconnect/resolve the broken bookmark data?
The bookmark data is created via:
NSError *err
NSData *bookmarkData = [myFileURL bookmarkDataWithOptions:NSURLBookmarkCreationSuitableForBookmarkFile
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&err];
though I get the same outcome if I use 0 for the options instead of NSURLBookmarkCreationSuitableForBookmarkFile.
I attempt to resolve the same bookmarkData via:
BOOL stale;
NSError *err
NSURL *resolvedURL = [NSURL URLByResolvingBookmarkData:self.bookmarkData
options:0
relativeToURL:nil
bookmarkDataIsStale:&stale
error:&err];
Going through the dance of writing the bookmark data to a finder alias file and then resolving the URL by reading bookmark data from that file followed by the above method does not produce the desired UI result either (though opening the alias file in the Finder does produce the desired UI dialog).
No. The UI potentially involved in bookmark/alias resolution is that displayed if the target is on an unmounted fileserver which requires authentication to connect. The reconnection dialog is provided by the Finder in response to an unresolvable alias; if you want your application to have similar behavior, you will need to implement it yourself.

Resources