I need help with Security Scoped Bookmarks

Sounds like something MBS would have to change if it’s wrong? Or do you think there’s something I can do in my code?

Cool to see those functions get so much attention.

First the functions only expose the functionality Apple provides.

Second, the plugin provides CFData as strings with no encoding. Because most users I saw prefer strings over CFBinaryDataMBS and other wrapper types. And it’s convenient as functions like binary stream also take strings directly. Of course this way you should not treat them as text. If ConvertEncoding is applied, the data gets corrupted.

Third, the functions accept both folder items and path URLs. That’s just a convenience. Both end up being CFURLRefs sent to Appel’s API. I can of course quickly add more variants taking CFURLMBS objects or returning CFBinaryDataMBS if that helps.

Thank you for the response, Christian,

Any ideas about why StartAccessingSecurityScopedResource is always returning false for me?

-I’m on Mavericks
-The debug app is sandboxed
-The debug app has User Selected Read Write and Application Bookmark entitlements
-The bookmarks are created and resolved properly

That’s as far as I can get.

Check that you’re actually not able to access the file when you call CFBookmarkMBS.StartAccessingSecurityScopedResource… I think I remember it returning false regardless of success… I could be mistaken, but looking through all of my bookmark code, I removed all the checks for success and just had to assume success until the file read/write fails…

No such luck. :frowning:

I tried it with a file first, attempting to delete it. Then I tried renaming the file (that required a bookmark for the parent folder, too). Nothing worked. The folderitems that were returned from resolving the bookmarks both show isReadable and isWritable as false.

Hmm… I’m trying your code and something struck me. IIRC, You can’t call StartAccessingSecurityScopedResource until you have quit the app and relaunched it and resolved the bookmark. It’s not considered SecurityScoped until it’s from a bookmark,and it can’t be “from a bookmark” if it’s already from an open/save dialog or filedrop - I think -… Also, once you’ve called StartAccessingSecurityScopedResource and failed, the app will not be able to access anything until it’s relaunched and successfully resolves bookmarks.

I’ll put together another test. This one will have to save the data somewhere so it might be a bit more complicated but I’ll keep it as simple as possible.

I really appreciate all the help. Hopefully I’ll be able to pass it forward to someone in the future.

The app I’m currently working on uses a binary format for it’s data file (I’m not storing them in the preferences), so I just dump out the binary data.

I would start by checking the encoding of the text that the MBS function returns and comparing it with what you’re getting from the Database.

You could also try encoding the string into HEX or base64 and then decoding it when you pass it to the MBS function.

[quote=65830:@Mike Gibson]I don’t think I ever get a CFURL object with this code. The MBS functions return a string when you create a bookmark, and a folderitem when you resolve the bookmark. I then pass that folderitem to StartAccessingSecurityScopedResource, which returns a boolean (always false for me which indicates a problem).

I just assumed that the folder item returned when I resolve the bookmark would be writable once I called StartAccessing, but it’s not.[/quote]
The boolean result can be a bit misleading as it will always return false if you’re trying to access a file that the user has granted permission for (via Open/Save Panel or D&D). I tend to check and see if the file exists and I can do what I need, if not then I worry about the return of the startAccessing.

Maybe you can send an sample project to Christian and he can figure out what’s going on.

For the time being only worry about this option, the others don’t make much difference unless you’re resolving to a volume IMHO.

I would suggest sharing what I’ve currently got, but it’s not in a form to be shared at the moment. This is something I’ll do once I’m close to completing the project (thus reducing dependancies).

The EncodeBase64 suggestion from Jim seems to be working with my database, the bookmark is coming back the same length and resolving.

Also be aware that sandboxd doesn’t much like the seeing the entitlements change in the same version number of your app. I’ve managed to get some things working after changing the app identifier after changing entitlements.

I put everything into a new app to write up as simple a test as possible that still allows you to save a bookmark, quit the app, open it again, and try to resolve the bookmark without ever granting read/write access through a dialog. The app identifier and entitlements are new for this one, and I’m still having the same issues.

This project has two pushbuttons that call ssbTest. The first button calls ssbTest(False), which will prompt the user to select a file, then prompt them to select the file’s parent folder, then create a bookmark of the parent folder and save it in a database.

Quit the app and run it again.

Next use the second button which calls ssbTest(True). That prompts the user for the same file, but then resolves the parent folder from the bookmark stored in the database. It calls StartAccessing and tries to rename the file. It calls StopAccessing before finishing.

It also pops up a messagebox with the bookmark length to ensure it isn’t being corrupted in the database.

Here are the three methods needed:

