feat: add app.getApplicationInfoForProtocol API (#24112)

* pre merge

* windows changes

* added tests

* clean up

* more cleanup

* lint error

* windows 7 support

* added windows 7 implementation

* code review

* lint and code review

* code review

* app.md merge conflict

* merge conflict app.md

accidently deleted code block

* 'lint'

* mis-moved getapplicationinfoforprotocol() into anonymous namespace

* fix test

* lint

* code review
This commit is contained in:
George Xu 2020-06-30 12:22:30 -07:00 committed by GitHub
parent 2cbd091e89
commit ee61eb9aa4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 261 additions and 20 deletions

View file

@ -815,6 +815,20 @@ Returns `String` - Name of the application handling the protocol, or an empty
This method returns the application name of the default handler for the protocol This method returns the application name of the default handler for the protocol
(aka URI scheme) of a URL. (aka URI scheme) of a URL.
### `app.getApplicationInfoForProtocol(url)` _macOS_ _Windows_
* `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 `Promise<Object>` - Resolve with an object containing the following:
* `icon` NativeImage - the display icon of the app handling the protocol.
* `path` String - installation path of the app handling the protocol.
* `name` String - display name of the app handling the protocol.
This method returns a promise that contains the application name, icon and path 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

View file

@ -1496,6 +1496,11 @@ void App::BuildPrototype(v8::Isolate* isolate,
.SetMethod( .SetMethod(
"removeAsDefaultProtocolClient", "removeAsDefaultProtocolClient",
base::BindRepeating(&Browser::RemoveAsDefaultProtocolClient, browser)) base::BindRepeating(&Browser::RemoveAsDefaultProtocolClient, browser))
#if !defined(OS_LINUX)
.SetMethod(
"getApplicationInfoForProtocol",
base::BindRepeating(&Browser::GetApplicationInfoForProtocol, browser))
#endif
.SetMethod( .SetMethod(
"getApplicationNameForProtocol", "getApplicationNameForProtocol",
base::BindRepeating(&Browser::GetApplicationNameForProtocol, browser)) base::BindRepeating(&Browser::GetApplicationNameForProtocol, browser))

View file

@ -13,7 +13,9 @@
#include "base/macros.h" #include "base/macros.h"
#include "base/observer_list.h" #include "base/observer_list.h"
#include "base/strings/string16.h" #include "base/strings/string16.h"
#include "base/task/cancelable_task_tracker.h"
#include "base/values.h" #include "base/values.h"
#include "gin/dictionary.h"
#include "shell/browser/browser_observer.h" #include "shell/browser/browser_observer.h"
#include "shell/browser/window_list_observer.h" #include "shell/browser/window_list_observer.h"
#include "shell/common/gin_helper/promise.h" #include "shell/common/gin_helper/promise.h"
@ -98,6 +100,12 @@ class Browser : public WindowListObserver {
base::string16 GetApplicationNameForProtocol(const GURL& url); base::string16 GetApplicationNameForProtocol(const GURL& url);
#if !defined(OS_LINUX)
// get the name, icon and path for an application
v8::Local<v8::Promise> GetApplicationInfoForProtocol(v8::Isolate* isolate,
const GURL& url);
#endif
// Set/Get the badge count. // Set/Get the badge count.
bool SetBadgeCount(int count); bool SetBadgeCount(int count);
int GetBadgeCount(); int GetBadgeCount();
@ -302,6 +310,9 @@ class Browser : public WindowListObserver {
// Observers of the browser. // Observers of the browser.
base::ObserverList<BrowserObserver> observers_; base::ObserverList<BrowserObserver> observers_;
// Tracks tasks requesting file icons.
base::CancelableTaskTracker cancelable_task_tracker_;
// Whether `app.exit()` has been called // Whether `app.exit()` has been called
bool is_exiting_ = false; bool is_exiting_ = false;

View file

@ -21,6 +21,7 @@
#include "shell/browser/native_window.h" #include "shell/browser/native_window.h"
#include "shell/browser/window_list.h" #include "shell/browser/window_list.h"
#include "shell/common/application_info.h" #include "shell/common/application_info.h"
#include "shell/common/gin_converters/image_converter.h"
#include "shell/common/gin_helper/arguments.h" #include "shell/common/gin_helper/arguments.h"
#include "shell/common/gin_helper/dictionary.h" #include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/error_thrower.h" #include "shell/common/gin_helper/error_thrower.h"
@ -31,6 +32,65 @@
namespace electron { namespace electron {
namespace {
NSString* GetAppPathForProtocol(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 nullptr;
}
NSString* app_path = [base::mac::CFToNSCast(openingApp.get()) path];
return app_path;
}
gfx::Image GetApplicationIconForProtocol(NSString* _Nonnull app_path) {
NSImage* image = [[NSWorkspace sharedWorkspace] iconForFile:app_path];
gfx::Image icon(image);
return icon;
}
base::string16 GetAppDisplayNameForProtocol(NSString* app_path) {
NSString* app_display_name =
[[NSFileManager defaultManager] displayNameAtPath:app_path];
return base::SysNSStringToUTF16(app_display_name);
}
} // namespace
v8::Local<v8::Promise> Browser::GetApplicationInfoForProtocol(
v8::Isolate* isolate,
const GURL& url) {
gin_helper::Promise<gin_helper::Dictionary> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(isolate);
NSString* ns_app_path = GetAppPathForProtocol(url);
if (!ns_app_path) {
promise.RejectWithErrorMessage(
"Unable to retrieve installation path to app");
return handle;
}
base::string16 app_path = base::SysNSStringToUTF16(ns_app_path);
base::string16 app_display_name = GetAppDisplayNameForProtocol(ns_app_path);
gfx::Image app_icon = GetApplicationIconForProtocol(ns_app_path);
dict.Set("name", app_display_name);
dict.Set("path", app_path);
dict.Set("icon", app_icon);
promise.Resolve(dict);
return handle;
}
void Browser::SetShutdownHandler(base::Callback<bool()> handler) { void Browser::SetShutdownHandler(base::Callback<bool()> handler) {
[[AtomApplication sharedApplication] setShutdownHandler:std::move(handler)]; [[AtomApplication sharedApplication] setShutdownHandler:std::move(handler)];
} }
@ -148,19 +208,12 @@ bool Browser::IsDefaultProtocolClient(const std::string& protocol,
} }
base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) { base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) {
NSURL* ns_url = [NSURL NSString* app_path = GetAppPathForProtocol(url);
URLWithString:base::SysUTF8ToNSString(url.possibly_invalid_spec())]; if (!app_path) {
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(); return base::string16();
} }
NSString* appPath = [base::mac::CFToNSCast(openingApp.get()) path]; base::string16 app_display_name = GetAppDisplayNameForProtocol(app_path);
NSString* appDisplayName = return app_display_name;
[[NSFileManager defaultManager] displayNameAtPath:appPath];
return base::SysNSStringToUTF16(appDisplayName);
} }
void Browser::SetAppUserModelID(const base::string16& name) {} void Browser::SetAppUserModelID(const base::string16& name) {}

View file

@ -21,11 +21,17 @@
#include "base/win/registry.h" #include "base/win/registry.h"
#include "base/win/win_util.h" #include "base/win/win_util.h"
#include "base/win/windows_version.h" #include "base/win/windows_version.h"
#include "chrome/browser/icon_manager.h"
#include "electron/electron_version.h" #include "electron/electron_version.h"
#include "shell/browser/api/electron_api_app.h"
#include "shell/browser/electron_browser_main_parts.h"
#include "shell/browser/ui/message_box.h" #include "shell/browser/ui/message_box.h"
#include "shell/browser/ui/win/jump_list.h" #include "shell/browser/ui/win/jump_list.h"
#include "shell/common/application_info.h" #include "shell/common/application_info.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_converters/image_converter.h"
#include "shell/common/gin_helper/arguments.h" #include "shell/common/gin_helper/arguments.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/skia_util.h" #include "shell/common/skia_util.h"
#include "ui/events/keycodes/keyboard_code_conversion_win.h" #include "ui/events/keycodes/keyboard_code_conversion_win.h"
@ -49,7 +55,6 @@ BOOL CALLBACK WindowsEnumerationHandler(HWND hwnd, LPARAM param) {
bool GetProcessExecPath(base::string16* exe) { bool GetProcessExecPath(base::string16* exe) {
base::FilePath path; base::FilePath path;
if (!base::PathService::Get(base::FILE_EXE, &path)) { if (!base::PathService::Get(base::FILE_EXE, &path)) {
LOG(ERROR) << "Error getting app exe path";
return false; return false;
} }
*exe = path.value(); *exe = path.value();
@ -81,23 +86,25 @@ bool IsValidCustomProtocol(const base::string16& scheme) {
return cmd_key.Valid() && cmd_key.HasValue(L"URL Protocol"); return cmd_key.Valid() && cmd_key.HasValue(L"URL Protocol");
} }
// Helper for GetApplicationInfoForProtocol().
// takes in an assoc_str
// (https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/ne-shlwapi-assocstr)
// and returns the application name, icon and path that handles the protocol.
//
// Windows 8 introduced a new protocol->executable binding system which cannot // Windows 8 introduced a new protocol->executable binding system which cannot
// be retrieved in the HKCR registry subkey method implemented below. We call // be retrieved in the HKCR registry subkey method implemented below. We call
// AssocQueryString with the new Win8-only flag ASSOCF_IS_PROTOCOL instead. // AssocQueryString with the new Win8-only flag ASSOCF_IS_PROTOCOL instead.
base::string16 GetAppForProtocolUsingAssocQuery(const GURL& url) { base::string16 GetAppInfoHelperForProtocol(ASSOCSTR assoc_str,
const GURL& url) {
const base::string16 url_scheme = base::ASCIIToUTF16(url.scheme()); const base::string16 url_scheme = base::ASCIIToUTF16(url.scheme());
if (!IsValidCustomProtocol(url_scheme)) if (!IsValidCustomProtocol(url_scheme))
return base::string16(); 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]; wchar_t out_buffer[1024];
DWORD buffer_size = base::size(out_buffer); DWORD buffer_size = base::size(out_buffer);
HRESULT hr = HRESULT hr =
AssocQueryString(ASSOCF_IS_PROTOCOL, ASSOCSTR_FRIENDLYAPPNAME, AssocQueryString(ASSOCF_IS_PROTOCOL, assoc_str, url_scheme.c_str(), NULL,
url_scheme.c_str(), NULL, out_buffer, &buffer_size); out_buffer, &buffer_size);
if (FAILED(hr)) { if (FAILED(hr)) {
DLOG(WARNING) << "AssocQueryString failed!"; DLOG(WARNING) << "AssocQueryString failed!";
return base::string16(); return base::string16();
@ -105,6 +112,32 @@ base::string16 GetAppForProtocolUsingAssocQuery(const GURL& url) {
return base::string16(out_buffer); return base::string16(out_buffer);
} }
void OnIconDataAvailable(const base::FilePath& app_path,
const base::string16& app_display_name,
gin_helper::Promise<gin_helper::Dictionary> promise,
gfx::Image icon) {
if (!icon.IsEmpty()) {
v8::HandleScope scope(promise.isolate());
gin_helper::Dictionary dict =
gin::Dictionary::CreateEmpty(promise.isolate());
dict.Set("path", app_path);
dict.Set("name", app_display_name);
dict.Set("icon", icon);
promise.Resolve(dict);
} else {
promise.RejectWithErrorMessage("Failed to get file icon.");
}
}
base::string16 GetAppDisplayNameForProtocol(const GURL& url) {
return GetAppInfoHelperForProtocol(ASSOCSTR_FRIENDLYAPPNAME, url);
}
base::string16 GetAppPathForProtocol(const GURL& url) {
return GetAppInfoHelperForProtocol(ASSOCSTR_EXECUTABLE, url);
}
base::string16 GetAppForProtocolUsingRegistry(const GURL& url) { base::string16 GetAppForProtocolUsingRegistry(const GURL& url) {
const base::string16 url_scheme = base::ASCIIToUTF16(url.scheme()); const base::string16 url_scheme = base::ASCIIToUTF16(url.scheme());
if (!IsValidCustomProtocol(url_scheme)) if (!IsValidCustomProtocol(url_scheme))
@ -169,6 +202,96 @@ void Browser::Focus(gin_helper::Arguments* args) {
EnumWindows(&WindowsEnumerationHandler, reinterpret_cast<LPARAM>(&pid)); EnumWindows(&WindowsEnumerationHandler, reinterpret_cast<LPARAM>(&pid));
} }
void GetFileIcon(const base::FilePath& path,
v8::Isolate* isolate,
base::CancelableTaskTracker* cancelable_task_tracker_,
const base::string16 app_display_name,
gin_helper::Promise<gin_helper::Dictionary> promise) {
base::FilePath normalized_path = path.NormalizePathSeparators();
IconLoader::IconSize icon_size = IconLoader::IconSize::LARGE;
auto* icon_manager = ElectronBrowserMainParts::Get()->GetIconManager();
gfx::Image* icon =
icon_manager->LookupIconFromFilepath(normalized_path, icon_size);
if (icon) {
gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(isolate);
dict.Set("icon", *icon);
dict.Set("name", app_display_name);
dict.Set("path", normalized_path);
promise.Resolve(dict);
} else {
icon_manager->LoadIcon(normalized_path, icon_size,
base::BindOnce(&OnIconDataAvailable, normalized_path,
app_display_name, std::move(promise)),
cancelable_task_tracker_);
}
}
void GetApplicationInfoForProtocolUsingRegistry(
v8::Isolate* isolate,
const GURL& url,
gin_helper::Promise<gin_helper::Dictionary> promise,
base::CancelableTaskTracker* cancelable_task_tracker_) {
base::FilePath app_path;
const base::string16 url_scheme = base::ASCIIToUTF16(url.scheme());
if (!IsValidCustomProtocol(url_scheme)) {
promise.RejectWithErrorMessage("invalid url_scheme");
return;
}
base::string16 command_to_launch;
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));
app_path = command_line.GetProgram();
} else {
promise.RejectWithErrorMessage(
"Unable to retrieve installation path to app");
return;
}
const base::string16 app_display_name = GetAppForProtocolUsingRegistry(url);
if (app_display_name.length() == 0) {
promise.RejectWithErrorMessage(
"Unable to retrieve application display name");
return;
}
GetFileIcon(app_path, isolate, cancelable_task_tracker_, app_display_name,
std::move(promise));
}
// resolves `Promise<Object>` - Resolve with an object containing the following:
// * `icon` NativeImage - the display icon of the app handling the protocol.
// * `path` String - installation path of the app handling the protocol.
// * `name` String - display name of the app handling the protocol.
void GetApplicationInfoForProtocolUsingAssocQuery(
v8::Isolate* isolate,
const GURL& url,
gin_helper::Promise<gin_helper::Dictionary> promise,
base::CancelableTaskTracker* cancelable_task_tracker_) {
base::string16 app_path = GetAppPathForProtocol(url);
if (app_path.empty()) {
promise.RejectWithErrorMessage(
"Unable to retrieve installation path to app");
return;
}
base::string16 app_display_name = GetAppDisplayNameForProtocol(url);
if (app_display_name.empty()) {
promise.RejectWithErrorMessage("Unable to retrieve display name of app");
return;
}
base::FilePath app_path_file_path = base::FilePath(app_path);
GetFileIcon(app_path_file_path, isolate, cancelable_task_tracker_,
app_display_name, std::move(promise));
}
void Browser::AddRecentDocument(const base::FilePath& path) { void Browser::AddRecentDocument(const base::FilePath& path) {
CComPtr<IShellItem> item; CComPtr<IShellItem> item;
HRESULT hr = SHCreateItemFromParsingName(path.value().c_str(), NULL, HRESULT hr = SHCreateItemFromParsingName(path.value().c_str(), NULL,
@ -358,7 +481,7 @@ bool Browser::IsDefaultProtocolClient(const std::string& protocol,
base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) { base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) {
// Windows 8 or above has a new protocol association query. // Windows 8 or above has a new protocol association query.
if (base::win::GetVersion() >= base::win::Version::WIN8) { if (base::win::GetVersion() >= base::win::Version::WIN8) {
base::string16 application_name = GetAppForProtocolUsingAssocQuery(url); base::string16 application_name = GetAppDisplayNameForProtocol(url);
if (!application_name.empty()) if (!application_name.empty())
return application_name; return application_name;
} }
@ -366,6 +489,24 @@ base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) {
return GetAppForProtocolUsingRegistry(url); return GetAppForProtocolUsingRegistry(url);
} }
v8::Local<v8::Promise> Browser::GetApplicationInfoForProtocol(
v8::Isolate* isolate,
const GURL& url) {
gin_helper::Promise<gin_helper::Dictionary> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
// Windows 8 or above has a new protocol association query.
if (base::win::GetVersion() >= base::win::Version::WIN8) {
GetApplicationInfoForProtocolUsingAssocQuery(
isolate, url, std::move(promise), &cancelable_task_tracker_);
return handle;
}
GetApplicationInfoForProtocolUsingRegistry(isolate, url, std::move(promise),
&cancelable_task_tracker_);
return handle;
}
bool Browser::SetBadgeCount(int count) { bool Browser::SetBadgeCount(int count) {
return false; return false;
} }

View file

@ -973,6 +973,23 @@ describe('app module', () => {
}); });
}); });
ifdescribe(process.platform !== 'linux')('getApplicationInfoForProtocol()', () => {
it('returns promise rejection for a bogus protocol', async function () {
await expect(
app.getApplicationInfoForProtocol('bogus-protocol://')
).to.eventually.be.rejectedWith(
'Unable to retrieve installation path to app'
);
});
it('returns resolved promise with appPath, displayName and icon', async function () {
const appInfo = await app.getApplicationInfoForProtocol('https://');
expect(appInfo.path).not.to.be.undefined();
expect(appInfo.name).not.to.be.undefined();
expect(appInfo.icon).not.to.be.undefined();
});
});
describe('isDefaultProtocolClient()', () => { describe('isDefaultProtocolClient()', () => {
it('returns false for a bogus protocol', () => { it('returns false for a bogus protocol', () => {
expect(app.isDefaultProtocolClient('bogus-protocol://')).to.equal(false); expect(app.isDefaultProtocolClient('bogus-protocol://')).to.equal(false);