Run External Application from XD Plugin

#9

On Mac you can set a link in a UI control that when clicked will launch with the file: protocol and launch the registered application.

This works on Mac:

// open page on the file system
var openLocationLink = h("a", {href:""}, "Open");
openLocationLink.setAttribute("href", "file:///Users/user/documents/project/page.html");

// open page in your plugin directory
var folder = await fileSystem.getPluginFolder();
var file = await folder.getEntry("instructions.html");
openLocationLink.setAttribute("href", "file://" + file.nativePath);

// open a folder in Finder
var folder = await fileSystem.getPluginFolder();
openLocationLink.setAttribute("href", "file://" + folder.nativePath);

I haven’t had this work on Windows.

But if the file type isn’t registered you can launch the browser and use that to redirect to a page in your plugin directory to redirect to a registered application types. Apple does this with iTunes for links to podcasts or apps. A window pops up and asks if you want you want to launch the registered link.

There are security concerns sure but they have chosen to install your plugin so there is a level of trust you have granted and in that there is a trust that an application you attempt to open would have to do with your plugin.

There has been discussion around the openExternal in the past and @kerrishotts might have some updated information on this.

2 Likes
UXP to C++ Plugin Communication
Is this correct to use image fill
#10

Just to clarify: I’m not saying it should be impossible. I’m just saying this shouldn’t be as easy as just using a simple command like openExternal(). For example, there could be some sort of way where the user gets prompted if link xy should get openend.

The thing is that it would be very easy to get harmful stuff into the plugin if Adobe isn’t extremely careful here (also, there could be things like an API activating it after review by Adobe is already done), and there are always some black sheep. If Adobe isn’t extremely careful, this could potentially lead to a big damage to the image of plugins and/or even Adobe XD in general (you could even dynamically download your harmful files so you don’t have to get them through review :slightly_frowning_face:).

Therefore, while I agree there is a certain level of trust by users installing the plugin, this shouldn’t get taken “as easily” as saying “there’s some level of trust, so just do it” :wink:

#11

Hi @Velara,

probably I’m missing something to make it work (I’m on Mac)

var openLocationLink = h(“a”, {href:""}, “Open”);

what does that ‘h’ mean?

#12

