Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add app.getApplicationNameForProtocol API (8-x-y) #21117

Merged
merged 1 commit into from Nov 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions shell/browser/browser.h
Expand Up @@ -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();
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 @@ -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<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(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<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 @@ -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', () => {
Expand Down