Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion flutter_inappwebview_windows/windows/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ add_executable(${TEST_RUNNER}
)
apply_standard_settings(${TEST_RUNNER})
target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main)
target_link_libraries(${TEST_RUNNER} PRIVATE flutter flutter_wrapper_plugin gtest_main)

# Enable automatic test discovery.
include(GoogleTest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ namespace flutter_inappwebview_plugin
);

auto callback = std::make_unique<WebViewChannelDelegate::ShouldOverrideUrlLoadingCallback>();
callback->owner = aliveToken();
callback->nonNullSuccess = [this, allowRequest, cancelRequest](const NavigationActionPolicy actionPolicy)
{
if (actionPolicy == NavigationActionPolicy::allow) {
Expand Down Expand Up @@ -843,6 +844,7 @@ namespace flutter_inappwebview_plugin
std::move(windowFeatures));

auto callback = std::make_unique<WebViewChannelDelegate::CreateWindowCallback>();
callback->owner = aliveToken();
auto defaultBehaviour = [this, windowId, urlRequest, deferral, args](const std::optional<const bool> handledByClient)
{
if (plugin && plugin->inAppWebViewManager && map_contains(plugin->inAppWebViewManager->windowWebViews, windowId)) {
Expand Down Expand Up @@ -892,6 +894,7 @@ namespace flutter_inappwebview_plugin
failedLog(args->get_PermissionKind(&resource));

auto callback = std::make_unique<WebViewChannelDelegate::PermissionRequestCallback>();
callback->owner = aliveToken();
auto defaultBehaviour = [this, deferral, args](const std::optional<const std::shared_ptr<PermissionResponse>> permissionResponse)
{
failedLog(args->put_State(COREWEBVIEW2_PERMISSION_STATE_DENY));
Expand Down Expand Up @@ -959,6 +962,7 @@ namespace flutter_inappwebview_plugin
{
if (channelDelegate) {
auto callback = std::make_unique<WebViewChannelDelegate::LoadResourceWithCustomSchemeCallback>();
callback->owner = aliveToken();
auto defaultBehaviour = [this, deferral, args](const std::optional<std::shared_ptr<CustomSchemeResponse>> response)
{
failedLog(deferral->Complete());
Expand All @@ -984,6 +988,7 @@ namespace flutter_inappwebview_plugin

if (settings->useShouldInterceptRequest) {
auto callback = std::make_unique<WebViewChannelDelegate::ShouldInterceptRequestCallback>();
callback->owner = aliveToken();
auto defaultBehaviour = [this, deferral, args](const std::optional<std::shared_ptr<WebResourceResponse>> response)
{
failedLog(deferral->Complete());
Expand Down Expand Up @@ -1164,6 +1169,7 @@ namespace flutter_inappwebview_plugin
);

auto callback = std::make_unique<WebViewChannelDelegate::DownloadStartRequestCallback>();
callback->owner = aliveToken();
auto defaultBehaviour = [this, deferral, args](const std::optional<const std::shared_ptr<DownloadStartResponse>> response)
{
failedLog(deferral->Complete());
Expand Down Expand Up @@ -1271,6 +1277,7 @@ namespace flutter_inappwebview_plugin
);

auto callback = std::make_unique<WebViewChannelDelegate::ReceivedClientCertRequestCallback>();
callback->owner = aliveToken();
auto defaultBehaviour = [this, deferral, args](const std::optional<std::shared_ptr<ClientCertResponse>> response)
{
failedLog(deferral->Complete());
Expand Down Expand Up @@ -1362,6 +1369,7 @@ namespace flutter_inappwebview_plugin
);

auto callback = std::make_unique<WebViewChannelDelegate::ReceivedHttpAuthRequestCallback>();
callback->owner = aliveToken();
auto defaultBehaviour = [this, deferral, args](const std::optional<std::shared_ptr<HttpAuthResponse>> response)
{
failedLog(deferral->Complete());
Expand Down Expand Up @@ -1449,6 +1457,7 @@ namespace flutter_inappwebview_plugin
);

auto callback = std::make_unique<WebViewChannelDelegate::ReceivedServerTrustAuthRequestCallback>();
callback->owner = aliveToken();
auto defaultBehaviour = [this, deferral, args](const std::optional<std::shared_ptr<ServerTrustAuthResponse>> response)
{
failedLog(deferral->Complete());
Expand Down Expand Up @@ -1565,6 +1574,7 @@ namespace flutter_inappwebview_plugin
static_cast<bool>(isUserInitiated));

auto callback = std::make_unique<WebViewChannelDelegate::LaunchingExternalUriSchemeCallback>();
callback->owner = aliveToken();
auto defaultBehaviour = [deferral, args](const std::optional<std::shared_ptr<LaunchingExternalUriSchemeResponse>> response)
{
failedLog(args->put_Cancel(FALSE));
Expand Down Expand Up @@ -1684,6 +1694,7 @@ namespace flutter_inappwebview_plugin
auto request = std::make_shared<NotificationReceivedRequest>(senderOriginValue, notificationControllerId, notificationPtr);

auto callback = std::make_unique<WebViewChannelDelegate::NotificationReceivedCallback>();
callback->owner = aliveToken();
auto defaultBehaviour = [deferral, args, notificationController](const std::optional<std::shared_ptr<NotificationReceivedResponse>> response)
{
failedLog(args->put_Handled(FALSE));
Expand Down Expand Up @@ -1750,6 +1761,7 @@ namespace flutter_inappwebview_plugin
SaveAsKindFromOptionalInteger(std::optional<int64_t>{ static_cast<int64_t>(kind) }));

auto callback = std::make_unique<WebViewChannelDelegate::SaveAsUIShowingCallback>();
callback->owner = aliveToken();
auto defaultBehaviour = [deferral](const std::optional<std::shared_ptr<SaveAsUIShowingResponse>> response)
{
if (deferral) {
Expand Down Expand Up @@ -1826,6 +1838,7 @@ namespace flutter_inappwebview_plugin
static_cast<bool>(suppressDefaultPolicy));

auto callback = std::make_unique<WebViewChannelDelegate::SaveFileSecurityCheckStartingCallback>();
callback->owner = aliveToken();
auto defaultBehaviour = [deferral](const std::optional<std::shared_ptr<SaveFileSecurityCheckStartingResponse>> response)
{
if (deferral) {
Expand Down Expand Up @@ -1893,6 +1906,7 @@ namespace flutter_inappwebview_plugin
static_cast<bool>(handled));

auto callback = std::make_unique<WebViewChannelDelegate::ScreenCaptureStartingCallback>();
callback->owner = aliveToken();
auto defaultBehaviour = [deferral](const std::optional<std::shared_ptr<ScreenCaptureStartingResponse>> response)
{
if (deferral) {
Expand Down Expand Up @@ -4153,6 +4167,7 @@ namespace flutter_inappwebview_plugin
*/

auto callback = std::make_unique<WebViewChannelDelegate::CallJsHandlerCallback>();
callback->owner = aliveToken();
callback->defaultBehaviour = [this, callHandlerID](const std::optional<const flutter::EncodableValue*> response)
{
std::string json = "null";
Expand Down Expand Up @@ -4191,6 +4206,9 @@ namespace flutter_inappwebview_plugin
InAppWebView::~InAppWebView()
{
debugLog("dealloc InAppWebView");
// Expire before tearing anything down: a Dart reply arriving from here on
// must find a dead token and drop the call.
aliveToken_.reset();
WebViewDropTarget::UnregisterWebView(this);
userContentController = nullptr;
if (webView) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define FLUTTER_INAPPWEBVIEW_PLUGIN_IN_APP_WEBVIEW_H_

#include <functional>
#include <memory>
#include <WebView2.h>
#include <wil/com.h>
#include <windows.ui.composition.desktop.h>
Expand Down Expand Up @@ -253,6 +254,13 @@ namespace flutter_inappwebview_plugin
return pageFrameId_;
}

// Lifetime token for callbacks that round-trip through Dart: they outlive
// this webview when a tab closes mid-call, and must not touch it afterwards.
std::weak_ptr<void> aliveToken() const
{
return aliveToken_;
}

static bool isSslError(const COREWEBVIEW2_WEB_ERROR_STATUS& webErrorStatus);
private:
// custom_platform_view
Expand Down Expand Up @@ -280,6 +288,7 @@ namespace flutter_inappwebview_plugin
std::map<std::string, std::unique_ptr<WebMessageListener>> webMessageListeners_;
std::map<std::string, std::shared_ptr<WebNotificationController>> webNotificationControllers_;
std::map<std::string, std::shared_ptr<PrintJobController>> printJobControllers_;
std::shared_ptr<int> aliveToken_ = std::make_shared<int>(0);

void registerEventHandlers();
void registerSurfaceEventHandlers();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,68 @@
#include <gtest/gtest.h>

#include <memory>

#include "in_app_webview/webview_visibility_state.h"
#include "types/base_callback_result.h"

namespace flutter_inappwebview_plugin::test {

namespace {

std::unique_ptr<BaseCallbackResult<bool>> makeCallback(bool* ran) {
auto callback = std::make_unique<BaseCallbackResult<bool>>();
callback->decodeResult = [](const flutter::EncodableValue* value) {
return std::make_optional(std::get<bool>(*value));
};
callback->defaultBehaviour = [ran](const std::optional<bool>) {
*ran = true;
};
callback->error = [ran](const std::string&, const std::string&,
const flutter::EncodableValue*) { *ran = true; };
return callback;
}

} // namespace

TEST(BaseCallbackResult, RunsHandlersWithoutOwner) {
auto ran = false;
auto callback = makeCallback(&ran);
callback->Success(flutter::EncodableValue(true));
EXPECT_TRUE(ran);
}

TEST(BaseCallbackResult, RunsHandlersWhileOwnerIsAlive) {
auto ran = false;
auto owner = std::make_shared<int>(0);
auto callback = makeCallback(&ran);
callback->owner = owner;
callback->Success(flutter::EncodableValue(true));
EXPECT_TRUE(ran);
}

TEST(BaseCallbackResult, DropsHandlersOnceOwnerIsGone) {
auto ran = false;
auto owner = std::make_shared<int>(0);
auto callback = makeCallback(&ran);
callback->owner = owner;
owner.reset();
callback->Success(flutter::EncodableValue(true));
EXPECT_FALSE(ran);
}

TEST(BaseCallbackResult, DropsErrorAndNotImplementedOnceOwnerIsGone) {
auto ran = false;
auto owner = std::make_shared<int>(0);
auto errorCallback = makeCallback(&ran);
errorCallback->owner = owner;
auto notImplementedCallback = makeCallback(&ran);
notImplementedCallback->owner = owner;
owner.reset();
errorCallback->Error("code", "message");
notImplementedCallback->NotImplemented();
EXPECT_FALSE(ran);
}

TEST(WebViewVisibilityState, StartsVisible) {
WebViewVisibilityState state;
EXPECT_TRUE(state.shouldBeVisible());
Expand Down
26 changes: 26 additions & 0 deletions flutter_inappwebview_windows/windows/types/base_callback_result.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@

#include <flutter/method_result_functions.h>
#include <flutter/standard_method_codec.h>
#include <memory>
#include <optional>

#include "../utils/log.h"

namespace flutter_inappwebview_plugin
{
template <typename T>
Expand All @@ -18,10 +21,27 @@ namespace flutter_inappwebview_plugin
std::function<void(const std::optional<T> result)> defaultBehaviour = [](const std::optional<T> result) {};
std::function<std::optional<T>(const flutter::EncodableValue* result)> decodeResult = [](const flutter::EncodableValue* result) { return std::nullopt; };

// Owner of the objects captured by the handlers above. Dart may reply long
// after that owner is gone (a tab closed while an RPC was in flight), so an
// expired token makes every handler a no-op instead of a use-after-free.
std::optional<std::weak_ptr<void>> owner;

bool isOwnerGone() const
{
if (!owner.has_value() || !owner.value().expired()) {
return false;
}
debugLog("dropping a Dart reply that outlived its owner");
return true;
}

BaseCallbackResult<T>() :
MethodResultFunctions(
[this](const flutter::EncodableValue* val)
{
if (isOwnerGone()) {
return;
}
std::optional<T> result = decodeResult ? decodeResult(val) : std::nullopt;
auto shouldRunDefaultBehaviour = false;
if (result.has_value()) {
Expand All @@ -36,12 +56,18 @@ namespace flutter_inappwebview_plugin
},
[this](const std::string& error_code, const std::string& error_message, const flutter::EncodableValue* error_details)
{
if (isOwnerGone()) {
return;
}
if (error) {
error(error_code, error_message, error_details);
}
},
[this]()
{
if (isOwnerGone()) {
return;
}
if (defaultBehaviour) {
defaultBehaviour(std::nullopt);
}
Expand Down
Loading