Skip to content

Commit

Permalink
[iOS] Create safe WebKit completion block utility functions.
Browse files Browse the repository at this point in the history
Completion blocks provided by WebKit to the WKUIDelegate contain a
helper object that will throw an exception if the block is deallocated
before being executed.  This CL adds helper functions that add a
similar mechanism that ensures that these blocks are executed before
deallocation.

Internally, these functions use wrapper objects that call the WebKit
blocks upon deallocation.  These wrapper objects are not exposed
externally in an attempt to ensure that their lifetime is tightly
coupled with the lifetime of the block.  Exposing the wrappers
publicly allows for the possibility of mistakenly extending the
lifetime of the WebKit blocks due to being erroneously retained.

Once the C++14 move semantics for lambda introducers are allowed by
the style guide, we can instead construct these call checker objects
using C++ objects whose destruction can be guaranteed to occur at a
certain time.

Bug: 915119
Change-Id: Ifeac4359f5436ede281def418eb81b1620ba2198
Reviewed-on: https://chromium-review.googlesource.com/c/1392356
Commit-Queue: Kurt Horimoto <kkhorimoto@chromium.org>
Reviewed-by: Eugene But <eugenebut@chromium.org>
Cr-Commit-Position: refs/heads/master@{#621424}
  • Loading branch information
Kurt Horimoto authored and Commit Bot committed Jan 10, 2019
1 parent 32d3995 commit 7e502f7
Show file tree
Hide file tree
Showing 5 changed files with 395 additions and 16 deletions.
3 changes: 3 additions & 0 deletions ios/chrome/browser/ui/dialogs/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ source_set("unit_tests") {
source_set("dialogs_internal") {
configs += [ "//build/config/compiler:enable_arc" ]
sources = [
"completion_block_util.h",
"completion_block_util.mm",
"dialog_presenter.h",
"dialog_presenter.mm",
"java_script_dialog_presenter_impl.h",
Expand Down Expand Up @@ -78,6 +80,7 @@ source_set("unit_tests_internal") {
configs += [ "//build/config/compiler:enable_arc" ]
testonly = true
sources = [
"completion_block_util_unittest.mm",
"dialog_presenter_unittest.mm",
"java_script_dialog_presenter_impl_unittest.mm",
"nsurl_protection_space_util_unittest.mm",
Expand Down
31 changes: 31 additions & 0 deletions ios/chrome/browser/ui/dialogs/completion_block_util.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef IOS_CHROME_BROWSER_UI_DIALOGS_COMPLETION_BLOCK_UTIL_H_
#define IOS_CHROME_BROWSER_UI_DIALOGS_COMPLETION_BLOCK_UTIL_H_

#import "ios/web/public/java_script_dialog_callback.h"

namespace completion_block_util {

// Typedefs are required to return Objective C blocks from C++ util functions.
typedef void (^AlertCallback)(void);
typedef void (^ConfirmCallback)(BOOL isConfirmed);
typedef void (^PromptCallback)(NSString* input);
typedef void (^HTTPAuthCallack)(NSString* user, NSString* password);

// Completion callbacks provided by web// for dialogs have a built-in
// mechanism that throws an exception if they are deallocated before being
// executed. The following utility functions convert these web// completion
// blocks into safer versions that ensure that the original block is called
// before being deallocated.
AlertCallback GetSafeJavaScriptAlertCompletion(AlertCallback callback);
ConfirmCallback GetSafeJavaScriptConfirmationCompletion(
ConfirmCallback callback);
PromptCallback GetSafeJavaScriptPromptCompletion(PromptCallback callback);
HTTPAuthCallack GetSafeHTTPAuthCompletion(HTTPAuthCallack callback);

} // completion_block_util

#endif // IOS_CHROME_BROWSER_UI_DIALOGS_COMPLETION_BLOCK_UTIL_H_
239 changes: 239 additions & 0 deletions ios/chrome/browser/ui/dialogs/completion_block_util.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/ui/dialogs/completion_block_util.h"

#include "base/logging.h"

#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif

using completion_block_util::AlertCallback;
using completion_block_util::ConfirmCallback;
using completion_block_util::PromptCallback;
using completion_block_util::HTTPAuthCallack;

#pragma mark - WebCompletionWrapper

// Superclass for wrapper objects through which completion blocks from web//
// are executed. The blocks passed on creation will be called before the
// wrapper is deallocated in order to prevent exceptions from being thrown.
// TODO(crbug.com/918189): Convert to C++ callback checker object when C++14
// lambda expressions are allowed by the Chromium style guide.
@interface WebCompletionWrapper : NSObject

// Whether the completion block has been executed.
@property(nonatomic, assign, getter=isExecuted) BOOL executed;

// Executes the wrapped completion block for cancelled dialogs.
- (void)executeCompletionForCancellation;

@end

@implementation WebCompletionWrapper

- (void)dealloc {
if (!self.executed)
[self executeCompletionForCancellation];
}

- (void)executeCompletionForCancellation {
NOTREACHED() << "Subclasses must implement.";
}

@end

#pragma mark - JavaScriptAlertCompletionWrapper

// Completion wrapper for JavaScript alert completions.
@interface JavaScriptAlertCompletionWrapper : WebCompletionWrapper {
AlertCallback _callback;
}

// Initializer that takes a JavaScript dialog callback.
- (instancetype)initWithCallback:(AlertCallback)callback
NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;

// Executes |_callback|.
- (void)executeCallback;

@end

@implementation JavaScriptAlertCompletionWrapper

- (instancetype)initWithCallback:(AlertCallback)callback {
if (self = [super init]) {
_callback = callback;
}
return self;
}

- (void)executeCompletionForCancellation {
[self executeCallback];
}

- (void)executeCallback {
if (self.executed || !_callback)
return;
_callback();
self.executed = YES;
}

@end

#pragma mark - JavaScriptConfirmCompletionWrapper

// Completion wrapper for JavaScript confirmation completions.
@interface JavaScriptConfirmCompletionWrapper : WebCompletionWrapper {
ConfirmCallback _callback;
}

// Initializer that takes a JavaScript dialog callback.
- (instancetype)initWithCallback:(ConfirmCallback)callback
NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;

// Executes |_callback| with |confirmed|.
- (void)executeCallbackWithConfirmation:(BOOL)confirmed;

@end

@implementation JavaScriptConfirmCompletionWrapper

- (instancetype)initWithCallback:(ConfirmCallback)callback {
if (self = [super init]) {
_callback = callback;
}
return self;
}

- (void)executeCompletionForCancellation {
[self executeCallbackWithConfirmation:NO];
}

- (void)executeCallbackWithConfirmation:(BOOL)confirmed {
if (self.executed || !_callback)
return;
_callback(confirmed);
self.executed = YES;
}

@end

#pragma mark - JavaScriptPromptCompletionWrapper

// Completion wrapper for JavaScript prompt completions.
@interface JavaScriptPromptCompletionWrapper : WebCompletionWrapper {
PromptCallback _callback;
}

// Initializer that takes a JavaScript dialog callback.
- (instancetype)initWithCallback:(PromptCallback)callback
NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;

// Executes |_callback| with |input|.
- (void)executeCallbackWithInput:(NSString*)input;

@end

@implementation JavaScriptPromptCompletionWrapper

- (instancetype)initWithCallback:(PromptCallback)callback {
if (self = [super init]) {
_callback = callback;
}
return self;
}

- (void)executeCompletionForCancellation {
[self executeCallbackWithInput:nil];
}

- (void)executeCallbackWithInput:(NSString*)input {
if (self.executed || !_callback)
return;
_callback(input);
self.executed = YES;
}

@end

#pragma mark - HTTPAuthCompletionWrapper

// Web completion wrapper for HTTP authentication dialog completions.
@interface HTTPAuthCompletionWrapper : WebCompletionWrapper {
HTTPAuthCallack _callback;
}

// Initializer that takes an HTTP authentication dialog callback.
- (instancetype)initWithCallback:(HTTPAuthCallack)callback
NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;

// Executes |_callback| with |user| and |password|.
- (void)executeCallbackWithUser:(NSString*)user password:(NSString*)password;

@end

@implementation HTTPAuthCompletionWrapper

- (instancetype)initWithCallback:(HTTPAuthCallack)callback {
if (self = [super init]) {
_callback = callback;
}
return self;
}

- (void)executeCompletionForCancellation {
[self executeCallbackWithUser:nil password:nil];
}

- (void)executeCallbackWithUser:(NSString*)user password:(NSString*)password {
if (self.executed || !_callback)
return;
_callback(user, password);
self.executed = YES;
}

@end

namespace completion_block_util {

AlertCallback GetSafeJavaScriptAlertCompletion(AlertCallback callback) {
JavaScriptAlertCompletionWrapper* wrapper =
[[JavaScriptAlertCompletionWrapper alloc] initWithCallback:callback];
return ^{
[wrapper executeCallback];
};
}

ConfirmCallback GetSafeJavaScriptConfirmationCompletion(
ConfirmCallback callback) {
JavaScriptConfirmCompletionWrapper* wrapper =
[[JavaScriptConfirmCompletionWrapper alloc] initWithCallback:callback];
return ^(BOOL is_confirmed) {
[wrapper executeCallbackWithConfirmation:is_confirmed];
};
}

PromptCallback GetSafeJavaScriptPromptCompletion(PromptCallback callback) {
JavaScriptPromptCompletionWrapper* wrapper =
[[JavaScriptPromptCompletionWrapper alloc] initWithCallback:callback];
return ^(NSString* text_input) {
[wrapper executeCallbackWithInput:text_input];
};
}

HTTPAuthCallack GetSafeHTTPAuthCompletion(HTTPAuthCallack callback) {
HTTPAuthCompletionWrapper* wrapper =
[[HTTPAuthCompletionWrapper alloc] initWithCallback:callback];
return ^(NSString* user, NSString* password) {
[wrapper executeCallbackWithUser:user password:password];
};
}

} // completion_block_util
89 changes: 89 additions & 0 deletions ios/chrome/browser/ui/dialogs/completion_block_util_unittest.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/ui/dialogs/completion_block_util.h"

#include "base/bind.h"
#include "testing/platform_test.h"

#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif

using SafeWebCompletionTest = PlatformTest;
using completion_block_util::AlertCallback;
using completion_block_util::ConfirmCallback;
using completion_block_util::PromptCallback;
using completion_block_util::HTTPAuthCallack;
using completion_block_util::GetSafeJavaScriptAlertCompletion;
using completion_block_util::GetSafeJavaScriptConfirmationCompletion;
using completion_block_util::GetSafeJavaScriptPromptCompletion;
using completion_block_util::GetSafeHTTPAuthCompletion;

// Tests that a safe JavaScript alert completion block executes the original
// callback if deallocated.
TEST_F(SafeWebCompletionTest, JavaScriptAlert) {
__block BOOL callback_executed = NO;
@autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"
AlertCallback callback = GetSafeJavaScriptAlertCompletion(^(void) {
callback_executed = YES;
});
#pragma clang diagnostic pop
}
EXPECT_TRUE(callback_executed);
}

// Tests that a safe JavaScript confirmation completion block executes the
// original callback if deallocated.
TEST_F(SafeWebCompletionTest, JavaScriptConfirmation) {
__block BOOL callback_executed = NO;
@autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"
ConfirmCallback callback =
GetSafeJavaScriptConfirmationCompletion(^(bool confirmed) {
EXPECT_FALSE(confirmed);
callback_executed = YES;
});
#pragma clang diagnostic pop
}
EXPECT_TRUE(callback_executed);
}

// Tests that a safe JavaScript prompt completion block executes the original
// callback if deallocated.
TEST_F(SafeWebCompletionTest, JavaScriptPrompt) {
__block BOOL callback_executed = NO;
@autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"
PromptCallback callback =
GetSafeJavaScriptPromptCompletion(^(NSString* input) {
EXPECT_FALSE(input);
callback_executed = YES;
});
#pragma clang diagnostic pop
}
EXPECT_TRUE(callback_executed);
}

// Tests that a safe HTTP authentication completion block executes the original
// callback if deallocated.
TEST_F(SafeWebCompletionTest, HTTPAuth) {
__block BOOL callback_executed = NO;
@autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"
HTTPAuthCallack callback =
GetSafeHTTPAuthCompletion(^(NSString* user, NSString* password) {
EXPECT_FALSE(user);
EXPECT_FALSE(password);
callback_executed = YES;
});
#pragma clang diagnostic pop
}
EXPECT_TRUE(callback_executed);
}
Loading

0 comments on commit 7e502f7

Please sign in to comment.