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:
parent
24939e8fa4
commit
9b01bb00d2
7 changed files with 183 additions and 20 deletions
|
@ -770,6 +770,21 @@ macOS machine. Please refer to
|
||||||
|
|
||||||
The API uses the Windows Registry and LSCopyDefaultHandlerForURLScheme internally.
|
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_
|
### `app.setUserTasks(tasks)` _Windows_
|
||||||
|
|
||||||
* `tasks` [Task[]](structures/task.md) - Array of `Task` objects
|
* `tasks` [Task[]](structures/task.md) - Array of `Task` objects
|
||||||
|
|
|
@ -1450,6 +1450,9 @@ void App::BuildPrototype(v8::Isolate* isolate,
|
||||||
.SetMethod(
|
.SetMethod(
|
||||||
"removeAsDefaultProtocolClient",
|
"removeAsDefaultProtocolClient",
|
||||||
base::BindRepeating(&Browser::RemoveAsDefaultProtocolClient, browser))
|
base::BindRepeating(&Browser::RemoveAsDefaultProtocolClient, browser))
|
||||||
|
.SetMethod(
|
||||||
|
"getApplicationNameForProtocol",
|
||||||
|
base::BindRepeating(&Browser::GetApplicationNameForProtocol, browser))
|
||||||
.SetMethod("_setBadgeCount",
|
.SetMethod("_setBadgeCount",
|
||||||
base::BindRepeating(&Browser::SetBadgeCount, browser))
|
base::BindRepeating(&Browser::SetBadgeCount, browser))
|
||||||
.SetMethod("_getBadgeCount",
|
.SetMethod("_getBadgeCount",
|
||||||
|
|
|
@ -92,6 +92,8 @@ class Browser : public WindowListObserver {
|
||||||
bool IsDefaultProtocolClient(const std::string& protocol,
|
bool IsDefaultProtocolClient(const std::string& protocol,
|
||||||
gin_helper::Arguments* args);
|
gin_helper::Arguments* args);
|
||||||
|
|
||||||
|
base::string16 GetApplicationNameForProtocol(const GURL& url);
|
||||||
|
|
||||||
// Set/Get the badge count.
|
// Set/Get the badge count.
|
||||||
bool SetBadgeCount(int count);
|
bool SetBadgeCount(int count);
|
||||||
int GetBadgeCount();
|
int GetBadgeCount();
|
||||||
|
|
|
@ -25,6 +25,14 @@ namespace electron {
|
||||||
const char kXdgSettings[] = "xdg-settings";
|
const char kXdgSettings[] = "xdg-settings";
|
||||||
const char kXdgSettingsDefaultSchemeHandler[] = "default-url-scheme-handler";
|
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) {
|
bool LaunchXdgUtility(const std::vector<std::string>& argv, int* exit_code) {
|
||||||
*exit_code = EXIT_FAILURE;
|
*exit_code = EXIT_FAILURE;
|
||||||
int devnull = open("/dev/null", O_RDONLY);
|
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())
|
if (!process.IsValid())
|
||||||
return false;
|
return false;
|
||||||
|
LaunchXdgUtilityScopedAllowBaseSyncPrimitives allow_base_sync_primitives;
|
||||||
return process.WaitForExit(exit_code);
|
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) {
|
bool SetDefaultWebClient(const std::string& protocol) {
|
||||||
std::unique_ptr<base::Environment> env(base::Environment::Create());
|
std::unique_ptr<base::Environment> env(base::Environment::Create());
|
||||||
|
|
||||||
std::vector<std::string> argv;
|
std::vector<std::string> argv = {kXdgSettings, "set"};
|
||||||
argv.emplace_back(kXdgSettings);
|
|
||||||
argv.emplace_back("set");
|
|
||||||
if (!protocol.empty()) {
|
if (!protocol.empty()) {
|
||||||
argv.emplace_back(kXdgSettingsDefaultSchemeHandler);
|
argv.emplace_back(kXdgSettingsDefaultSchemeHandler);
|
||||||
argv.push_back(protocol);
|
argv.emplace_back(protocol);
|
||||||
}
|
}
|
||||||
std::string desktop_name;
|
std::string desktop_name;
|
||||||
if (!env->GetVar("CHROME_DESKTOP", &desktop_name)) {
|
if (!env->GetVar("CHROME_DESKTOP", &desktop_name)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
argv.push_back(desktop_name);
|
argv.emplace_back(desktop_name);
|
||||||
|
|
||||||
int exit_code;
|
int exit_code;
|
||||||
bool ran_ok = LaunchXdgUtility(argv, &exit_code);
|
bool ran_ok = LaunchXdgUtility(argv, &exit_code);
|
||||||
|
@ -91,27 +112,18 @@ bool Browser::IsDefaultProtocolClient(const std::string& protocol,
|
||||||
if (protocol.empty())
|
if (protocol.empty())
|
||||||
return false;
|
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;
|
std::string desktop_name;
|
||||||
if (!env->GetVar("CHROME_DESKTOP", &desktop_name))
|
if (!env->GetVar("CHROME_DESKTOP", &desktop_name))
|
||||||
return false;
|
return false;
|
||||||
argv.push_back(desktop_name);
|
const std::vector<std::string> argv = {kXdgSettings, "check",
|
||||||
|
kXdgSettingsDefaultSchemeHandler,
|
||||||
std::string reply;
|
protocol, desktop_name};
|
||||||
int success_code;
|
const auto output = GetXdgAppOutput(argv);
|
||||||
bool ran_ok = base::GetAppOutputWithExitCode(base::CommandLine(argv), &reply,
|
if (!output)
|
||||||
&success_code);
|
|
||||||
|
|
||||||
if (!ran_ok || success_code != EXIT_SUCCESS)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Allow any reply that starts with "yes".
|
// Allow any reply that starts with "yes".
|
||||||
return base::StartsWith(reply, "yes", base::CompareCase::SENSITIVE) ? true
|
return base::StartsWith(output.value(), "yes", base::CompareCase::SENSITIVE);
|
||||||
: false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo implement
|
// Todo implement
|
||||||
|
@ -120,6 +132,14 @@ bool Browser::RemoveAsDefaultProtocolClient(const std::string& protocol,
|
||||||
return false;
|
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) {
|
bool Browser::SetBadgeCount(int count) {
|
||||||
if (IsUnityRunning()) {
|
if (IsUnityRunning()) {
|
||||||
unity::SetDownloadCount(count);
|
unity::SetDownloadCount(count);
|
||||||
|
|
|
@ -132,6 +132,22 @@ bool Browser::IsDefaultProtocolClient(const std::string& protocol,
|
||||||
return result == NSOrderedSame;
|
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) {}
|
void Browser::SetAppUserModelID(const base::string16& name) {}
|
||||||
|
|
||||||
bool Browser::SetBadgeCount(int count) {
|
bool Browser::SetBadgeCount(int count) {
|
||||||
|
|
|
@ -71,6 +71,68 @@ bool GetProtocolLaunchPath(gin_helper::Arguments* args, base::string16* exe) {
|
||||||
return true;
|
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,
|
bool FormatCommandLineString(base::string16* exe,
|
||||||
const std::vector<base::string16>& launch_args) {
|
const std::vector<base::string16>& launch_args) {
|
||||||
if (exe->empty() && !GetProcessExecPath(exe)) {
|
if (exe->empty() && !GetProcessExecPath(exe)) {
|
||||||
|
@ -293,6 +355,17 @@ bool Browser::IsDefaultProtocolClient(const std::string& protocol,
|
||||||
return keyVal == exe;
|
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) {
|
bool Browser::SetBadgeCount(int count) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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', () => {
|
describe('app launch through uri', () => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue