Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Prev Previous commit
Next Next commit
Refactor file structure, plugin class, for testability
  • Loading branch information
stuartmorgan-g committed Jul 14, 2021
commit 99b2ab190e4f089ad03145d33057c298172e48c8
34 changes: 34 additions & 0 deletions packages/url_launcher/url_launcher_windows/windows/system_apis.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "system_apis.h"

#include <windows.h>

namespace url_launcher_plugin {

SystemApis::SystemApis() {}

SystemApisImpl::SystemApisImpl() {}

LSTATUS SystemApisImpl::RegCloseKey(HKEY key) { return ::RegCloseKey(key); }

LSTATUS SystemApisImpl::RegOpenKeyExW(HKEY key, LPCWSTR sub_key, DWORD options,
REGSAM desired, PHKEY result) {
return ::RegOpenKeyExW(key, sub_key, options, desired, result);
}

LSTATUS SystemApisImpl::RegQueryValueExW(HKEY key, LPCWSTR value_name,
LPDWORD type, LPBYTE data,
LPDWORD data_size) {
return ::RegQueryValueExW(key, value_name, nullptr, type, data, data_size);
}

HINSTANCE SystemApisImpl::ShellExecuteW(HWND hwnd, LPCWSTR operation,
LPCWSTR file, LPCWSTR parameters,
LPCWSTR directory, int show_flags) {
return ::ShellExecuteW(hwnd, operation, file, parameters, directory,
show_flags);
}

} // namespace url_launcher_plugin
56 changes: 56 additions & 0 deletions packages/url_launcher/url_launcher_windows/windows/system_apis.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <windows.h>

namespace url_launcher_plugin {

// An interface wrapping system APIs used by the plugin, for mocking.
class SystemApis {
public:
SystemApis();
virtual ~SystemApis();

// Disallow copy and move.
SystemApis(const SystemApis&) = delete;
SystemApis& operator=(const SystemApis&) = delete;

// Wrapper for RegCloseKey.
virtual LSTATUS RegCloseKey(HKEY key) = 0;

// Wrapper for RegQueryValueEx.
virtual LSTATUS RegQueryValueExW(HKEY key, LPCWSTR value_name, LPDWORD type,
LPBYTE data, LPDWORD data_size) = 0;

// Wrapper for RegOpenKeyEx.
virtual LSTATUS RegOpenKeyExW(HKEY key, LPCWSTR sub_key, DWORD options,
REGSAM desired, PHKEY result) = 0;

// Wrapper for ShellExecute.
virtual HINSTANCE ShellExecuteW(HWND hwnd, LPCWSTR operation, LPCWSTR file,
LPCWSTR parameters, LPCWSTR directory,
int show_flags) = 0;
};

// Implementation of SystemApis using the Win32 APIs.
class SystemApisImpl : public SystemApis {
public:
SystemApisImpl();
virtual ~SystemApisImpl();

// Disallow copy and move.
SystemApisImpl(const SystemApisImpl&) = delete;
SystemApisImpl& operator=(const SystemApisImpl&) = delete;

// SystemApis Implementation:
virtual LSTATUS RegCloseKey(HKEY key);
virtual LSTATUS RegOpenKeyExW(HKEY key, LPCWSTR sub_key, DWORD options,
REGSAM desired, PHKEY result);
virtual LSTATUS RegQueryValueExW(HKEY key, LPCWSTR value_name, LPDWORD type,
LPBYTE data, LPDWORD data_size);
virtual HINSTANCE ShellExecuteW(HWND hwnd, LPCWSTR operation, LPCWSTR file,
LPCWSTR parameters, LPCWSTR directory,
int show_flags);
};

} // namespace url_launcher_plugin
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "include/url_launcher_windows/url_launcher_plugin.h"
#include "url_launcher_plugin.h"

#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <windows.h>

#include <memory>
#include <optional>
#include <sstream>
#include <string>

#include "include/url_launcher_windows/url_launcher_plugin.h"

namespace url_launcher_plugin {

namespace {

using flutter::EncodableMap;
Expand Down Expand Up @@ -54,19 +59,7 @@ std::string GetUrlArgument(const flutter::MethodCall<>& method_call) {
return url;
}

class UrlLauncherPlugin : public flutter::Plugin {
public:
static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar);

virtual ~UrlLauncherPlugin();

private:
UrlLauncherPlugin();

// Called when a method is called on plugin channel;
void HandleMethodCall(const flutter::MethodCall<>& method_call,
std::unique_ptr<flutter::MethodResult<>> result);
};
} // namespace

