Security Scoped Bookmark in App Extension
Asked Answered
S

2

6

I am creating a TodayWidget app extension which displays information about user selected folders outside the application directory.

In my main application I am able to use powerbox via NSOpenPanel to select the folder. I can then save a security scoped bookmark to the user defaults of the app group container accessible by my TodayWidget.

The TodayWidget can read in the bookmark data, but when it calls URLByResolvingBookmarkData, it errors out with:

The file couldn’t be opened because it isn’t in the correct format.

Both my main application and the TodayWidget have the below entitlements:

  • com.apple.security.files.bookmarks.app-scope
  • com.apple.security.files.user-selected.read-only

From Apple's documentation, only the application that created the security scoped bookmark can use it. I guess these means embedded applications aren't allowed?

I've looked in to using XPC, but that doesn't really help the problem, as XPC can't use security scoped bookmark either, only a normal bookmark. As soon as the computer is restarted, the XPC process will lose access to the directories.

Really all I need is a way for the XPC process to get read access to user specified directories. Is there a way without having to relaunch my main application every restart of the computer?

Sledgehammer answered 11/3, 2015 at 22:54 Comment(1)
related: #37897618Gabriel
F
6

You have probably already solved this or moved on. But for all those that are attempting something similar I will leave this here for them. In order to access security scoped bookmarks in a different app they have to be transferred as NSData and re-resolved in the other application.

In my case I show an open dialog in the main application and then save the scoped bookmark into a shared NSUserDefaults suite. The other applications are also part of that suite and then access the container of NSData's and resolve them into usable NSURL's

Here are the relevant bits of code:

//Inside my main application's open function 
... get url from NSOpenPanel
BookmarkUtils.saveURLForOtherApplications(openPanel.URL!)


//Inside BookmarkUtils.swift
static func saveURLForOtherApplications(url:NSURL)->Bool{
    let defaults  = NSUserDefaults(suiteName: <#Suite-Name#>)!
    //I store them as a dictionary of path->encoded URL
    let sandboxedBookmarks:NSMutableDictionary
    if let prevBookmarks =  defaults.objectForKey(kSandboxKey) as? NSDictionary{
       sandboxedBookmarks = NSMutableDictionary(dictionary:prevBookmarks)
    }
    else{
        sandboxedBookmarks = NSMutableDictionary()
    }
    if let shareData = BookmarkUtils.transportDataForSecureFileURL(url){
        sandboxedBookmarks.setObject(shareData, forKey:url.path!)
        defaults.setObject(sandboxedBookmarks, forKey:kSandboxKey)
        defaults.synchronize()
        return true
    }
    else{
        println("Failed to save URL Data");
        return false
    }
}

static func transportDataForSecureFileURL(fileURL:NSURL)->NSData?{
    // use normal bookmark for encoding security-scoped URLs for transport between applications
    var error:NSError? = nil
    if let data =  fileURL.bookmarkDataWithOptions(NSURLBookmarkCreationOptions.allZeros, includingResourceValuesForKeys:nil, relativeToURL:nil, error:&error){
        return data;
    }
    else{
        println("Error creating transport data!\(error)")
        return nil
    }
}

So then in my extension (Today view in my case) I do something like this...

class TodayViewController: ...
    ...
    override func viewDidLoad() {
        super.viewDidLoad()

        var status = [MyCoolObjects]()
        for url in BookmarkUtils.sharedURLSFromApp(){
            BookmarkUtils.startAccessingSecureFileURL(url)
            status.append(statusOfURL(url))
            BookmarkUtils.stopAccessingSecureFileURL(url)
        }

    self.listViewController.contents = status
}

And the relevant bookmark looks something like:

static func sharedURLSFromApp()->[NSURL]{
    var urls = [NSURL]()
    if let defaults  = NSUserDefaults(suiteName: <#Suite-Name#>){
        if let prevBookmarks =  defaults.objectForKey(kSandboxKey) as? NSDictionary{
            for key in prevBookmarks.allKeys{
                if let transportData = prevBookmarks[key as! NSString] as? NSData{
                    if let url = secureFileURLFromTransportData(transportData){
                        urls.append(url)
                    }
                }
            }
        }
    }
    return urls
}



static func secureFileURLFromTransportData(data:NSData)->NSURL?{
    // use normal bookmark for decoding security-scoped URLs received from another application
    var bookmarkIsStale:ObjCBool = false;
    var error:NSError? = nil;
    if let fileURL = NSURL(byResolvingBookmarkData: data, options: NSURLBookmarkResolutionOptions.WithoutUI, relativeToURL: nil, bookmarkDataIsStale: &bookmarkIsStale, error: &error){
        return fileURL
    }
    else if(bookmarkIsStale){
        println("Bookmark was stale....")
    }
    else if let resolveError = error{
        println("Error resolving from transport data:\(resolveError)")
    }
    return nil
}

This solution works for me. Once you resolve the shared URL you can then create a bookmark for that application and save it for later if so desired.There may be better ways out there, hopefully Apple works on this as it is currently painful to share permissions with extensions.

Freefloating answered 1/5, 2015 at 15:49 Comment(5)
If I understand the code and explanation, you are sharing a normal bookmark between your application and today extension, not a security scoped bookmark? In transportDataForSecureFileURL the bookmark is created without the security scoped option, same goes for in secureFileURLFromTransportData, the bookmark is resolved without the security scoped option. If that is the case, what are you able to do with this normal bookmark, do you have read access to the file/folder for the bookmark?Sledgehammer
Yes you have access with the url. It is magic. Once you resolve that url if you want to access it across reboots you need to then encode the normal url to a security scoped bookmark in the secondary application. See here: devforums.apple.com/thread/142513?start=0&tstart=0 and #11361543Freefloating
Thank you - after hours of searching, this is exactly what I needed to point me in the right direction to get a file handed off from a macOS Finder Share extension to its containing app. I had a couple slight modifications (like when transforming the bookmark to data, I used URL.BookmarkCreationOptions.minimalBookmark instead of .allZeros. But passing a URL bookmark finally allows my container app to play with a file that the share extension was acting on.Adroit
Just ran into this and found a case where this solution does not work: (1) user selects a file in host app, (2) data for plain url (not security scoped) is saved to shared UserDefaults, (3) system is restarted ⚡️, (4) extension now fails to resolve the bookmark data. The restart is the problem. After that, the plain bookmark cannot be resolved anymore, neither by the host app, nor the extension.Remuneration
@Remuneration read the comments above. After transferring the URL you have to create a security scoped bookmark in the secondary app which will still be valid after restarts. That means you have to run the secondary app at least once before a restart.Stralka
C
0

Actually you don't need to startAccessingSecureFileURL and it will return fail to start. just transform bookmark data to url will gain access.

Camaraderie answered 9/12, 2016 at 8:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.