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.
This commit is contained in:
Andrew MacDonald 2019-11-06 17:50:33 -08:00 committed by Shelley Vohr
parent 24939e8fa4
commit 9b01bb00d2
7 changed files with 183 additions and 20 deletions

View file

@ -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

View file

@ -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",

View file

@ -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();

View file

@ -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);
@ -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);
@ -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
@ -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);

View file

@ -132,6 +132,22 @@ bool Browser::IsDefaultProtocolClient(const std::string& protocol,
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) {

View file

@ -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)) {
@ -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;
}

View file

@ -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', () => {