Sub ssbTest(bResolve as Boolean)
  'Run first with bResolve false
  'select a file, when prompted select its parent
  'a bookmark is created from parent and saved in db
  'Quit app
  'Run again with bResolve Rrue, select the same file
  'The parent is found automatically and the bookmark retrieved
  'the bookmark is resolved
  'call startaccessing the resolved directory
  'Now we have read/write access to the file from the dlg
  'Now we have read/write access to the directory from the bookmark
  'rename the file
  Dim dlg1 as New OpenDialog
  dim dlg2 as new SelectFolderDialog
  dim s as string
  dim d, dResolved, f, fRename as FolderItem
  dim b, isStale as boolean
  dim bookmark as string
  dim optionsResolve as UInt32 = CFBookmarkMBS.kResolutionWithSecurityScope
  dim optionsCreate as UInt32 = CFBookmarkMBS.kCreationWithSecurityScope
  f = dlg1.ShowModal
  if f <> nil then
    if bResolve = false then
      dlg2.InitialDirectory = f.Parent
      d = dlg2.ShowModal 
      if d <> nil then
        bookmark = CFBookmarkMBS.CreateBookmarkData(d, optionsCreate)
        MsgBox(cstr(len(bookmark)))
        if bookmark <> "" then
          savePrefsData("bookmark" + d.absolutePath, EncodeBase64(bookmark))
          MsgBox "Bookmark saved, please restart app"
        else
          MsgBox "Bookmark failed"
        end if
      end if
    else
      d = f.Parent
      if d <> nil then
        bookmark = Base64Decode(getPrefsData("bookmark" + d.absolutepath, ""))
        MsgBox(cstr(len(bookmark)))
        dResolved = CFBookmarkMBS.ResolveBookmarkData(bookmark, optionsResolve, nil, isStale)
        if dResolved <> nil then
          b = CFBookmarkMBS.StartAccessingSecurityScopedResource(dResolved)
          fRename = d.Child("NewName.test")
          if fRename.Exists = false then
            f.Name = fRename.Name
            if fRename.Exists = true then
              MsgBox "Rename success"
            else
              MsgBox "Rename failed"
            end if
          end if
          CFBookmarkMBS.StopAccessingSecurityScopedResource(dResolved)
        end if
      end if
    end if
  end if
End Sub

[code]Sub savePrefsData(Name as string, Value as string)
Dim db As New REALSQLDatabase
dim dr as new DatabaseRecord
dim sql as string
db.DatabaseFile = SpecialFolder.ApplicationData.Child(“db.rsd”)
if db.DatabaseFile.exists = false then
If db.CreateDatabaseFile Then
db.SQLExecute(“CREATE TABLE prefs ( Name TEXT, Value TEXT);”)
db.Commit
db.close
Else
MsgBox “DB Create error”
End If
end if

if db.DatabaseFile.Exists then
if db.connect then
sql = “select Name,Value from prefs where Name=’” + Name + “’”
dim rs as RecordSet = db.SQLSelect(sql)
if not rs.eof then
rs.Edit
rs.Field(“value”).stringvalue = Value
rs.Update
rs.Close
else
dr.column(“Name”) = Name
dr.column(“Value”) = Value
db.insertRecord “prefs”, dr
end if
db.commit
db.close
end if
end if

End Sub
[/code]

[code]Function getPrefsData(Name as string, Default as string) As string
Dim db As New REALSQLDatabase
dim dr as new DatabaseRecord
dim sql as string
dim Value as string

db.DatabaseFile = SpecialFolder.ApplicationData.Child(“db.rsd”)
if db.databasefile <> nil then
if db.DatabaseFile.Exists then
if db.connect() then
sql = “select name,value from prefs where Name=’” + Name + “’”
dim rs as RecordSet = db.SQLSelect(sql)
if not rs.eof then
Value = rs.field(“value”).StringValue
else
Value = Default
end if
db.close
end if
end if
else
Value = Default
end if
return Value
End Function
[/code]

Ok, gonna have to say something is wrong with christian’s bookmark resolving code. I was able to resolve and access files using declares. reliably! Looks like the bookmark creation is fine…

If anyone is interested I could wrap it up into a handy module. (I will anyway for my own use, but it won’t be as pretty)
Maybe this could be done via MacOSLib (or easily added to it)

I do love the MBS plugins, I use them in every app and I think they’re worth every penny. However, it seems they are the cause of all my confusion. I just rewrote my test function using Mac OS Lib and it works fine. When I call StartAccessing it returns true.

I used some code that Jim McKay posted about NSURL that I found in another forum post here. Thank you, Jim, Christian, and Sam for helping me get to the answer.

For others looking for a working code sample using Mac OS Lib to create, store and resolve security scoped bookmarks, here is my test code that renames a file using SSBs that are stored in a database across sessions:

Create a project. Add Mac OS Lib. Add two buttons, one that calls ssbTest(false), one with ssbTest(true). Push the first one, select a file, select the parent folder when prompted. Quit the app. Push the second button, select the same file. It will resolve your stored bookmark and rename the file. Make sure you sandbox the app and give it entitlements for files.user-selected.read-write and files.bookmarks.app-scope.

Sub ssbTest(bResolve as Boolean)
  'Run first with bResolve false
  'select a file, when prompted select its parent
  'a bookmark is created from parent and saved in db
  'Quit app
  'Run again with bResolve True, select the same file
  'The parent is found automatically and the bookmark retrieved
  'the bookmark is resolved
  'call startaccessing the resolved directory
  'Now we have read/write access to the file from the dlg
  'Now we have read/write access to the directory from the bookmark
  'rename the file
  Dim dlg1 as New OpenDialog
  dim dlg2 as new SelectFolderDialog
  dim s as string
  dim d, f, fRename as FolderItem
  dim b, isStale as boolean
  dim bookmark as string
  dim err as boolean
  
  f = dlg1.ShowModal
  if f <> nil then
    if bResolve = false then
      dlg2.InitialDirectory = f.Parent
      d = dlg2.ShowModal
      if d <> nil then
        dim myNSURL As new NSURL(d)
        dim options As integer=NSURL.NSURLBookmarkCreationWithSecurityScope
        bookmark=EncodeBase64(myNSURL.BookmarkData(options,array(""),nil).StringValue)
        MsgBox(cstr(len(bookmark)))
        if bookmark <> "" then
          savePrefsData("bookmark" + d.absolutePath, bookmark)
          MsgBox "Bookmark saved, please restart app"
        else
          MsgBox "Bookmark failed"
        end if
      end if
    else
      d = f.Parent
      if d <> nil then
        bookmark = DecodeBase64(getPrefsData("bookmark" + d.absolutePath, ""))
        if bookmark <> "" then
          MsgBox(cstr(len(bookmark)))
          dim options as integer = 0
          options=NSURL.NSURLBookmarkResolutionWithSecurityScope
          dim myNSURL As new NSURL(new NSData(bookmark),options,nil,false)
          MsgBox "Resolved to: "+myNSURL.FilePathURL.absoluteString
          err=myNSURL.StartAccessingSecurityScopedResource
          fRename = d.Child("NewName.test")
          if fRename.Exists = false then
            f.Name = fRename.Name
            if fRename.Exists = true then
              MsgBox "Rename success"
            else
              MsgBox "Rename failed"
            end if
          end if
          err=myNSURL.StopAccessingSecurityScopedResource
        end if
      end if
    end if
  end if
End Sub

[code]Function getPrefsData(Name as string, Default as string) As string
Dim db As New REALSQLDatabase
dim dr as new DatabaseRecord
dim sql as string
dim Value as string

db.DatabaseFile = SpecialFolder.ApplicationData.Child(“db.rsd”)
if db.databasefile <> nil then
if db.DatabaseFile.Exists then
if db.connect() then
sql = “select name,value from prefs where Name=’” + Name + “’”
dim rs as RecordSet = db.SQLSelect(sql)
if not rs.eof then
Value = rs.field(“value”).StringValue
else
Value = Default
end if
db.close
end if
end if
else
Value = Default
end if
return Value
End Function
[/code]

[code]Sub savePrefsData(Name as string, Value as string)
Dim db As New REALSQLDatabase
dim dr as new DatabaseRecord
dim sql as string
db.DatabaseFile = SpecialFolder.ApplicationData.Child(“db.rsd”)
if db.DatabaseFile.exists = false then
If db.CreateDatabaseFile Then
db.SQLExecute(“CREATE TABLE prefs ( Name TEXT, Value TEXT);”)
db.Commit
db.close
Else
MsgBox “DB Create error”
End If
end if

if db.DatabaseFile.Exists then
if db.connect then
sql = “select Name,Value from prefs where Name=’” + Name + “’”
dim rs as RecordSet = db.SQLSelect(sql)
if not rs.eof then
rs.Edit
rs.Field(“value”).stringvalue = Value
rs.Update
rs.Close
else
dr.column(“Name”) = Name
dr.column(“Value”) = Value
db.insertRecord “prefs”, dr
end if
db.commit
db.close
end if
end if

End Sub
[/code]

[quote=66008:@jim mckay]Ok, gonna have to say something is wrong with christian’s bookmark resolving code. I was able to resolve and access files using declares. reliably! Looks like the bookmark creation is fine…

