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.
|
||||
|
||||
### `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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
Loading…
Reference in a new issue