@PaoloBiagini It’s some sort of micro-library for creating DOM objects (cf. https://github.com/AdobeXD/plugin-samples/tree/master/ui-hello-h).

Edit: It also seems to be known (if that’s the same, I’m not quite sure :wink:) under the name ‘hyperscript’ (see https://github.com/hyperhype/hyperscript).

1 Like
#13

thank you very much @pklaschka, I will check that as soon as possible.

1 Like
#14

Wow – lots of great discussion on here… apologies for not getting in on this earlier, but it’s been a hectic week!

Anyway – to the points here, this is something we do have to be very careful about. It’s not so much specific developers we’re worried about here (after all, I don’t think anyone on this forum would do something malicious to their users), but more in the aggregate, and we do have to keep in mind that arbitrary code comes with considerable risks and the user is rarely in an educated position to make that determination of trust. (As we can see from the browsers and their continued refinement of secure vs unsecure pages, forms, submissions, etc.)

Specifically, when it comes to running an external application from XD, we have the following issues:

  • Security risks – already mentioned in these threads
  • Privacy concerns – access to low level information could potentially breach the user’s privacy in ways our current API surface hasn’t considered.
  • Cross-platform concerns – plugins that launch external programs are that much harder to write in a cross-platform manner.
  • Permissions and UX – how do we communicate the attempt to the end user to get access (or do we at all), and if so, how do we ensure that users have the information they need to make an educated decision. (And then, what happens if the user rejects the attempt?)

None of these are necessarily insurmountable, but we have to be very careful here. Right now we are exploring what this would look like, but I’ve no additional news to share at this point precisely because there are huge boulders to address.

For now, I think the best thing the community can do is to continue submitting specific use cases – as in, why do you need to launch a particular external application (the Zepplin example is a good point)? That knowledge will hopefully lead us to a better solution that can be done in a secure, cross-platform, user-trustable manner.

3 Likes
#15

That’s about right but it is only a single function that is used in some of the plugin examples to create elements for your UI.

It’s the same as calling createElement, setting properties then adding any nested elements. It’s useful in that it may be easier to read.

In fact here is the whole function:

function h(tag, props, ...children) {
    let element = document.createElement(tag);
    if (props) {
        if (props.nodeType || typeof props !== "object") {
            children.unshift(props);
        }
        else {
            for (let name in props) {
                let value = props[name];
                if (name == "style") {
                    Object.assign(element.style, value);
                }
                else {
                    element.setAttribute(name, value);
                    element[name] = value;
                }
            }
        }
    }
    for (let child of children) {
        element.appendChild(typeof child === "object" ? child : document.createTextNode(child));
    }
    return element;
}

And here it is creating an Alert dialog:

let alertDialog =
	 h("dialog", {name:"Alert"},
		h("form", { method:"dialog", style: { width: 380 }, },
		  alertForm.header = h("h1", "Header"),
		  h("label", { class: "row" },
			 alertForm.message = h("span", { }, "Message"),
		  ),
		  h("footer",
			 h("button", { uxpVariant: "cta", type: "submit", onclick(e){ closeDialog(alertDialog) } }, "OK")
		  )
		)
	 )
2 Likes
#16

many thanks for all explanations!

I directly inserted the h function inside my main.js but, calling this function the folder doesn’t open in Finder (‘exportFolder’ being set early in the code)

function openExportFolder()
{
    var openLocationLink = h("a", {href:""}, "Open");
    openLocationLink.setAttribute("href", "file://" + exportFolder.nativePath);

    console.log(exportFolder.nativePath);
 }

though exportFolder.nativePath printed in the console is right.

#17

This is not the same as shell.openExternal() and you don’t have to use h.

You have to create a hyperlink in your plugin’s UI and then when the user clicks on it it will open your folder.

So something like this:

/**
* Shorthand for creating Elements.
* @param {*} tag The tag name of the element.
* @param {*} [props] Optional props.
* @param {*} children Child elements or strings
*/
function h(tag, props, ...children) {
    let element = document.createElement(tag);
    if (props) {
        if (props.nodeType || typeof props !== "object") {
            children.unshift(props);
        }
        else {
            for (let name in props) {
                let value = props[name];
                if (name == "style") {
                    Object.assign(element.style, value);
                }
                else {
                    element.setAttribute(name, value);
                    element[name] = value;
                }
            }
        }
    }
    for (let child of children) {
        element.appendChild(typeof child === "object" ? child : document.createTextNode(child));
    }
    return element;
}

function onsubmit() {
    //  dialog is automatically closed after submit unless you call e.preventDefault()
}


let labelWidth = 75;
let hyperlinkLabel = null;
let dialog =
    h("dialog",
        h("form", { method:"dialog", style: { width: 380 }, onsubmit },
            h("h1", "Export Complete"),

            h("label", { class: "row", marginTop:10 },
                h("span", { style: { width: labelWidth, flex: "1" } }, "Export completed successfully!")
            ),
                
            h("footer",
                h("label", { class: "row", paddingTop:0, style: {border:"0px solid #888888",  alignItems:"center"} },
                    hyperlinkLabel = h("a", { href:"",
                        style: {
                            textAlign: "center", marginTop:"0", position:"relative", top:"0", height:23, paddingTop:"4.5",
                            flex: "1" , backgroundColor:"#2D96F0", border:"0px solid #888888", verticalAlign:"middle",
                            paddingLeft:"16", paddingRight:"16", marginRight:"0", borderRadius: "12",
                            fontSize:"11px", fontWeight:"700", color:"#FFFFFF"
                        }
                    }, "Reveal in Finder")
                ),
                h("button", { uxpVariant: "cta", type: "submit", onclick(e){ onsubmit(); dialog.close(); e.preventDefault; } }, "Close")
            )
        )
    )
    
async function openAlert() {
    const fileSystem = require("uxp").storage.localFileSystem;
    document.body.appendChild(dialog);
    dialog.showModal();
    const pluginFolder = await fileSystem.getPluginFolder();
    var path = "file://" + pluginFolder.nativePath;
    hyperlinkLabel.href = path;
    hyperlinkLabel.title = path;
}

module.exports = {
    commands: {
        menuCommand: openAlert
    }
};

Manifest:

{
    "id": "UI_OPEN_IN_FINDER",
    "name": "(UI) Open in Finder",
    "version": "1.0.0",
    "host": {
        "app": "XD",
        "minVersion": "13.0.0"
    },
    "uiEntryPoints": [
        {
            "type": "menu",
            "label": "Open in Finder",
            "commandId": "menuCommand"
        }
    ]
}

OpenInFinder.xdx (2.7 KB)

#18

Using hyperlink works, thank you @Velara!
Too bad we can’t use any hover/click effect to simulate a button.

2 Likes
#19

@PaoloBiagini Did you test this on Windows? Just as a “warning”: I once tried to achieve this and it didn’t work on WIndows, so you might have to test if this really works on Windows machines (please note: I’m not saying it doesn’t work – I could also have done something wrong when I tried it – I’m just saying you’ll need to test it thoroughly :wink:) .

#20

hi @pklaschka, thank you.
unfortunately I can’t make any test on a Windows system at the moment.
Did you get a warning in the console?

#21

@PaoloBiagini I didn’t get a chance to test it, yet (I will do so later today and then write again). However, the main thing is that this wouldn’t give you any messages, but just do nothing when the button/link gets clicked…

#22

I’ve just tested it on Windows: unfortunately, clicking the button nothing happens; even no message in the console.

2 Likes
#23

any progress ? on this work around on windows

#24

Nothing new, as far as I know :frowning:

#25

I confirmed it doesn’t work on Windows at this point in time.

I have also confirmed the redirect option I suggested above appears to have been a bug that’s been fixed or required the Apple plugin to handle the redirects or have a registered handler to work.

In Windows the file protocol does not open a web page the same as it did on Mac. So I setup a web page at http://mydomain to redirect to file://localhtmlfile. I tested redirecting and file://location_a would redirect to file://location_b.

So since http protocol worked on Windows I sent the url in the hyperlink to http://mydomain/launcher.html?url=encryptedfilepathhere.

When launcher.html page was loaded I parsed the url parameter and set the windows.location.href to the unecrypted file path. Nothing happened. I tried manually setting the href in the console. Same thing no change.

I guess you can’t redirect from a http to a file location. You may be able to redirect to another registered protocol and launch applications that way. I haven’t tried.

So then, using the same launcher.html page, I added a hyperlink on the page and set the url that the users could click. Surely you should be able to go to a local page (file://) by manually clicking on a link? That didn’t do anything either.

Since that didn’t work I copied the unecrypted file path into a text input and added a copy to clipboard button right next to it. This allows the user to manually copy the url and then paste it into the browser address bar.

As I was adding copy to clipboard functionality to the web page I remembered that I already had this ability in the plugin. :neutral_face:

The only advantage in the end is that by setting up an http launcher page the user could open their browser and make it the active window and then paste the url in the browser.

Since opening file or folder or launching a registered protocol works on Mac but not on Windows (or the browser) I’m guessing it’s a Windows safety mechanism (or limitation of the underlying API).

But in the browser not redirecting to a local file from within a browser from an http address might be a security issue.

If it’s a security issue, there’s an argument here. One is that if you installed the software then that means you trust it otherwise why install it? But if it’s failing silently that’s making a decision for you. It should leave that up to you.

At the plugin level, the OS level and at the browser level, any of them could have opened up a dialog window and asked, “Do you want navigate to ‘file://…’?” A per privilege permission system, like in the browsers, at any of those domains would fix this.

@kerrishotts Is the limitation in Windows with the underlying API being used or is it a difference in security mechanisms between OSX and Windows?

#26

May be we can try deeplinking protocol and make simple application to open suck links does any one tried this i mean how plugin deeplinks works in new api

#27

I think UWP has more sandboxing when it comes to triggering URI schemes. I think a few will work (try mailto, for example), but I’m not sure arbitrary file:// access is an option. Any ability to launch external apps using a tags is pretty much luck at the moment.

Not that we’re not thinking about ways to handle this, but it’s not a simple solution. Even rendering a popup dialog means that we have to consider localization, user experience (does it pop up every time? once? etc.), and more. So we want to make sure we get this right without incurring unnecessary security and privacy risks for XD users.

1 Like
#28

This discussion has a feature request here.