From 66566836dea23047399b4ecd0a37a81eafe82f5b Mon Sep 17 00:00:00 2001 From: Andrew MacDonald Date: Wed, 6 Nov 2019 17:50:33 -0800 Subject: [PATCH] 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 * 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. --- docs/api/app.md | 15 +++++++ shell/browser/api/atom_api_app.cc | 3 ++ shell/browser/browser.h | 2 + shell/browser/browser_linux.cc | 60 ++++++++++++++++--------- shell/browser/browser_mac.mm | 16 +++++++ shell/browser/browser_win.cc | 73 +++++++++++++++++++++++++++++++ spec-main/api-app-spec.ts | 34 ++++++++++++++ 7 files changed, 183 insertions(+), 20 deletions(-) diff --git a/docs/api/app.md b/docs/api/app.md index 7252b5b355704..d47928025f92a 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -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 diff --git a/shell/browser/api/atom_api_app.cc b/shell/browser/api/atom_api_app.cc index d5078fa3dd597..d90f489664eda 100644 --- a/shell/browser/api/atom_api_app.cc +++ b/shell/browser/api/atom_api_app.cc @@ -1452,6 +1452,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", diff --git a/shell/browser/browser.h b/shell/browser/browser.h index 9c160cc37ed19..1319240e4b88a 100644 --- a/shell/browser/browser.h +++ b/shell/browser/browser.h @@ -89,6 +89,8 @@ class Browser : public WindowListObserver { bool IsDefaultProtocolClient(const std::string& protocol, mate::Arguments* args); + base::string16 GetApplicationNameForProtocol(const GURL& url); + // Set/Get the badge count. bool SetBadgeCount(int count); int GetBadgeCount(); diff --git a/shell/browser/browser_linux.cc b/shell/browser/browser_linux.cc index c73c434465893..fcbb359e09a5a 100644 --- a/shell/browser/browser_linux.cc +++ b/shell/browser/browser_linux.cc @@ -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& argv, int* exit_code) { *exit_code = EXIT_FAILURE; int devnull = open("/dev/null", O_RDONLY); @@ -39,24 +47,37 @@ bool LaunchXdgUtility(const std::vector& argv, int* exit_code) { if (!process.IsValid()) return false; + LaunchXdgUtilityScopedAllowBaseSyncPrimitives allow_base_sync_primitives; return process.WaitForExit(exit_code); } +base::Optional GetXdgAppOutput( + const std::vector& 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(); + + return base::make_optional(reply); +} + bool SetDefaultWebClient(const std::string& protocol) { std::unique_ptr env(base::Environment::Create()); - std::vector argv; - argv.emplace_back(kXdgSettings); - argv.emplace_back("set"); + std::vector 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); @@ -91,27 +112,18 @@ bool Browser::IsDefaultProtocolClient(const std::string& protocol, if (protocol.empty()) return false; - std::vector 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 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 @@ -120,6 +132,14 @@ bool Browser::RemoveAsDefaultProtocolClient(const std::string& protocol, return false; } +base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) { + const std::vector 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); diff --git a/shell/browser/browser_mac.mm b/shell/browser/browser_mac.mm index ebf82554904e8..36c6a1e21e61a 100644 --- a/shell/browser/browser_mac.mm +++ b/shell/browser/browser_mac.mm @@ -131,6 +131,22 @@ return result == NSOrderedSame; } +base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) { + NSURL* ns_url = [NSURL + URLWithString:base::SysUTF8ToNSString(url.possibly_invalid_spec())]; + base::ScopedCFTypeRef out_err; + base::ScopedCFTypeRef 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) { diff --git a/shell/browser/browser_win.cc b/shell/browser/browser_win.cc index b272b179a4cda..932c77c830d36 100644 --- a/shell/browser/browser_win.cc +++ b/shell/browser/browser_win.cc @@ -71,6 +71,68 @@ bool GetProtocolLaunchPath(mate::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& launch_args) { if (exe->empty() && !GetProcessExecPath(exe)) { @@ -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; } diff --git a/spec-main/api-app-spec.ts b/spec-main/api-app-spec.ts index 62946cd0ca0be..861a9b2c3bc5e 100644 --- a/spec-main/api-app-spec.ts +++ b/spec-main/api-app-spec.ts @@ -883,6 +883,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', () => {