Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat: add app.getApplicationNameForProtocol API (#20399)
* Add GetApplicationNameForProtocol.

* Fix Windows implementation.

* Fix up test.

* Add documentation.

* Implement for real on Linux using xdg-mime.

Also ensure we allow blocking calls here to avoid errant DCHECKing.

* Improve docs for Linux.

* Clean up tests.

* Add a note about not relying on the precise format.

* Update docs/api/app.md

Co-Authored-By: Shelley Vohr <codebytere@github.com>

* Remove needless `done()`s from tests.

* Use vector list initialization.

* Add a simple test for isDefaultProtocolClient.

* Remove unneeded include and skip a test on Linux CI.

* We no longer differentiate between CI and non-CI test runs.
  • Loading branch information
ajmacd authored and codebytere committed Nov 7, 2019
1 parent 24939e8 commit 9b01bb0
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 20 deletions.
15 changes: 15 additions & 0 deletions docs/api/app.md
Expand Up @@ -770,6 +770,21 @@ macOS machine. Please refer to

The API uses the Windows Registry and LSCopyDefaultHandlerForURLScheme internally.

### `app.getApplicationNameForProtocol(url)`

* `url` String - a URL with the protocol name to check. Unlike the other
methods in this family, this accepts an entire URL, including `://` at a
minimum (e.g. `https://`).

Returns `String` - Name of the application handling the protocol, or an empty
string if there is no handler. For instance, if Electron is the default
handler of the URL, this could be `Electron` on Windows and Mac. However,
don't rely on the precise format which is not guaranteed to remain unchanged.
Expect a different format on Linux, possibly with a `.desktop` suffix.

This method returns the application name of the default handler for the protocol
(aka URI scheme) of a URL.

### `app.setUserTasks(tasks)` _Windows_

* `tasks` [Task[]](structures/task.md) - Array of `Task` objects
Expand Down
3 changes: 3 additions & 0 deletions shell/browser/api/atom_api_app.cc
Expand Up @@ -1450,6 +1450,9 @@ void App::BuildPrototype(v8::Isolate* isolate,
.SetMethod(
"removeAsDefaultProtocolClient",
base::BindRepeating(&Browser::RemoveAsDefaultProtocolClient, browser))
.SetMethod(
"getApplicationNameForProtocol",
base::BindRepeating(&Browser::GetApplicationNameForProtocol, browser))
.SetMethod("_setBadgeCount",
base::BindRepeating(&Browser::SetBadgeCount, browser))
.SetMethod("_getBadgeCount",
Expand Down
2 changes: 2 additions & 0 deletions shell/browser/browser.h
Expand Up @@ -92,6 +92,8 @@ class Browser : public WindowListObserver {
bool IsDefaultProtocolClient(const std::string& protocol,
gin_helper::Arguments* args);

base::string16 GetApplicationNameForProtocol(const GURL& url);

// Set/Get the badge count.
bool SetBadgeCount(int count);
int GetBadgeCount();
Expand Down
60 changes: 40 additions & 20 deletions shell/browser/browser_linux.cc
Expand Up @@ -25,6 +25,14 @@ namespace electron {
const char kXdgSettings[] = "xdg-settings";
const char kXdgSettingsDefaultSchemeHandler[] = "default-url-scheme-handler";

// The use of the ForTesting flavors is a hack workaround to avoid having to
// patch these as friends into the associated guard classes.
class LaunchXdgUtilityScopedAllowBaseSyncPrimitives
: public base::ScopedAllowBaseSyncPrimitivesForTesting {};

class GetXdgAppOutputScopedAllowBlocking
: public base::ScopedAllowBlockingForTesting {};

bool LaunchXdgUtility(const std::vector<std::string>& argv, int* exit_code) {
*exit_code = EXIT_FAILURE;
int devnull = open("/dev/null", O_RDONLY);
Expand All @@ -39,24 +47,37 @@ bool LaunchXdgUtility(const std::vector<std::string>& argv, int* exit_code) {

if (!process.IsValid())
return false;
LaunchXdgUtilityScopedAllowBaseSyncPrimitives allow_base_sync_primitives;
return process.WaitForExit(exit_code);
}

base::Optional<std::string> GetXdgAppOutput(
const std::vector<std::string>& argv) {
std::string reply;
int success_code;
GetXdgAppOutputScopedAllowBlocking allow_blocking;
bool ran_ok = base::GetAppOutputWithExitCode(base::CommandLine(argv), &reply,
&success_code);

if (!ran_ok || success_code != EXIT_SUCCESS)
return base::Optional<std::string>();

return base::make_optional(reply);
}

bool SetDefaultWebClient(const std::string& protocol) {
std::unique_ptr<base::Environment> env(base::Environment::Create());

std::vector<std::string> argv;
argv.emplace_back(kXdgSettings);
argv.emplace_back("set");
std::vector<std::string> argv = {kXdgSettings, "set"};
if (!protocol.empty()) {
argv.emplace_back(kXdgSettingsDefaultSchemeHandler);
argv.push_back(protocol);
argv.emplace_back(protocol);
}
std::string desktop_name;
if (!env->GetVar("CHROME_DESKTOP", &desktop_name)) {
return false;
}
argv.push_back(desktop_name);
argv.emplace_back(desktop_name);

int exit_code;
bool ran_ok = LaunchXdgUtility(argv, &exit_code);
Expand Down Expand Up @@ -91,27 +112,18 @@ bool Browser::IsDefaultProtocolClient(const std::string& protocol,
if (protocol.empty())
return false;

std::vector<std::string> argv;
argv.emplace_back(kXdgSettings);
argv.emplace_back("check");
argv.emplace_back(kXdgSettingsDefaultSchemeHandler);
argv.push_back(protocol);
std::string desktop_name;
if (!env->GetVar("CHROME_DESKTOP", &desktop_name))
return false;
argv.push_back(desktop_name);

std::string reply;
int success_code;
bool ran_ok = base::GetAppOutputWithExitCode(base::CommandLine(argv), &reply,
&success_code);

if (!ran_ok || success_code != EXIT_SUCCESS)
const std::vector<std::string> argv = {kXdgSettings, "check",
kXdgSettingsDefaultSchemeHandler,
protocol, desktop_name};
const auto output = GetXdgAppOutput(argv);
if (!output)
return false;

// Allow any reply that starts with "yes".
return base::StartsWith(reply, "yes", base::CompareCase::SENSITIVE) ? true
: false;
return base::StartsWith(output.value(), "yes", base::CompareCase::SENSITIVE);
}

// Todo implement
Expand All @@ -120,6 +132,14 @@ bool Browser::RemoveAsDefaultProtocolClient(const std::string& protocol,
return false;
}

base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) {
const std::vector<std::string> argv = {
"xdg-mime", "query", "default",
std::string("x-scheme-handler/") + url.scheme()};

return base::ASCIIToUTF16(GetXdgAppOutput(argv).value_or(std::string()));
}

bool Browser::SetBadgeCount(int count) {
if (IsUnityRunning()) {
unity::SetDownloadCount(count);
Expand Down
16 changes: 16 additions & 0 deletions shell/browser/browser_mac.mm
Expand Up @@ -132,6 +132,22 @@
return result == NSOrderedSame;
}

base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) {
NSURL* ns_url = [NSURL
URLWithString:base::SysUTF8ToNSString(url.possibly_invalid_spec())];
base::ScopedCFTypeRef<CFErrorRef> out_err;
base::ScopedCFTypeRef<CFURLRef> openingApp(LSCopyDefaultApplicationURLForURL(
(CFURLRef)ns_url, kLSRolesAll, out_err.InitializeInto()));
if (out_err) {
// likely kLSApplicationNotFoundErr
return base::string16();
}
NSString* appPath = [base::mac::CFToNSCast(openingApp.get()) path];
NSString* appDisplayName =
[[NSFileManager defaultManager] displayNameAtPath:appPath];
return base::SysNSStringToUTF16(appDisplayName);
}

void Browser::SetAppUserModelID(const base::string16& name) {}

bool Browser::SetBadgeCount(int count) {
Expand Down
73 changes: 73 additions & 0 deletions shell/browser/browser_win.cc
Expand Up @@ -71,6 +71,68 @@ bool GetProtocolLaunchPath(gin_helper::Arguments* args, base::string16* exe) {
return true;
}

// Windows treats a given scheme as an Internet scheme only if its registry
// entry has a "URL Protocol" key. Check this, otherwise we allow ProgIDs to be
// used as custom protocols which leads to security bugs.
bool IsValidCustomProtocol(const base::string16& scheme) {
if (scheme.empty())
return false;
base::win::RegKey cmd_key(HKEY_CLASSES_ROOT, scheme.c_str(), KEY_QUERY_VALUE);
return cmd_key.Valid() && cmd_key.HasValue(L"URL Protocol");
}

// Windows 8 introduced a new protocol->executable binding system which cannot
// be retrieved in the HKCR registry subkey method implemented below. We call
// AssocQueryString with the new Win8-only flag ASSOCF_IS_PROTOCOL instead.
base::string16 GetAppForProtocolUsingAssocQuery(const GURL& url) {
const base::string16 url_scheme = base::ASCIIToUTF16(url.scheme());
if (!IsValidCustomProtocol(url_scheme))
return base::string16();

// Query AssocQueryString for a human-readable description of the program
// that will be invoked given the provided URL spec. This is used only to
// populate the external protocol dialog box the user sees when invoking
// an unknown external protocol.
wchar_t out_buffer[1024];
DWORD buffer_size = base::size(out_buffer);
HRESULT hr =
AssocQueryString(ASSOCF_IS_PROTOCOL, ASSOCSTR_FRIENDLYAPPNAME,
url_scheme.c_str(), NULL, out_buffer, &buffer_size);
if (FAILED(hr)) {
DLOG(WARNING) << "AssocQueryString failed!";
return base::string16();
}
return base::string16(out_buffer);
}

base::string16 GetAppForProtocolUsingRegistry(const GURL& url) {
const base::string16 url_scheme = base::ASCIIToUTF16(url.scheme());
if (!IsValidCustomProtocol(url_scheme))
return base::string16();

// First, try and extract the application's display name.
base::string16 command_to_launch;
base::win::RegKey cmd_key_name(HKEY_CLASSES_ROOT, url_scheme.c_str(),
KEY_READ);
if (cmd_key_name.ReadValue(NULL, &command_to_launch) == ERROR_SUCCESS &&
!command_to_launch.empty()) {
return command_to_launch;
}

// Otherwise, parse the command line in the registry, and return the basename
// of the program path if it exists.
const base::string16 cmd_key_path = url_scheme + L"\\shell\\open\\command";
base::win::RegKey cmd_key_exe(HKEY_CLASSES_ROOT, cmd_key_path.c_str(),
KEY_READ);
if (cmd_key_exe.ReadValue(NULL, &command_to_launch) == ERROR_SUCCESS) {
base::CommandLine command_line(
base::CommandLine::FromString(command_to_launch));
return command_line.GetProgram().BaseName().value();
}

return base::string16();
}

bool FormatCommandLineString(base::string16* exe,
const std::vector<base::string16>& launch_args) {
if (exe->empty() && !GetProcessExecPath(exe)) {
Expand Down Expand Up @@ -293,6 +355,17 @@ bool Browser::IsDefaultProtocolClient(const std::string& protocol,
return keyVal == exe;
}

base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) {
// Windows 8 or above has a new protocol association query.
if (base::win::GetVersion() >= base::win::Version::WIN8) {
base::string16 application_name = GetAppForProtocolUsingAssocQuery(url);
if (!application_name.empty())
return application_name;
}

return GetAppForProtocolUsingRegistry(url);
}

bool Browser::SetBadgeCount(int count) {
return false;
}
Expand Down
34 changes: 34 additions & 0 deletions spec-main/api-app-spec.ts
Expand Up @@ -846,6 +846,40 @@ describe('app module', () => {
})
})
})

it('sets the default client such that getApplicationNameForProtocol returns Electron', () => {
app.setAsDefaultProtocolClient(protocol)
expect(app.getApplicationNameForProtocol(`${protocol}://`)).to.equal('Electron')
})
})

describe('getApplicationNameForProtocol()', () => {
it('returns application names for common protocols', function () {
// We can't expect particular app names here, but these protocols should
// at least have _something_ registered. Except on our Linux CI
// environment apparently.
if (process.platform === 'linux') {
this.skip()
}

const protocols = [
'http://',
'https://'
]
protocols.forEach((protocol) => {
expect(app.getApplicationNameForProtocol(protocol)).to.not.equal('')
})
})

it('returns an empty string for a bogus protocol', () => {
expect(app.getApplicationNameForProtocol('bogus-protocol://')).to.equal('')
})
})

describe('isDefaultProtocolClient()', () => {
it('returns false for a bogus protocol', () => {
expect(app.isDefaultProtocolClient('bogus-protocol://')).to.equal(false)
})
})

describe('app launch through uri', () => {
Expand Down

0 comments on commit 9b01bb0

Please sign in to comment.