If anyone is interested I could wrap it up into a handy module. (I will anyway for my own use, but it won’t be as pretty)
Maybe this could be done via MacOSLib (or easily added to it)[/quote]

I agree with your assessment about the bookmark resolving code. I think we could all benefit from the module, too. This is something that really should be easy to add for MAS apps but it’s so daunting I’ve been avoiding even trying it for a long time.

Ok, I just had a breakthrough DUH moment. CFURLCreateByResolvingBookmarkData returns a CFURL object with scope embedded in it (usually). If you look at the string value of the CFURL you’ll see something like “/Home/Me/Somefile.txt?jksjfjsfkhsdkhfkhsdkfhkjsdhkf”
When the a folderitem is made from that, you lose the scope and StartAccessing/StopAccesingSecurityScopedResource FAILS!!!
I made a little module that keeps a dictionary of filepaths and the CFURL I got by resolving them. Then I pass the CFURL with scope info to the start/stop methods.
It Works!!
Your code above is doing the same thing, but by resolving the bookmark and immediately using the NSURL.
I am SOOOO happy I finally understand this!
Maybe a subclass of folderitem could work here to hold and release the CFURL…

ah. so I will add CFURL functions to my plugin.

New plugin is here:
http://www.macsw.de/plugin/Prerelease/

CreateBookmarkData(URL as CFURLMBS, options as UInt32, resourcePropertiesToInclude() as string, relativeToURL as CFURLMBS = “”) as string
CreateBookmarkData(URL as CFURLMBS, options as UInt32 = 1024, relativeToURL as CFURLMBS = “”) as string
CreateBookmarkData(URL as CFURLMBS, options as UInt32, resourcePropertiesToInclude() as string, relativeToURL as folderitem = nil) as string
CreateBookmarkData(URL as CFURLMBS, options as UInt32 = 1024, relativeToURL as folderitem = nil) as string
ResolveBookmarkDataToCFURLMBS(bookmark as string, options as UInt32, relativeToURL as folderitem, resourcePropertiesToInclude() as string, byref isStale as boolean) as CFURLMBS
ResolveBookmarkDataToCFURLMBS(bookmark as string, options as UInt32, relativeToURL as folderitem, byref isStale as boolean) as CFURLMBS
ResolveBookmarkDataToCFURLMBS(bookmark as string, options as UInt32, relativeToURL as CFURLMBS, resourcePropertiesToInclude() as string, byref isStale as boolean) as CFURLMBS
ResolveBookmarkDataToCFURLMBS(bookmark as string, options as UInt32, relativeToURL as CFURLMBS, byref isStale as boolean) as CFURLMBS
StartAccessingSecurityScopedResource(URL as CFURLMBS) as boolean
StopAccessingSecurityScopedResource(URL as CFURLMBS)

Awesome!
I also have a Folderitem subclass to share. Take a look at the project file to see how to use it.
SSBookmarks.zip
It’s free for all to use and modify!

Pretty simple:

sf=new SSFolderitem(f,doSecurityScope) //constructor using an existing folderitem, and whether to use security scope TextArea1.Text=EncodeBase64(sf.Bookmark ) //SSFolderitem.Bookmark returns a bookmark file

and

//constructor with bookmark data and whether to use security scope dim fResolved as new SSFolderitem(DecodeBase64(TextArea1.text),doSecurityScope)

Then there’s:

b = fResolved.StartAccess //request access.
and

fResolved.StopAccess //done accessing

[quote=66147:@jim mckay]Awesome!
I also have a Folderitem subclass to share. Take a look at the project file to see how to use it.
SSBookmarks.zip
It’s free for all to use and modify!

Pretty simple:

sf=new SSFolderitem(f,doSecurityScope) //constructor using an existing folderitem, and whether to use security scope TextArea1.Text=EncodeBase64(sf.Bookmark ) //SSFolderitem.Bookmark returns a bookmark file

and

//constructor with bookmark data and whether to use security scope dim fResolved as new SSFolderitem(DecodeBase64(TextArea1.text),doSecurityScope)

Then there’s:

b = fResolved.StartAccess //request access.
and

fResolved.StopAccess //done accessing [/quote]
Jim

I can see that this works on a file, but does it also work for a folder?

In other words, can I gain access to a folder and its files and sub-folders from the top access?

Mike,
Pleaase note that there is no “Mac OS Lib”. There is, however, a “macoslib” (or MacOSLib).

Also, I believe you have a coding error here:

#if TargetCocoa then

That should be:

#if TargetCocoa = true then

or, even better:

#if (TargetCocoa = true) = true then

You can never use too many of those boolean tests!

And I see that you stay away from using the not operator, that’s good. Nobody want to see things like this:

if not (fAfter.exists = true) then

(sorry, couldn’t resist)