Bypassing the Execute JS in Browser Actions

The various Execute JS in Browser actions in KM 10 and below:

  • Execute a JavaScript in Front Browser
  • Execute a JavaScript in Google Chrome
  • Execute a JavaScript in Safari

Provide a lot of convenience (they make all your Keyboard Maestro variable values available to the JavaScript code), but that convenience comes with a caveat, which is summarised on the Wiki Page:

actions:Execute a JavaScript in Browser [Keyboard Maestro Wiki]

:warning: Note that because your variables are passed to the web page, any other scripts running on that web page will have access them, and this could be a privacy issue if the information is misused by the web page.

The privacy issues with these actions can be mitigated, to some extent, by observing some best practices when using them (essentially deleting the document.kmvar reference as soon as you no longer need it, rather than letting it persist, still legible to other scripts, after the macro has finished.)

For a summary of these best practices, see the thread:

Execute JavaScript in Browser Actions – Best Practices


In contexts where you want stronger security, however, you can bypass those actions entirely, and send your JS to a browser directly from a different type of action: Execute a JavaScript for Automation

The basic pattern differs slightly between Safari and the Chrome (or Chrome-based) browsers.

Simplest pattern (for browser JS one-liners):

Expand disclosure triangle for Safari version
(() => {
    "use strict";

    // ----------------- SAFARI VERSION ------------------

    // Example of some JS code to run in Safari
    const javaScriptCode = "window.location.href";

    const
        safari = Application("Safari"),
        windows = safari.windows;

    return windows.length > 0 ? (
        safari.doJavaScript(
            javaScriptCode, {
                in: windows.at(0).currentTab
            }
        )
    ) : "No windows open in Safari";
})();
Expand disclosure triangle for Chrome and Chrome-based version
(() => {
    "use strict";

    // ----------------- CHROME VERSION ------------------

    // Example of some JS code to run in Chrome
    const javaScriptCode = "window.location.href";

    const
        chrome = Application("Google Chrome"),
        windows = chrome.windows;

    return windows.length > 0 ? (
        windows.at(0).tabs.at(0).execute({
            javascript: javaScriptCode
        })
    ) : "No windows open in Chrome";
})();

For multi-line JavaScripts, you can

  1. Wrap your code in a function
  2. convert that function to a JS code string at run-time, and pass it to the browser.

For example, if we wanted to capture all the links on a page, in a Markdown format, we might write a Safari function like:

// Example of a JS function to evaluate in Safari
const SafariJS = () => {
    const links = document.querySelectorAll("a");

    return Array.from(links)
        .map(link => {
            const
                label = link.textContent.trim(),
                url = link.href;

            return `[${label}](${url})`;
        })
        .join("\n\n");
};

and the code itself would have exactly the same contents for Chrome browsers:

// Example of a JS function to evaluate in Chrome
const ChromeJS = () => {
    const links = document.querySelectorAll("a");

    return Array.from(links)
        .map(link => {
            const
                label = link.textContent.trim(),
                url = link.href;

            return `[${label}](${url})`;
        })
        .join("\n\n");
};

JavaScript for Automation (JXA) can pass functions like these to browsers if we:

  1. Convert the function to a code string,
  2. and wrap the code in ( ... code here... )() bracketing for immediate evaluation

Fortunately, a JavaScript function is easily converted to a string. Given the function definitions above, we can write things like:

SafariJS.toString()

ChromeJS.toString()

or, even more conveniently, use the JS template format, which makes it easy to wrap the stringified function in the bracketing that we need:

`(${SafariJS})()`

`(${ChromeJS})()`

The full working examples look like this:

Expand disclosure triangle to view Safari example
(() => {
    "use strict";

    // ------------------ JS FOR SAFARI ------------------

    // Example of a JS function to evaluate in Safari
    const SafariJS = () => {
        const links = document.querySelectorAll("a");

        return Array.from(links)
            .map(link => {
                const
                    label = link.textContent.trim(),
                    url = link.href;

                return `[${label}](${url})`;
            })
            .join("\n\n");
    };

    // ------------ JAVASCRIPT FOR AUTOMATION ------------
    const
        safari = Application("Safari"),
        windows = safari.windows;

    return windows.length > 0 ? (
        safari.doJavaScript(
            `(${SafariJS})()`, {
                in: windows.at(0).currentTab
            }
        )
    ) : "No windows open in Safari";
})();
Expand disclosure triangle to view Chrome example
(() => {
    "use strict";

    // -------------- JAVASCRIPT FOR CHROME --------------

    // Example of a JS function to evaluate in Chrome
    const ChromeJS = () => {
        const links = document.querySelectorAll("a");

        return Array.from(links)
            .map(link => {
                const
                    label = link.textContent.trim(),
                    url = link.href;

                return `[${label}](${url})`;
            })
            .join("\n\n");
    };

    // ------------ JAVASCRIPT FOR AUTOMATION ------------
    const
        chrome = Application("Google Chrome"),
        windows = chrome.windows;

    return windows.length > 0 ? (
        windows.at(0).tabs.at(0).execute({
            javascript: `(${ChromeJS})()`
        })
    ) : "No windows open in Chrome";
})();
3 Likes