// static
void UrlLauncherPlugin::RegisterWithRegistrar(
Expand All @@ -86,7 +79,11 @@ void UrlLauncherPlugin::RegisterWithRegistrar(
registrar->AddPlugin(std::move(plugin));
}

UrlLauncherPlugin::UrlLauncherPlugin() = default;
UrlLauncherPlugin::UrlLauncherPlugin()
: system_apis_(std::make_unique<SystemApisImpl>()) {}

UrlLauncherPlugin::UrlLauncherPlugin(std::unique_ptr<SystemApis> system_apis)
: system_apis_(std::move(system_apis)) {}

UrlLauncherPlugin::~UrlLauncherPlugin() = default;

Expand All @@ -99,17 +96,10 @@ void UrlLauncherPlugin::HandleMethodCall(
result->Error("argument_error", "No URL provided");
return;
}
std::wstring url_wide = Utf16FromUtf8(url);

int status = static_cast<int>(reinterpret_cast<INT_PTR>(
::ShellExecute(nullptr, TEXT("open"), url_wide.c_str(), nullptr,
nullptr, SW_SHOWNORMAL)));

if (status <= 32) {
std::ostringstream error_message;
error_message << "Failed to open " << url << ": ShellExecute error code "
<< status;
result->Error("open_error", error_message.str());
std::optional<std::string> error = LaunchUrl(url);
if (error) {
result->Error("open_error", error.value());
return;
}
result->Success(EncodableValue(true));
Expand All @@ -120,29 +110,55 @@ void UrlLauncherPlugin::HandleMethodCall(
return;
}

bool can_launch = false;
size_t separator_location = url.find(":");
if (separator_location != std::string::npos) {
std::wstring scheme = Utf16FromUtf8(url.substr(0, separator_location));
HKEY key = nullptr;
if (::RegOpenKeyEx(HKEY_CLASSES_ROOT, scheme.c_str(), 0, KEY_QUERY_VALUE,
&key) == ERROR_SUCCESS) {
can_launch = ::RegQueryValueEx(key, L"URL Protocol", nullptr, nullptr,
nullptr, nullptr) == ERROR_SUCCESS;
::RegCloseKey(key);
}
}
bool can_launch = CanLaunchUrl(url);
result->Success(EncodableValue(can_launch));
} else {
result->NotImplemented();
}
}

} // namespace
bool UrlLauncherPlugin::CanLaunchUrl(const std::string& url) {
size_t separator_location = url.find(":");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Off-topic for this patch, but I bet there's some Win32 API for parsing URLs out there. If the Dart side of this is already using the Url class it's much less of an issue; otherwise, : can appear as the port separator etc., or just as part of some garbage string the person passed in here. I suppse the net result would mostly be them shooting themselves in their own foot regardless unless they're writing an app to poke at a very specific and maybe not-particularly-valuable to know subset of users' registry keys.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the Dart side of this is already using the Url class it's much less of an issue

The Dart side of the API is kind of a hot mess (flutter/flutter#79043), and one of the things I dislike most about it is that it takes a String. Someday I want to get back to that proposal and do the overhaul.

I suppse the net result would mostly be them shooting themselves in their own foot regardless

Yes, and at least unlike some of the other issues (like including unencoded spaces, which people do all the time) trying to pass a "URL" that doesn't actually have a scheme should completely fail on every platform, so be very easy to notice during development.

if (separator_location == std::string::npos) {
return false;
}
std::wstring scheme = Utf16FromUtf8(url.substr(0, separator_location));

HKEY key = nullptr;
if (system_apis_->RegOpenKeyExW(HKEY_CLASSES_ROOT, scheme.c_str(), 0,
KEY_QUERY_VALUE, &key) != ERROR_SUCCESS) {
return false;
}
bool has_handler =
system_apis_->RegQueryValueExW(key, L"URL Protocol", nullptr, nullptr,
nullptr) == ERROR_SUCCESS;
system_apis_->RegCloseKey(key);
return has_handler;
}

std::optional<std::string> UrlLauncherPlugin::LaunchUrl(
const std::string& url) {
std::wstring url_wide = Utf16FromUtf8(url);

int status = static_cast<int>(reinterpret_cast<INT_PTR>(
system_apis_->ShellExecuteW(nullptr, TEXT("open"), url_wide.c_str(),
nullptr, nullptr, SW_SHOWNORMAL)));

// Per ::ShellExecuteW documentation, anything >32 indicates success.
if (status <= 32) {
std::ostringstream error_message;
error_message << "Failed to open " << url << ": ShellExecute error code "
<< status;
return std::optional<std::string>(error_message.str());
}
return std::nullopt;
}

} // namespace url_launcher_plugin

void UrlLauncherPluginRegisterWithRegistrar(
FlutterDesktopPluginRegistrarRef registrar) {
UrlLauncherPlugin::RegisterWithRegistrar(
url_launcher_plugin::UrlLauncherPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarManager::GetInstance()
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <windows.h>

#include <memory>
#include <optional>
#include <sstream>
#include <string>

#include "system_apis.h"

namespace url_launcher_plugin {

class UrlLauncherPlugin : public flutter::Plugin {
public:
static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar);

virtual ~UrlLauncherPlugin();

// Disallow copy and move.
UrlLauncherPlugin(const UrlLauncherPlugin&) = delete;
UrlLauncherPlugin& operator=(const UrlLauncherPlugin&) = delete;

private:
UrlLauncherPlugin();

// Creates a plugin instance with the given SystemApi instance.
//
// Exists for unit testing with mock implementations.
UrlLauncherPlugin(std::unique_ptr<SystemApis> system_apis);

// Called when a method is called on plugin channel;
void HandleMethodCall(const flutter::MethodCall<>& method_call,
std::unique_ptr<flutter::MethodResult<>> result);

// Returns whether or not the given URL has a registered handler.
bool CanLaunchUrl(const std::string& url);

// Attempts to launch the given URL. On failure, returns an error string.
std::optional<std::string> LaunchUrl(const std::string& url);

std::unique_ptr<SystemApis> system_apis_;
};

} // namespace url_launcher_plugin