Really vicious bug under Sierra with fonts

I have this app called Fonts Manager 3 which makes use of NSFontManagerMBS.

A customer from the MAS messaged in that the font was recognized when he dragged it onto the app, but not the second time around he launched the app.

No error while running in the IDE. Everything OK.

Once sandboxed and signed, the bug manifested. I use aliases for the fonts. Registration no longer works with an alias when the app is sandboxed.

I am sure there will not be many Xojo developers using that feature, but indeed, one cannot use an alias for a font in a sandboxed app under Sierra and High Sierra.

An app simply signed for Gatekeeper is not affected.

The issue here is you probably need security scoped bookmarks. When a user drag-and-drops or opens a file with your application, you get a security scoped URL. This works fine as long as you have your application open. The moment you quit the application, you lose the security scoped URL. Mac OS will cache the security scoped URL and associate it with your application, but that can go away when the cache is cleared. A security scoped bookmark allows you to keep a permanent copy which you can store in a database, file, or wherever you have access. You can just reload the security scoped bookmark and reconstruct the security scoped URL.

Not really a bug but expected behaviour when your app is sandboxes.
As Brendan said, you need to use Security scoped bookmarks.
You can do this with Sam’s Sandboxkit.
I use this in several sandboxed apps and works fine.

You can use secure bookmarks to keep references:

see CFBookmarkMBS module.

As others have pointed out, this is probably because once your application is quit any security associations are lost.

Personally, I would do my best to avoid using Security-Scoped Bodgemarks. While they might seem like a ‘good’ idea, they have many drawbacks and make an egg shell look solid and dependable.

The more reliable solution would be to move the font into your application’s container, then create a hard link from the relocated font back to it’s original location. Although I’ve had some trouble with this technique on High Sierra, it might be worth revisiting.

Sam’s advice is incorrect. If you want to access files outside of the app sandbox, security scoped bookmarks is the official way to do it. I have used security scoped bookmarks extensively, and I have not had a problem with them.

Using Sam’s egg analogy, if you squeeze an egg in the long direction it breaks easily, but if you squeeze it in the other direction it is very hard to break it in your hand. Security scoped bookmarks have a few moving parts to using them and you need to understand their usage and reduce the complexity by encapsulating the functionality. The bottom line is security scoped bookmarks work just fine.

How do you gentlemen explain that before Sierra the app worked perfectly with an alias created in App support (no need for security scope there) ?

I verified, the alias created under Sierra is perfectly valid if I double click on it, which means it is not blocked by the sandbox.

Anyway, I am going to solve the issue without convoluted security scope thingies : I will simply copy the font file to AppSupport. As advised by Sam, who is probably the most competent in the matter (Thank you Sam).

Exit the pesky issue with alias.

Actually you need it, but the system underneath the hood is creating it for you, but this is like a ticking time bomb if the system cache for you application is erased. In other words, it is not permanent. This is why you need to create a security scoped bookmark yourself and then save it some place.

You are free to choose the implementation you like, but moving the file and replacing it with a hard link violates user expectations of what you are doing with their file. If Apple found out during review that is what you did under the hood, they may frown upon it. Add to the reviewer notes that you are using hard links instead of security scoped bookmarks and how you are using the hard links and have it escalated to the review committee to see if they approve. If they say yes, you are good to go.

Apple has been consistently tightening the screws on security with each release and using security scoped bookmarks is the best way to handle this and future proof your code. It is the only blessed door from Apple for reaching outside the sandbox.

There is another solution that is included in Sandboxkit and thats called ‘recent items’. That could also work because it is similar to Security scoped bookmarks but much easier to implement.

One of my apps was rejected for doing this. Never looked into the ‘why is it rejected?’ and wend to use Security scoped bookmarks anyway.

Sam only implies it can be nightmare to implement Security scoped bookmarks . Which is true when using Xojo. I also struggled with it. You need a lot of work (read: time and testing) to make it work correctly.
A good shortcut is to use ‘recent items’ if your apps allows this kind of way.

I hate to say it, but this really isn’t such a big deal with Swift/Xcode. It’s simply an irritant. However, I concur with Sam that it’s downright soul destroying trying to get it working with Xojo.

There isn’t a Feedback case regarding security scoped bookmarks. Perhaps we should create one, see if Xojo can help us out a little here?

I’ll share what I use for security scoped bookmarks (written in Objective-c). Some enterprising individual out there can translate this to Xojo or make a plugin out of it.

Basically how it works is when you get a security scoped URL like from an open panel you pass it to the initWithUrl initializer. Then you call getBookmarkData to get the data and save it somewhere. The next time your application launches, get the data you previously saved and send it to the initWithBookmarkData initializer.

This has the advantage of making the handling of security scoped bookmarks object orient and hiding the details which are easy to mess up.

[code]@interface SecurityScopedBookmark : NSObject

// Initializers.

  • (instancetype) initWithUrl:(NSURL *)url;
  • (instancetype) initWithBookmarkData:(NSData *)data;

// Get the url.

  • (NSURL *) getSecurityUrl;

// Get the bookmark data.

  • (NSData *) getBookmarkData;

@end

@implementation SecurityScopedBookmark
{
// The security scoped url.
NSURL *_securityScopedUrl;

// Access flag.
NSInteger _hasSecurityAccess;

// Security scoped bookmark data.
NSData *_bookmarkData;

}

