How to run AppleScript from inside a SwiftUI View? - macos

I'm kind of getting some more understanding with basic SwiftUI but now I wanted to extend my application to actually do some stuff regarding system components for me. Basically I want to run an AppleScript from inside my app which creates a signature in Mac Mail. The script itself is pretty simple:
// Generates a signature in Mac Mail
tell application "Mail"
set newSig to make new signature with properties {name:"The Signature Name"}
set content of newSig to "My New Signature Content"
end tell
I have created a view with a button which should execute the script:
import SwiftUI
struct SomeView: View {
#State var status = ""
var body: some View {
VStack(alignment: .center) {
Button(action: {
let source = """
tell application \"Mail\"
set newSig to make new signature with properties {name: \"The Signature Name\"}
set content of newSig to \"My New Signature Content\"
end tell
"""
var error: NSDictionary?
if let scriptObject = NSAppleScript(source: source) {
if let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error) {
self.status = output.stringValue ?? "some default"
} else if (error != nil) {
self.status = "error: \(error)"
}
}
}) {
Text("Generate").font(.callout)
}
Text("\(self.status)")
}
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
SomeView()
}
}
Everything executes but I get the error
AppleScript run error = {
NSAppleScriptErrorAppName = Mail;
NSAppleScriptErrorBriefMessage = "Application isn\U2019t running.";
NSAppleScriptErrorMessage = "Mail got an error: Application isn\U2019t running.";
NSAppleScriptErrorNumber = "-600";
NSAppleScriptErrorRange = "NSRange: {0, 0}";
}
After some research i found this article which describes the problem quite well. Apparently this error is because the app is running in a sandbox and within the sandbox Mail is indeed not running. I kind of get Apple's idea not to let applications do whatever they want without the user's consent...
Anyway, unfortunately this article describes the solution using Objective C and this is something I have even less of a clue than SwiftUI.
Can anybody tell me how to run (or copy my script.scpt file to the accessible library folder and the run) a script from within a SwitUI View? This would be so much help!
Thanks!!!

I have played around quite a bit with this manner and after having numerous discussions and trial & errors on that subject, I want to present 2 solutions (until now it seems that these are the only possible solutions for a sandboxed application).
First of all, if you don't consider to distribute your app on the App Store, you can forget about the following, since you just have to deactivate the Sandbox and you are basically free to do whatever you want!
In case your planning to distribute a Sandboxed App, the only way of running a script and interacting with other apps on the user's system is to run a script file from the Application Script folder. This folder is a designated folder in the user library structure: /Users/thisUser/Library/Application Scripts/com.developerName.appName/. Whatever script goes in here you have the right to run from your application appName.
You basically have two options to get your script file into that folder:
Option 1 - Install the Script File
This is (in my opinion) clearly the option you should go for if your script is static (does not require any additional user data from your application). All you have to do is
Select the project (1)
click on Build Phases (2)
add the Copy Files setting (if not already present)
choose the script file location in your app (singing might be a good option when distributing via AppStore)
add the Application Script folder of your application to the destination. Therefore, choose Absolute Path and enter Users/$USER/Library/Application Scripts/$PRODUCT_BUNDLE_IDENTIFIER in the Path field.
You can also select the Copy only when installing option if your script is entirely static but in case you have changes on the script when the application is closed and reopened and you want to update the script, leave this option blank (I have not tested this!).
After this is done you can execute the script via NSUserScriptTask. A more detailed description of how you could implement this is given here.
Option 2 - Giving access to the folder and copy the file on demand
This is certainly the solution when your script file updates dynamically according to e.g. user inputs. Unfortunately, this is a bit of a hassle and does not have (in my opinion) satisfying solutions. To do so, you will have to grant access to the folder (in this case Application Scripts/). This is done via NSOpenPanel. A really good tutorial how to implement this is given here.
Per default you will have read permission to that folder only. Since you are trying to copy a file into that folder you will have to change that to read/write in your Capabilities as well.
I hope this will help some people to "shine some light into the dark"! For me this was quite a bit of a journey since there is only very little information out there.

Related

BrowserComponent "onDownloadStart" event

