Pastcuts

Creating Pastcuts 1.0

So we need to figure out what we need to hook beforehand. Back when shortcuts were shared as unsigned .shortcut files, I remember you could pull a shortcut file (which is just a plist) and modify it to have a lower WFWorkflowMinimumClientVersion, and boom import. Even if importing has been disabled, those unsigned shortcut files are still in use behind the scenes when importing shortcuts from iCloud in iOS 13/14 (you can even call the icloud API to receive the unsigned file if you want, ex links like these https://www.icloud.com/shortcuts/8d4e206d568d4aadb624b2a6191a3771 have this API https://www.icloud.com/shortcuts/api/records/8d4e206d568d4aadb624b2a6191a3771 in which contain a link to the signed shortcut for iOS 15+ and unsigned for iOS 14-), so that value must be in use somewhere. Shortcuts on iOS 13+ differs greatly behind the scenes from iOS 12-, with a lot of it being in iOS itself rather than the app - more specifically, ContentKit.framework, ActionKit.framework, and (the most important) WorkflowKit.framework. First let's understand what we need to hook. Thankfully shortcuts has a built-in action that makes this easy for us: View Content Graph of (x). Make a shortcut to get all shortcuts, choose list action, then view content graph. Run and choose a shortcut, and View Content graph should appear. Tap on the shortcut's name, then Shortcut. We can see 5 items - WFWorkflowReference, Shortcut, WFImage, WFWorkflowRecord, and NSString. We're going to modify WFWorkflowRecord. Let's take a look at the header - https://headers.cynder.me/index.php?sdk=ios/13.7&fw=/PrivateFrameworks/WorkflowKit.framework&file=%2FHeaders%2FWFWorkflowRecord.h. Oooo, NSString *minimumClientVersion - that sounds useful! Just hook minimumClientVersion in WFWorkflowRecord, and make it return NSString of 1. Boom - You just created Pastcuts 1.0!

Pastcuts 1.1.2+

Okay, so, what's wrong here? Well, in 1.2 I wanted Pastcuts to convert shortcut actions when importing. I realized that my logic was bad. Why? Well, we're hooking minimumClientVersion in EVERY shortcut loaded. This might be bad for performance / battery, and while we're not doing anything that can go wrong that much, once we get to hooking actions, if we make one mistake, not just that shortcut gets affected but EVERY shortcut gets affected, so it's pretty dangerous as well. So, let's just switch to hooking (also in WorkflowKit) WFSharedShortcut's workflowRecord instead. Let's do id rettype = %orig;. Then, [rettype setMinimumClientVersion:@"1"]; to make minimumClientVersion 1 in it. Then just return rettype, and boom, now we only hook when a shortcut is imported, making us MUCH more optimized! I was planning on waiting for Pastcuts 1.2, but I decided this optimization is enough to deserve a quick 1.1.2 release.

%hook WFSharedShortcut
-(id)workflowRecord {
  id rettype = %orig;
  [rettype setMinimumClientVersion:@"1"];
  return rettype;
}
%end

Pastcuts 1.2/1.3 (starting to get good - we're actually converting actions)!

Okay, so we can import all Shortcuts imported now. I missed that gallery shortcuts won't be under WFSharedShortcut, so we should also do the same hook for WFGalleryShortcut's workflowRecord as well. Now, even though we can import any action, iOS 13/14 doesn't have every action iOS 15 does, does it? Let's fix that.

So first off I want to thank u/gluebyte for documenting some of the backwards (in)compatibility of iOS 15. See the post here https://www.reddit.com/r/shortcuts/comments/opak23/backward_incompatibility_of_ios_15_shortcuts/. Let's start by converting the stop action back to the exit action.

We need to get the actions, obv. NSArray *origShortcutActions = (NSArray *)[rettype actions];. I also create a mutable copy of the actions as newMutableShortcutActions. Then I proceed to have a for loop to loop through all the actions in origShortcutActions. Actions codewise are very similar to Shortcut's unsigned plist format, so if you're already aware of it this should be fairly easy to understand.

WFWorkflowActionIdentifier is a string representing the action'd id. To check if the action is a stop action, we just check if the action in the loop's WFWorkflowActionIdentifier is equal to string is.workflow.actions.output.

If so, I create a mutable copy of the action to modify it. Then I change the WFWorkflowActionIdentifier to equal is.workflow.actions.exit. I then proceed to attempt to get its output and correspond to is.workflow.actions.exit's way.

If you're unaware of how an action is structured, I really recommend creating a shortcut with the action, using the Get My Shortcuts action, Choose from List, and then get file of type com.apple.plist to view the shortcuts unsigned plist. It should give you a good look at how the action works without even needing to look at rev'ing WorkflowKit/ActionKit.

(Note: That being said, when needed to, I highly recommend learning some basic reverse engineering. I made Pastcuts before I knew anything and just tried a bunch of classes hoping to eventually get it right. Don't do this. That's a ton more effort and rev'ing gives you a much better chance of ensuring what you're hooking is good to hook. I have since and it's been very helpful.)

Here's an example of this:

%hook WFSharedShortcut
-(id)workflowRecord {
  id rettype = %orig;
  [rettype setMinimumClientVersion:@"1"];
  NSArray *origShortcutActions = (NSArray *)[rettype actions];
  NSMutableArray *newMutableShortcutActions = [origShortcutActions mutableCopy];
  int shortcutActionsObjectIndex = 0;
  for (id shortcutActionsObject in origShortcutActions) {
    //this safety check is only needed if you are unaware of in actions it potentially contains more than NSDictionaries.
    if ([shortcutActionsObject isKindOfClass:[NSDictionary class]]){
      if ([[shortcutActionsObject objectForKey:@"WFWorkflowActionIdentifier"] isEqualToString:@"is.workflow.actions.output"]) {
        NSMutableDictionary *mutableShortcutActionsObject = [shortcutActionsObject mutableCopy];

        [mutableShortcutActionsObject setValue:@"is.workflow.actions.exit" forKey:@"WFWorkflowActionIdentifier"];
        if ([[[[[mutableShortcutActionsObject objectForKey:@"WFWorkflowActionParameters"] objectForKey:@"WFOutput"] objectForKey:@"Value"] objectForKey:@"attachmentsByRange"] objectForKey:@"{0, 1}"]) {
    //in iOS 15, if an Exit action has output it's converted into the Output action, so we convert it back

          NSDictionary *actionParametersWFResult = [[NSDictionary alloc] initWithObjectsAndKeys:@"placeholder", @"Value", @"WFTextTokenAttachment", @"WFSerializationType", nil];
          NSMutableDictionary *mutableActionParametersWFResult = [actionParametersWFResult mutableCopy];
          [mutableActionParametersWFResult setValue:[[[[[mutableShortcutActionsObject objectForKey:@"WFWorkflowActionParameters"] objectForKey:@"WFOutput"] objectForKey:@"Value"] objectForKey:@"attachmentsByRange"] objectForKey:@"{0, 1}"] forKey:@"Value"];
          NSDictionary *actionParameters = [[NSDictionary alloc] initWithObjectsAndKeys:@"placeholder", @"WFResult", nil];
          NSMutableDictionary *mutableActionParameters = [actionParameters mutableCopy];
          [mutableActionParameters setValue:mutableActionParametersWFResult forKey:@"WFResult"];
          [mutableShortcutActionsObject setValue:mutableActionParameters forKey:@"WFWorkflowActionParameters"];
        }
        newMutableShortcutActions[shortcutActionsObjectIndex] = [[NSDictionary alloc] initWithDictionary:mutableShortcutActionsObject];
      }
    }
  }
  return rettype;
}
%end

The (potential) future - iOS 12 support?

Remember how I claimed iOS 13 works greatly behind the scenes than iOS 12 Shortcuts? I've finally learned a bit of rev'ing and took a look back at Shortcuts 2.2.2 - and it looks like they may have a lot more in common than I originally thought.

While in iOS 12 there is no WorkflowKit system framework, Shortcuts 2.2.2 embeds frameworks that later become embedded. ActionKit.framework, ContentKit.framework, WorkflowUI.framework are all embedded. And, most interestingly, WorkflowAppKit is embedded. This isn't a 1:1 copy of WorkflowKit but it's fairly similar. A lot of code seems to e later re-used in WorkflowKit. My best guess is that they knew they couldn't rework the whole app to integrate into the OS, so rather as a stopgap they did some embedded frameworks they planned to integrate later when fully finished. iOS 12 Shortcuts is not fully made of these frameworks however, the binary does in fact have some actual code implemented, with it all being Swift. I'd honestly say iOS 12 Shortcuts has more Swift than iOS 13.

WorkflowAppKit, from what I can see does not handle gallery shortcuts, that's by Shortcuts itself which is all in swift, so adding gallery support isn't so easy. But, for what's going to matter the most, importing, is handled by WorkflowAppKit's WFSharedShortcut - hmm, where have we seen this before?

It's nearly identical to how it is in WorkflowKit in iOS 13. WFWorkflowRecord however, doesn't exist yet - instead WFWorkflow is used. So, we should just be able to hook that and (hopefully) be good.

That's not saying it'll be as neat as Pastcuts iOS 13/14 for iOS 15 shortcuts - iOS 12 to 13, unlike 14 to 15, does differ a lot in structure, with iOS 12 being more dependent on actions using input passed in from the last action, and iOS 13+ is more dependent on using the passed in magic variable of the action.

Perhaps, maybe, it could be usual to convert actions that rely on that magic variable structure in iOS 13 to cycle through all params, and place the magic variables in a Get Variable action on top of the action. iOS 13 also features a lot of acitons that you may be able to easily just substitute with a Get Variable action.