Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Security Scoped Bookmark in App Extension

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?

like image 408
duncanc4 Avatar asked Mar 11 '15 22:03

duncanc4


1 Answers

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.

like image 149
utahwithak Avatar answered Oct 19 '22 16:10

utahwithak