I'm trying to build a simple web browser using BrowserComponent. Are there any options to check when a user clicks on a "download" button (how to detect download)? When developing directly with Android, there is an event "onDownloadStart". Is there something similar?
Thanks
We don't support that behavior as it isn't portable. Androids download facility stores files "elsewhere" and requires some additional permissions. Instead you can intercept the URL navigation logic and decide whether you want to perform a download or not, you can then use something like the Util download methods to perform the actual file download.
e.g.:
bc.addBrowserNavigationCallback(url -> {
// *** WARNING: this code runs off the EDT and must not block!!!! ***
if(shouldIDownloadThisURL(url) {
String file = getStorageFileNameForUrl(url);
Util.downloadUrlToStorageInBackground(url, file,
ev -> fileDownloadCompleted(file));
return false;
}
return true;
});

Using OS X JavaScript for Automation (JXA) to listen to "open location" Apple Event

I am using OS X JavaScript for Automation (JXA), and I want to be able to capture the "open location" Apple Event.
Per http://www.macosxautomation.com/applescript/linktrigger/ , I have setup a customer URL handler. How do I do the equivalent of
on open location this_URL
...
end open location
with JXA? I tried all of the following, but could not get any of them to execute:
app = Application.currentApplication();
app.includeStandardAdditions = true;
function run() {
app.displayDialog(JSON.stringify(arguments));
}
function openLocation() {
app.displayDialog(JSON.stringify(arguments));
}
function openDocuments() {
app.displayDialog(JSON.stringify(arguments));
}
function onOpenLocation() {
app.displayDialog(JSON.stringify(arguments));
}
Apple's JXA docs (https://developer.apple.com/library/mac/releasenotes/InterapplicationCommunication/RN-JavaScriptForAutomation/Articles/OSX10-10.html#//apple_ref/doc/uid/TP40014508-CH109-SW15 ) don't discuss how to handle the open location event. My script would get opened because I could get an alert to display if I added it outside the functions. I just couldn't get a function to execute and be passed in the URL.
I am working around this by having a AppleScript handler that that then invokes my JXA code, but this is certainly less than ideal.
I also didn't see anything in the JXA Cookbook (https://github.com/dtinth/JXA-Cookbook ) about this.
This question is old, but to any present or future visitors from Google, the correct function name to use is GURLGURL (no, I'm not joking). Your function signature should look something like this:
#!/usr/bin/env osascript -l JavaScript
function GURLGURL(url) {
const App = Application.currentApplication();
App.includeStandardAdditions = true;
App.doShellScript(`echo ${url} > ~/Downloads/url-arg.txt`);
return true;
}
I discovered this after reading through this archived developer document, and then probing my JXA script's applet process with the Console app. The message that gets dispatched is GURLGURL as in get URL.
Cheers to Apple for curating the worst developer experience for the best version of AppleScript.
As you suggest, the trick (for the moment) seems to be to pass control immediately to a second (JavaScript for Automation) script in the same bundle.
on open location strURL
run script (path to resource "jsHandler.scpt" in directory "Scripts") with parameters {{|URL|:strURL}}
end open location

Read and Write access for FinderSync extension in a sandboxed environment

The scenario
The user right-clicks a directory in Finder and finds a custom MenuItem. Clicking that Item will tell my app to open up a window where the user can do his work. When he is finished files need to be written to to the folder he selected by right-clicking.
The Problem
I got everything to work now, but the very last part. The extension can't write to the selected folder.
The user selecting the folder he wants to interact with seems to not be part of the Powerbox which - how I understand it - is only activated with openPanel and savePanel. How do I get the rights to interact with the folder that the user selected through my menu item? I can't find a reference to any possible solution to that problem in the developer library. Not in the sandboxing guide not in the extensions guide.
The possibility to add custom menu items would be rather useless if there was no way to use the selected files and folders so I'm sure there must be a way for accessing them.
Maybe the way I'm trying to write is wrong. My main app writes a temporary file into a shared group folder. After that it sends a notification that the extension listens to:
func copyFile(notification:NSNotification)
{
NSLog("write file")
if let target = tmpTarget
{
let secureContainer = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.de.enie.Nu")
let contents = NSFileManager.defaultManager().contentsOfDirectoryAtURL(secureContainer!, includingPropertiesForKeys: nil, options: NSDirectoryEnumerationOptions.SkipsHiddenFiles | NSDirectoryEnumerationOptions.SkipsPackageDescendants | NSDirectoryEnumerationOptions.SkipsSubdirectoryDescendants , error: nil)
for content in contents as! [NSURL]
{
NSLog("tmp data: \(content.path!)")
if content.lastPathComponent!.stringByDeletingPathExtension == "SharedData"
{
NSLog("found shared file")
NSFileManager.defaultManager().copyItemAtURL(content, toURL: target.URLByAppendingPathComponent(content.lastPathComponent!), error: nil)
NSFileManager.defaultManager().removeItemAtURL(content, error: nil)
}
}
tmpTarget = nil
}
}
The attempt to write the file results in these console notifications:
open on /Users//Desktop/SharedData.png: Operation not permitted
deny file-write-create /Users//Desktop/SharedData.png
Any ideas how to get access to user selected folders are appreciated.
Update
I just reassured that I did no mistakes in any way. While I'm allowed to access folders via the NSOpenPanel (which means entitlements should be right) I can not create folders in / or even bookmark the target url of my default FIFinderSyncController.
Even though the Finder Sync App Extension is granted "User Selected File" Sandbox File Access, the selectedItemURLs() files accessed by the user via Finder Sync App Extension right-click seemingly do not count as being "user-selected". The sandbox thus denies your Finder Sync app access to those files.
As the other answer notes, the only way around this is to use a temporary entitlement for wider file access. Or to use a Powerbox NSOpenPanel to have the user select a containing folder, and use that security-scoped bookmark to access the sandboxed files.
Please duplicate my Apple bug report requesting this behavior be allowed:
Finder Sync App Extension selectedItemURLs() should receive "User Selected File" Sandbox file access.
rdar://42874694
https://openradar.appspot.com/radar?id=5063363058991104
You should be able to write to the selected file if you grant the entitlement: com.apple.security.files.user-selected.read-write

Registering a protocol handler in Windows 8

I'm trying to register my application that will handle opening of links, e,g, http://stackoverflow.com. I need to do this explicitly for Windows 8, I have itworking in earlier versions of Windows. According to MSDN this has changed in Win8.
I've been through the Default Programs page on MSDN (msdn.microsoft.com/en-us/library/cc144154.aspx) page on MSDN. It provides a great walkthrough on handling file types but is light on details for protocols. Registering an Application to a URL Protocol only goes over the steps involved in setting up a new protocol, but not how to correctly add a new handler to an existing protocol.
I've also tried the registry settings outlined in other SO posts.
One more thing, the application is not a Metro/Windows Store App, so adding an entry in the manifest won't work for me.
You were on the right track with the Default Programs web page - in fact, it's my reference for this post.
The following adapts their example:
First, you need a ProgID in HKLM\SOFTWARE\Classes that dictates how to handle any input given to it (yours may already exist):
HKLM\SOFTWARE\Classes
MyApp.ProtocolHandler //this is the ProgID, subkeys are its properties
(Default) = My Protocol //name of any type passed to this
DefaultIcon
(Default) = %ProgramFiles%\MyApp\MyApp.exe, 0 //for example
shell
open
command
(Default) = %ProgramFiles%\MyApp\MyApp.exe %1 //for example
Then fill the registry with DefaultProgram info inside a Capabilities key:
HKLM\SOFTWARE\MyApp
Capabilities
ApplicationDescription
URLAssociations
myprotocol = MyApp.ProtocolHandler //Associated with your ProgID
Finally, register your application's capabilities with DefaultPrograms:
HKLM\SOFTWARE
RegisteredApplications
MyApplication = HKLM\SOFTWARE\MyApp\Capabilities
Now all "myprotocol:" links should trigger %ProgramFiles%\MyApp\MyApp.exe %1.
Side note since this is a top answer found when googling this kind of an issue:
Make sure the path in the shell command open is a proper path to your application.
I spent an entire day debugging issue that seemed only to affect Chrome and Edge on Windows 10. They never triggered the protocol handler while Firefox did.
What was the issue? The path to the .bat file used mixed
\ and / slashes.
Using only proper \ slashes in the path made Edge & Chrome suddenly able to pick up the request.
LaunchUriAsync(Uri)
Starts the default app associated with the URI scheme name for the specified URI.
You can allow the user to specify, in this case.
http://msdn.microsoft.com/library/windows/apps/Hh701476
// Create the URI to launch from a string.
var uri = new Uri(uriToLaunch);
// Calulcate the position for the Open With dialog.
// An alternative to using the point is to set the rect of the UI element that triggered the launch.
Point openWithPosition = GetOpenWithPosition(LaunchUriOpenWithButton);
// Next, configure the Open With dialog.
// Here is where you choose the program.
var options = new Windows.System.LauncherOptions();
options.DisplayApplicationPicker = true;
options.UI.InvocationPoint = openWithPosition;
options.UI.PreferredPlacement = Windows.UI.Popups.Placement.Below;
// Launch the URI.
bool success = await Windows.System.Launcher.LaunchUriAsync(uri, options);
if (success)
{
// URI launched: uri.AbsoluteUri
}
else
{
// URI launch failed. uri.AbsoluteUri
}

Using Pgp.exe from a MVC application

I've been tasked with converting a legacy application to mvc. The app used pgp.exe to pgp sign user input and send it as an email. The application works locally and on a test server but won't run on a live server. I've had to jump though hoops such as running a specified user in the application pool so that we can set the keys in the users profile BUT it worked.
For some reason on the live server which is windows 2003 IIS 6 and identical to the testing server it fails. The problem is pgp.exe just wont seem to sign and create files the message I get from the console out put is. "Signature Error"?? When I put the command into a shell window logged in as the app pool user it runs no problem (after a fight with some permissions) but when running through the mvc application/IIS server it fails. The code used to call the process is below.
var startInfo = new ProcessStartInfo();
startInfo.FileName = _pgpexeLocation;
//startInfo.FileName = "pgp.exe";
startInfo.Arguments = string.Format("-sta \"{0}\" -u keyuser-z keypass +COMPATIBLE +FORCE", _tempFilePath);
startInfo.UseShellExecute = false;
startInfo.RedirectStandardOutput = true;
startInfo.LoadUserProfile = true;
using (Process exeProcess = Process.Start(startInfo))
{
// TODO: set limit to wait for and deal with exit
exeProcess.WaitForExit();
//var stringItem = exeProcess.StandardOutput.ReadToEnd();
//Logger.Info(stringItem);
}
I'm clutching at straws here hoping somebody has done something similar before and can help. I'm guessing it's key location or file location not being picked up somewhere but not sure what else to try?
Turns out that even though the app pool was using a specific user and I'd set the keys up in that users appdata folder when I checked the underlying process call it was actually trying to pick the keys up from the Default User profile. Not sure if this was an IIS config or something similar but moving the keys and pgp folder to this appdata instead worked?

Resources