Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hs.urlevent: Fix multiple query params and pass in fullURL to hs.urlevent's bind callback #3639

Open
gvteja opened this issue May 28, 2024 · 2 comments

Comments

@gvteja
Copy link

gvteja commented May 28, 2024

Hi, I'm trying to implement something very similar to https://bargsten.org/wissen/custom-protocol-handler-macos/ to open links in the default browser, from within a Chrome PWA. I use the bind url handler to have hammerspoon open a URL in the default browser. The lua script for this handler is very simple and is as follows

hs.urlevent.bind("myevent", function(eventName, params)
  local ref = params["ref"]
  if ref ~= nil then
    -- log.i('Ref is ' .. ref) 
    hs.execute("open " .. ref)
  end
end)

I noticed two problems when using it:

  • If the URL already has a query param, then the callback does not get any param for ref.
    https://example.com/ -> works
    https://example.com?k=v -> does not work
  • To have more freedom for the callback handler, would it be possible to pass in the full URL directly to the callback, instead of only propagating the parsed query params? I see that we do have the value at
    local ok, err = xpcall(function() return callbacks[event](event, params, senderPID) end, debug.traceback)

    This way, even if issue 1 is not fixed, I can handle that case in my handler code
@Rhys-T
Copy link

Rhys-T commented Jun 17, 2024

So if I'm understanding correctly, you're opening a URL that looks something like hammerspoon://openInDefaultBrowser?ref=https://example.com?k=v? If so, that would explain why ref ends up nil - that's not a syntactically valid URL1. You need to URL-encode the original URL (or anything else you want to put into a parameter) to make sure it gets handled safely, giving you something like hammerspoon://openInDefaultBrowser?ref=https%3B%2F%2Fexample.com%3Fk%3Dv.

(Imagine you had hammerspoon://openInDefaultBrowser?ref=https://example.com?a=1&b=2. Where does ref end? How would HS know whether b=2 was a parameter to example.com or to openInDefaultBrowser?)

Irrelevant details of why the parameter gets lost

To build the params table, HS first splits the query string (the part after ?) into parameters at & characters. Then it splits each parameter at = characters - but if that doesn't result in exactly two strings (a key and a value), then it throws that parameter out entirely and continues to the next one.

NSString *query = [url query];
NSArray *queryPairs = [query componentsSeparatedByString:@"&"];
NSMutableDictionary *pairs = [NSMutableDictionary dictionary];
for (NSString *queryPair in queryPairs) {
NSArray *bits = [queryPair componentsSeparatedByString:@"="];
if ([bits count] != 2) { continue; }
NSString *key = [[bits objectAtIndex:0] stringByRemovingPercentEncoding];
NSString *value = [[bits objectAtIndex:1] stringByRemovingPercentEncoding];
[pairs setObject:value forKey:key];
}


For instance, if you're using a userscript to prefix the hrefs of links with the hammerspoon URL, you could change it to something like2:

links.forEach((l) => {
  // instead of just concatenating…
  // l.href = 'hammerspoon://openInDefaultBrowser?ref=' + l.href
  // …encode the original URL first:
  l.href = 'hammerspoon://openInDefaultBrowser?ref=' + encodeURIComponent(l.href)
})

Once the parameters are encoded properly, Hammerspoon should automatically handle decoding them before putting them into the params table.

Passing the full URL through to hammerspoon URL handlers does sound like a reasonable feature request to me, though - the http/https and mailto handlers already receive it, after all.


One other thing, not directly related to the original issue: hs.execute-ing open is not the best way to send the URL to the default browser. For one thing, there's no need to run an external command like that: you can just do hs.urlevent.openURL(ref). For another, you're currently not quoting the URL before putting it into a shell command, which means that various special characters that might appear in the URL - like ? or & - will get interpreted by the shell instead. At best this will break the command whenever those characters appear, and not open the URL - at worst it might allow anything that can open a hammerspoon URL to run arbitrary shell commands on your Mac.

It is possible to quote a string so that it can be safely passed to a shell command:

function shellArg(x)
	-- Replace every single-quote character with '\'' then wrap the whole thing in single quotes.
	-- This is the easiest way to quote an arbitrary string for a POSIX shell. (Yes, really.)
	return [[']] .. x:gsub([[']], [['\'']]) .. [[']]
end
-- then something like:
hs.execute('open ' .. shellArg(ref))

But in this case it's easier and safer to skip the shell, just use hs.urlevent.openURL(ref), and not have to worry about getting the quoting right.

Footnotes

  1. Technically it's valid - according to the relevant URL specifications, you can put pretty much whatever you want into the query string, and formatting it into discrete parameters with & is optional. But if you are trying to interpret it as parameters, they need to be encoded properly. And as you discovered, Hammerspoon currently doesn't let you look at the query string any other way.

  2. Note that the word URI in JavaScript's encodeURIComponent function is spelled with an I (uppercase i), not an L, for mostly-historical reasons.

@gvteja
Copy link
Author

gvteja commented Jun 18, 2024

Thank you so for taking the time to provide detailed explanation and education! Really appreciate it.

One other thing, not directly related to the original issue: hs.execute-ing open is not the best way to send the URL to the default browser. For one thing, there's no need to run an external command like that: you can just do hs.urlevent.openURL(ref).

Great! Thanks for the better recommendation. Adopted this now.

l.href = 'hammerspoon://openInDefaultBrowser?ref=' + encodeURIComponent(l.href)

Got it, this makes sense.

Passing the full URL through to hammerspoon URL handlers does sound like a reasonable feature request to me, though - the http/https and mailto handlers already receive it, after all.

Yup, I noticed that it was already passed to the other handlers. So created #3640 to pass it in for urlevent case as well. I'm currently using this build on my laptop and it's working well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants