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.getApplicationInfoForProtocol API #24112

Merged
merged 19 commits into from Jun 30, 2020
14 changes: 14 additions & 0 deletions docs/api/app.md
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions shell/browser/api/electron_api_app.cc
Expand Up @@ -1495,6 +1495,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))
Expand Down
11 changes: 11 additions & 0 deletions shell/browser/browser.h
Expand Up @@ -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"
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;

Expand Down
75 changes: 64 additions & 11 deletions shell/browser/browser_mac.mm
Expand Up @@ -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"
Expand All @@ -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)];
}
Expand Down Expand Up @@ -148,19 +208,12 @@
}

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) {}
Expand Down
159 changes: 150 additions & 9 deletions shell/browser/browser_win.cc
Expand Up @@ -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"

Expand All @@ -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();
Expand Down Expand Up @@ -81,30 +86,58 @@ 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();
}
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) {
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved
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))
Expand Down Expand Up @@ -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;
}
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved

base::string16 app_display_name = GetAppDisplayNameForProtocol(url);

if (app_display_name.empty()) {
promise.RejectWithErrorMessage("Unable to retrieve display name of app");
return;
}
georgexu99 marked this conversation as resolved.
Show resolved Hide resolved

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,
Expand Down Expand Up @@ -358,14 +481,32 @@ 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;
}

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;
}
Expand Down