Browse Source

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
George Xu 4 years ago
parent
commit
ee61eb9aa4

+ 14 - 0
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<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_
 
 * `tasks` [Task[]](structures/task.md) - Array of `Task` objects

+ 5 - 0
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))

+ 11 - 0
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<v8::Promise> 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<BrowserObserver> observers_;
 
+  // Tracks tasks requesting file icons.
+  base::CancelableTaskTracker cancelable_task_tracker_;
+
   // Whether `app.exit()` has been called
   bool is_exiting_ = false;
 

+ 64 - 11
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<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) {
   [[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<CFErrorRef> out_err;
-  base::ScopedCFTypeRef<CFURLRef> 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) {}

+ 150 - 9
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<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) {
   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<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) {
   CComPtr<IShellItem> 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<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) {
   return false;
 }

+ 17 - 0
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);