0

I have an App Store app in which the free version is not scriptable, but the premium version is. AppleScript support is one of the key differences. I know the App Store reviewers are pushing more and more towards free + in-app-purchase, which will help declutter the App Store. Fine, I'll play ball.

Now I need to do something programmatically that I've always just worked into the build.

  • Is there a way to disable AppleScript if my OSAScriptingDefinition and NSAppleScriptEnabled are set in my Info.plist? This would still allow people to open the dictionary, and maybe they'd like what they see and consider activating the upgrade. Or,

  • Is there a way to enable AppleScript after the fact? Obviously with code-signing, I can't do things like modify Info.plist, or add my SDEF to the bundle later. But maybe if the SDEF were in a non-standard location, I could load it from the bundle and tell the system about it manually.

Does the SDEF have to live in my bundle? If not, I'm not sure how to point to the user's Application Support directory in the sandbox. I've also considered xinclude an SDEF I can install after the fact, but again, the SDEF and plist require actual directory paths and not functions.

I've tried a couple of things such as attempting to set NSScriptSuiteRegistry's singleton to nil, to no effect.

Because OSAScriptingDefinition and NSAppleScriptEnabled enable "automatic" support, surely there must be a manual way to make them to effect if not in the plist, and hopefully with a public API.

Any ideas here? Thanks!

balthisar
  • 185
  • 1
  • 15

2 Answers2

1

A few points, for orientation:

  • All AppleScript commands are subclasses of NSScriptCommand
  • All AppleScript objects are represented by subclasses of NSScriptObjectSpecifier
  • The scriptability of an app is controlled by its shared instance of NSScriptSuiteRegistry

This gives you a few options. You could try, for instance, overriding NSScriptSuiteRegistry setSharedScriptSuiteRegistry: and setting it to nil for the free version. You could also write a category on NSScriptCommand and/or NSScriptObjectSpecifier that does a version check. That would give you fine-grained control: you could call it from any methods that handle a script command or returns a script object, and decide on the fly which you want to allow and which you want to block; maybe even pop up a 'Pay for Full AppleScript Access' dialog.

Ted Wrigley
  • 2,921
  • 2
  • 7
  • 17
  • I can't seem to get the application to use my `NSScriptSuiteRegistry` subclass. Even if I create the singleton `MyScriptSuiteRegistry` in the app controller's -init, the real `NSScriptSuiteRegistry` is being created and used, which is a shame, because I like this approach. – balthisar Nov 12 '19 at 19:07
  • I don't think you need to create it, just call it sometime after the app loads. In other words, let it load its script registry normally, and then call `[NSScriptSuiteRegistry setSharedScriptSuiteRegistry:nil]` if the app is in 'free version' state. – Ted Wrigley Nov 12 '19 at 21:08
  • If it's like some of Cocoa's other singletons, I have to instantiate a subclass before the default instance is created -- there's not really a legal way to override the method without subclassing. I could swizzle, but I still don't have access to the ivar that I'd have to make nil. – balthisar Nov 12 '19 at 23:25
  • It seems to me that cocoa scripting automatically creates his singleton; it would have to, otherwise cocoa scripting wouldn't work at all. Of course, I'm not sure *when* it creates it — if might not be available till after `applicationDidFinishLaunching:` — but as I remember you don't have to do anything special once you've enabled scripting support. Maybe set a breakpoint on `setSharedScriptSuiteRegistry` and see where the app calls it. – Ted Wrigley Nov 12 '19 at 23:40
  • Yeah, that's the issue, Cocoa creates it automatically. If it were, say, NSDocumentController, I could instantiate a subclass before the Cocoa-instantiated one, and it would be my NSDocumentController. Cocoa wouldn't create another. – balthisar Nov 13 '19 at 00:58
  • It turns out, this is really easy, and you pointed out the way. If you post an answer, I'll accept it: `NSAppleScriptEnabled` in the Info.plist triggers the creation of the singleton; if I leave out the plist setting and _never_ make a reference to `[NSScriptSuiteRegistry sharedScriptSuiteRegistry]`, then my SDEF is effectively ignored. As soon as I make a reference to the singleton, though, it's created, and I can use what's defined in the SDEF. This solves the problem for me. – balthisar Nov 13 '19 at 01:20
  • Not sure if it was buffering or not, but the above statement is incorrect. In any case, instantiating my subclass and overriding `loadSuitesFromBundle` to do nothing solved the issue for me. – balthisar Nov 15 '19 at 14:41
  • Yeah, that surprised me when you said it worked. I really think you need to set up your app for scriptability and then block it in runtime through one of the methods I suggested. – Ted Wrigley Nov 15 '19 at 15:13
  • Can you modify your answer to recommend the loadSuitesFromBundle? I hate to leave something unanswered, and I hate to not give credit where it's due, because ultimately your answer (in the current form) led me to where I needed to be. – balthisar Nov 24 '19 at 00:25
1

CocoaScripting is a black box and not very adaptable. Simplest (kludgy) solution would be to wait until CS has installed its Apple event handlers then call -[NSAppleEventManager setEventHandler:andSelector:forEventClass:andEventID:] to replace those with a dummy handler that always sends back a "requires in-app purchase" error. (Don’t replace the standard open, quit, etc. handlers obviously.)

foo
  • 664
  • 1
  • 4
  • 4
  • A lot of the functionality is via AppleScript properties, though, and it looks like CocoaScripting is using KVC directly to access those. – balthisar Nov 12 '19 at 20:40
  • You’ll need to replace CS’s `get` (core/getd) and `set` (core/setd) handlers as well. – foo Nov 12 '19 at 23:05