From ee61eb9aa4cc541ae2b33fb2d9129510d8cc2b65 Mon Sep 17 00:00:00 2001 From: George Xu <33054982+georgexu99@users.noreply.github.com> Date: Tue, 30 Jun 2020 12:22:30 -0700 Subject: [PATCH] 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 --- docs/api/app.md | 14 +++ shell/browser/api/electron_api_app.cc | 5 + shell/browser/browser.h | 11 ++ shell/browser/browser_mac.mm | 75 ++++++++++-- shell/browser/browser_win.cc | 159 ++++++++++++++++++++++++-- spec-main/api-app-spec.ts | 17 +++ 6 files changed, 261 insertions(+), 20 deletions(-) diff --git a/docs/api/app.md b/docs/api/app.md index 1f040f9667fc..1569203711cd 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -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 (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` - 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_ * `tasks` [Task[]](structures/task.md) - Array of `Task` objects diff --git a/shell/browser/api/electron_api_app.cc b/shell/browser/api/electron_api_app.cc index 4fb29b518257..175d46b1d87e 100644 --- a/shell/browser/api/electron_api_app.cc +++ b/shell/browser/api/electron_api_app.cc @@ -1496,6 +1496,11 @@ void App::BuildPrototype(v8::Isolate* isolate, .SetMethod( "removeAsDefaultProtocolClient", base::BindRepeating(&Browser::RemoveAsDefaultProtocolClient, browser)) +#if !defined(OS_LINUX) + .SetMethod( + "getApplicationInfoForProtocol", + base::BindRepeating(&Browser::GetApplicationInfoForProtocol, browser)) +#endif .SetMethod( "getApplicationNameForProtocol", base::BindRepeating(&Browser::GetApplicationNameForProtocol, browser)) diff --git a/shell/browser/browser.h b/shell/browser/browser.h index b0e88dbd3b34..3a1499295f03 100644 --- a/shell/browser/browser.h +++ b/shell/browser/browser.h @@ -13,7 +13,9 @@ #include "base/macros.h" #include "base/observer_list.h" #include "base/strings/string16.h" +#include "base/task/cancelable_task_tracker.h" #include "base/values.h" +#include "gin/dictionary.h" #include "shell/browser/browser_observer.h" #include "shell/browser/window_list_observer.h" #include "shell/common/gin_helper/promise.h" @@ -98,6 +100,12 @@ class Browser : public WindowListObserver { base::string16 GetApplicationNameForProtocol(const GURL& url); +#if !defined(OS_LINUX) + // get the name, icon and path for an application + v8::Local GetApplicationInfoForProtocol(v8::Isolate* isolate, + const GURL& url); +#endif + // Set/Get the badge count. bool SetBadgeCount(int count); int GetBadgeCount(); @@ -302,6 +310,9 @@ class Browser : public WindowListObserver { // Observers of the browser. base::ObserverList observers_; + // Tracks tasks requesting file icons. + base::CancelableTaskTracker cancelable_task_tracker_; + // Whether `app.exit()` has been called bool is_exiting_ = false; diff --git a/shell/browser/browser_mac.mm b/shell/browser/browser_mac.mm index ec0f832479ef..e0608bb2aca4 100644 --- a/shell/browser/browser_mac.mm +++ b/shell/browser/browser_mac.mm @@ -21,6 +21,7 @@ #include "shell/browser/native_window.h" #include "shell/browser/window_list.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/dictionary.h" #include "shell/common/gin_helper/error_thrower.h" @@ -31,6 +32,65 @@ namespace electron { +namespace { + +NSString* GetAppPathForProtocol(const GURL& url) { + NSURL* ns_url = [NSURL + URLWithString:base::SysUTF8ToNSString(url.possibly_invalid_spec())]; + base::ScopedCFTypeRef out_err; + + base::ScopedCFTypeRef 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 Browser::GetApplicationInfoForProtocol( + v8::Isolate* isolate, + const GURL& url) { + gin_helper::Promise promise(isolate); + v8::Local 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 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) { - NSURL* ns_url = [NSURL - URLWithString:base::SysUTF8ToNSString(url.possibly_invalid_spec())]; - base::ScopedCFTypeRef out_err; - base::ScopedCFTypeRef openingApp(LSCopyDefaultApplicationURLForURL( - (CFURLRef)ns_url, kLSRolesAll, out_err.InitializeInto())); - if (out_err) { - // likely kLSApplicationNotFoundErr + NSString* app_path = GetAppPathForProtocol(url); + if (!app_path) { return base::string16(); } - NSString* appPath = [base::mac::CFToNSCast(openingApp.get()) path]; - NSString* appDisplayName = - [[NSFileManager defaultManager] displayNameAtPath:appPath]; - return base::SysNSStringToUTF16(appDisplayName); + base::string16 app_display_name = GetAppDisplayNameForProtocol(app_path); + return app_display_name; } void Browser::SetAppUserModelID(const base::string16& name) {} diff --git a/shell/browser/browser_win.cc b/shell/browser/browser_win.cc index 47e476c4110c..717ec16b01ca 100644 --- a/shell/browser/browser_win.cc +++ b/shell/browser/browser_win.cc @@ -21,11 +21,17 @@ #include "base/win/registry.h" #include "base/win/win_util.h" #include "base/win/windows_version.h" +#include "chrome/browser/icon_manager.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/win/jump_list.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/dictionary.h" #include "shell/common/skia_util.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) { base::FilePath path; if (!base::PathService::Get(base::FILE_EXE, &path)) { - LOG(ERROR) << "Error getting app exe path"; return false; } *exe = path.value(); @@ -81,23 +86,25 @@ bool IsValidCustomProtocol(const base::string16& scheme) { 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 // 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) { +base::string16 GetAppInfoHelperForProtocol(ASSOCSTR assoc_str, + 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); + AssocQueryString(ASSOCF_IS_PROTOCOL, assoc_str, url_scheme.c_str(), NULL, + out_buffer, &buffer_size); if (FAILED(hr)) { DLOG(WARNING) << "AssocQueryString failed!"; return base::string16(); @@ -105,6 +112,32 @@ base::string16 GetAppForProtocolUsingAssocQuery(const GURL& url) { return base::string16(out_buffer); } +void OnIconDataAvailable(const base::FilePath& app_path, + const base::string16& app_display_name, + gin_helper::Promise 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) { const base::string16 url_scheme = base::ASCIIToUTF16(url.scheme()); if (!IsValidCustomProtocol(url_scheme)) @@ -169,6 +202,96 @@ void Browser::Focus(gin_helper::Arguments* args) { EnumWindows(&WindowsEnumerationHandler, reinterpret_cast(&pid)); } +void GetFileIcon(const base::FilePath& path, + v8::Isolate* isolate, + base::CancelableTaskTracker* cancelable_task_tracker_, + const base::string16 app_display_name, + gin_helper::Promise 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 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` - 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 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) { CComPtr item; 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) { // Windows 8 or above has a new protocol association query. if (base::win::GetVersion() >= base::win::Version::WIN8) { - base::string16 application_name = GetAppForProtocolUsingAssocQuery(url); + base::string16 application_name = GetAppDisplayNameForProtocol(url); if (!application_name.empty()) return application_name; } @@ -366,6 +489,24 @@ base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) { return GetAppForProtocolUsingRegistry(url); } +v8::Local Browser::GetApplicationInfoForProtocol( + v8::Isolate* isolate, + const GURL& url) { + gin_helper::Promise promise(isolate); + v8::Local 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) { return false; } diff --git a/spec-main/api-app-spec.ts b/spec-main/api-app-spec.ts index 3b4bfd0a47d2..4a136bf99ff5 100644 --- a/spec-main/api-app-spec.ts +++ b/spec-main/api-app-spec.ts @@ -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()', () => { it('returns false for a bogus protocol', () => { expect(app.isDefaultProtocolClient('bogus-protocol://')).to.equal(false);