//=====================================================
#pragma mark - Initialization -
//=====================================================

  • (instancetype) initWithUrl:(NSURL *)url
    {
    // Call the super class init.
    self = [super init];

    // Did we get it?
    if (self)
    {
    // Initialize the reference.
    _securityScopedUrl = url;
    _hasSecurityAccess = -1;

      // Create the security scoped bookmark data.
      [self createBookmarkData:url];
    

    }

    // Return the object.
    return self;
    }

//-----------------------------------------------------

  • (instancetype) initWithBookmarkData:(NSData *)data
    {
    // Call the super class init.
    self = [super init];

    // Did we get it?
    if (self)
    {
    // Initialize the reference.
    _securityScopedUrl = nil;
    _hasSecurityAccess = 0;

      // Did we get data?
      if (data)
      {
      	// Is there some data?
      	if (data.length > 0)
      	{
      		// Save the bookmark data.
      		_bookmarkData = data;
    
      		// Resolve it as a security scoped bookmark.
      		_securityScopedUrl = [self resolveSecurityUrl];
      	}
      }
    

    }

    // Return the object.
    return self;
    }

//-----------------------------------------------------

  • (void) createBookmarkData:(NSURL *)url
    {
    NSError *error;

    // Create a security scoped bookmark.
    _bookmarkData = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error];

    // Was there an error?
    if (error)
    {
    // Send the error to the system log.
    [self handleError:error context:@“Error in creating bookmark data.”];
    }
    }

//-----------------------------------------------------

  • (void) dealloc
    {
    // Is there anything to do?
    if (_securityScopedUrl)
    {
    // Stop using the resource.
    if (_hasSecurityAccess > 0) [_securityScopedUrl stopAccessingSecurityScopedResource];
    }
    }

//=====================================================
#pragma mark - Resource Access -
//=====================================================

  • (NSURL *) getSecurityUrl
    {
    // Are we working with the original url?
    if (_hasSecurityAccess == -1) return _securityScopedUrl;

    // Has access been granted?
    if (_hasSecurityAccess == 0)
    {
    // Access the resource.
    if (! [_securityScopedUrl startAccessingSecurityScopedResource])
    {
    // Send the error to the system log.
    [self handleError:nil context:@“Error in starting access.”];

      	// Unable to access the url.
      	return nil;
      }
      else
      {
      	// Mark it as being used.
      	_hasSecurityAccess++;
      }
    

    }

    // Return the url.
    return _securityScopedUrl;
    }

//-----------------------------------------------------

  • (NSData *) getBookmarkData
    {
    // Return the bookmark data.
    return _bookmarkData;
    }

//=====================================================
#pragma mark - Utilities -
//=====================================================

  • (NSURL *) resolveSecurityUrl
    {
    NSURL *reslovedUrl;
    NSError *error;
    BOOL isStale;
    NSURLBookmarkResolutionOptions resolveOptions;

    // Set up the options.
    resolveOptions = NSURLBookmarkResolutionWithoutUI | NSURLBookmarkResolutionWithSecurityScope;

    // Resolve the bookmark data.
    reslovedUrl = [NSURL URLByResolvingBookmarkData:_bookmarkData
    options:resolveOptions
    relativeToURL:nil
    bookmarkDataIsStale:&isStale
    error:&error];

    // Did an error occur?
    if (error)
    {
    // Send the error to the log.
    [self handleError:error context:@“Error in resolving bookmark.”];

      // Unresolved.
      return nil;
    

    }

    // Is it stale?
    if (isStale)
    {
    // Recreate the bookmark data with the new url.
    [self createBookmarkData:reslovedUrl];

      // Resolve it again.
      reslovedUrl = [NSURL URLByResolvingBookmarkData:_bookmarkData
      										options:resolveOptions
      								  relativeToURL:nil
      							bookmarkDataIsStale:nil
      										  error:&error];
    
      // Did an error occur?
      if (error)
      {
      	// Send the error to the log.
      	[self handleError:error context:@"Error in resolving stale bookmark."];
    
      	// Unresolved.
      	return nil;
      }
    

    }

    // Return the URL.
    return reslovedUrl;
    }

//-----------------------------------------------------

  • (void) handleError:(NSError *)error context:(NSString *)context
    {
    // Do we have an error object?
    if (error)
    {
    // Send the error to the system log.
    NSLog(@“SSBM: %@ (%@)”, context, error.localizedDescription);
    }
    else
    {
    // Send the error to the system log.
    NSLog(@“SSBM: %@”, context);
    }
    }

//-----------------------------------------------------

@end[/code]

No offense, Sir, but you have not got the faintest idea of what my app is all about. Besides, an alias is not a hard link.

I am sorry, but your assumptions are of absolutely no help whatsoever. Thank you, but no thank you.

I have implemented Sam’s advice to simply copy the font, and the bug has been corrected. Case closed.

Sam suggested copying the file and making a hard link to the original location which is probably something one would not want to do because of sandboxing. If you just want to copy the file, that is fine.

“No offense…,” what has happened to kindness?

Glad that it helped mate. In regards to your earlier question about why you need it now, was your previous version Sandboxed? I know you’ve been making fonts and font apps for donkey’s years.

@Brendan Murphy Dude you need to chill, your experience has been very different than mine, which doesn’t mean that you’re right and I’m wrong. Just means that you’ve been lucky so far.

@Brendan Murphy : I appreciate your efforts, but I am afraid your solutions are far more complex than needed.
Small and simple is beautiful to me, and I appreciate minimalism.

@Sam Rowlands : Yes, the previous version was sandboxed, and sold in the MAS for a year at least.
Today I am working under High Sierra to make sure something else does not get broken in the process.

The way I solved the issue was simply to copy the font to App Support, and retain the path for next use. Works great. Thanks.