Skip to content

Commit

Permalink
Adds support for POST request with bodies on WKWebView.
Browse files Browse the repository at this point in the history
These requests are now handled through a javascript hack to work around
this webkit bug: https://bugs.webkit.org/show_bug.cgi?id=145410.

BUG=489692

Review URL: https://codereview.chromium.org/1375023002

Cr-Commit-Position: refs/heads/master@{#363256}
(cherry picked from commit 8d13d52)

Review URL: https://codereview.chromium.org/1516303002 .

Cr-Commit-Position: refs/branch-heads/2564@{crosswalk-project#326}
Cr-Branched-From: 1283eca-refs/heads/master@{#359700}
  • Loading branch information
Arcank committed Dec 11, 2015
1 parent f57face commit b35e6bf
Show file tree
Hide file tree
Showing 9 changed files with 447 additions and 9 deletions.
3 changes: 3 additions & 0 deletions ios/web/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ source_set("web") {
"web_state/js/crw_js_invoke_parameter_queue.mm",
"web_state/js/crw_js_plugin_placeholder_manager.h",
"web_state/js/crw_js_plugin_placeholder_manager.mm",
"web_state/js/crw_js_post_request_loader.h",
"web_state/js/crw_js_post_request_loader.h",
"web_state/js/crw_js_window_id_manager.h",
"web_state/js/crw_js_window_id_manager.mm",
"web_state/js/page_script_util.h",
Expand Down Expand Up @@ -429,6 +431,7 @@ test("ios_web_unittests") {
"web_state/js/crw_js_early_script_manager_unittest.mm",
"web_state/js/crw_js_injection_manager_unittest.mm",
"web_state/js/crw_js_invoke_parameter_queue_unittest.mm",
"web_state/js/crw_js_post_request_loader_unittest.mm",
"web_state/js/crw_js_window_id_manager_unittest.mm",
"web_state/js/page_script_util_unittest.mm",
"web_state/ui/crw_static_file_web_view_unittest.mm",
Expand Down
4 changes: 4 additions & 0 deletions ios/web/ios_web.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@
'web_state/js/credential_util.mm',
'web_state/js/crw_js_early_script_manager.h',
'web_state/js/crw_js_early_script_manager.mm',
'web_state/js/crw_js_post_request_loader.h',
'web_state/js/crw_js_post_request_loader.mm',
'web_state/js/crw_js_injection_manager.mm',
'web_state/js/crw_js_injection_receiver.mm',
'web_state/js/crw_js_invoke_parameter_queue.h',
Expand Down Expand Up @@ -416,12 +418,14 @@
'ios_web_js_bundle_wk',
],
'sources': [
'web_state/js/resources/post_request.js',
'web_state/js/resources/plugin_placeholder.js',
'web_state/js/resources/window_id.js',
'webui/resources/web_ui.js',
],
'link_settings': {
'mac_bundle_resources': [
'<(SHARED_INTERMEDIATE_DIR)/post_request.js',
'<(SHARED_INTERMEDIATE_DIR)/plugin_placeholder.js',
'<(SHARED_INTERMEDIATE_DIR)/window_id.js',
'<(SHARED_INTERMEDIATE_DIR)/web_ui.js',
Expand Down
1 change: 1 addition & 0 deletions ios/web/ios_web_unittests.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
'web_state/js/crw_js_early_script_manager_unittest.mm',
'web_state/js/crw_js_injection_manager_unittest.mm',
'web_state/js/crw_js_invoke_parameter_queue_unittest.mm',
'web_state/js/crw_js_post_request_loader_unittest.mm',
'web_state/js/crw_js_window_id_manager_unittest.mm',
'web_state/js/page_script_util_unittest.mm',
'web_state/ui/crw_static_file_web_view_unittest.mm',
Expand Down
28 changes: 28 additions & 0 deletions ios/web/web_state/js/crw_js_post_request_loader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2015 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_WEB_WEB_STATE_JS_CRW_JS_POST_REQUEST_LOADER_H_
#define IOS_WEB_WEB_STATE_JS_CRW_JS_POST_REQUEST_LOADER_H_

#import <WebKit/WebKit.h>

@class CRWWKScriptMessageRouter;

// Class to load POST requests in a provided web view via JavaScript.
@interface CRWJSPOSTRequestLoader : NSObject

// Asynchronously loads a POST |request| in provided |webView|.
// It temporarily installs JavaScript message routers with |messageRouter| to
// handle HTTP errors. The |completionHandler| is called once the request has
// been executed. In case of successful request, the passed error is nil.
// The |completionHandler| must not be null. The |messageRouter| and |webView|
// must not be nil. The |request| must be a POST request.
- (void)loadPOSTRequest:(NSURLRequest*)request
inWebView:(WKWebView*)webView
messageRouter:(CRWWKScriptMessageRouter*)messageRouter
completionHandler:(void (^)(NSError*))completionHandler;

@end

#endif // IOS_WEB_WEB_STATE_JS_CRW_JS_POST_REQUEST_LOADER_H_
156 changes: 156 additions & 0 deletions ios/web/web_state/js/crw_js_post_request_loader.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright 2015 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/web/web_state/js/crw_js_post_request_loader.h"

#include "base/json/string_escape.h"
#import "base/mac/scoped_nsobject.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/web/web_state/js/page_script_util.h"
#import "ios/web/web_state/ui/crw_wk_script_message_router.h"

namespace {

// Escapes characters and encloses given string in quotes for use in JavaScript.
NSString* EscapeAndQuoteStringForJavaScript(NSString* unescapedString) {
std::string string = base::SysNSStringToUTF8(unescapedString);
return base::SysUTF8ToNSString(base::GetQuotedJSONString(string));
}

// JavaScript message handler name installed in WKWebView for request errors.
NSString* const kErrorHandlerName = @"POSTErrorHandler";

// JavaScript message handler name installed in WKWebView for successful
// request completion.
NSString* const kSuccessHandlerName = @"POSTSuccessHandler";

} // namespace

@interface CRWJSPOSTRequestLoader () {
base::scoped_nsobject<NSString> _requestScript;
}

// JavaScript used to execute POST requests. Lazily instantiated.
@property(nonatomic, copy, readonly) NSString* requestScript;

// Handler for UIApplicationDidReceiveMemoryWarningNotification.
- (void)handleMemoryWarning;

// Forms a JavaScript method call to |requestScript| that executes given
// |request|.
- (NSString*)scriptToExecutePOSTRequest:(NSURLRequest*)request;

// Converts a dictionary of HTTP request headers to a JavaScript object.
- (NSString*)JSONForJavaScriptFromRequestHeaders:(NSDictionary*)headers;

@end

@implementation CRWJSPOSTRequestLoader

- (instancetype)init {
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(handleMemoryWarning)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
}
return self;
}

- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}

- (NSString*)requestScript {
if (!_requestScript) {
_requestScript.reset([web::GetPageScript(@"post_request") copy]);
}
return _requestScript;
}

- (void)loadPOSTRequest:(NSURLRequest*)request
inWebView:(WKWebView*)webView
messageRouter:(CRWWKScriptMessageRouter*)messageRouter
completionHandler:(void (^)(NSError*))completionHandler {
DCHECK([request.HTTPMethod isEqualToString:@"POST"]);
DCHECK(webView);
DCHECK(messageRouter);
DCHECK(completionHandler);

// Install error handling and success routers.
[messageRouter setScriptMessageHandler:^(WKScriptMessage* message) {
// Cleaning up script handlers.
[messageRouter removeScriptMessageHandlerForName:kErrorHandlerName
webView:webView];
[messageRouter removeScriptMessageHandlerForName:kSuccessHandlerName
webView:webView];
completionHandler(nil);
}
name:kSuccessHandlerName
webView:webView];

[messageRouter setScriptMessageHandler:^(WKScriptMessage* message) {
NSNumber* statusCode = message.body;
NSError* error = [NSError errorWithDomain:NSURLErrorDomain
code:statusCode.integerValue
userInfo:nil];
[messageRouter removeScriptMessageHandlerForName:kErrorHandlerName
webView:webView];
[messageRouter removeScriptMessageHandlerForName:kSuccessHandlerName
webView:webView];
completionHandler(error);
}
name:kErrorHandlerName
webView:webView];

NSString* HTML =
[NSString stringWithFormat:@"<html><script>%@%@</script></html>",
self.requestScript,
[self scriptToExecutePOSTRequest:request]];
[webView loadHTMLString:HTML baseURL:request.URL];
}

#pragma mark - Private methods.

- (void)handleMemoryWarning {
// Request script can be recreated from file at any moment.
_requestScript.reset();
}

- (NSString*)scriptToExecutePOSTRequest:(NSURLRequest*)request {
NSDictionary* headers = [request allHTTPHeaderFields];
NSString* headerString = [self JSONForJavaScriptFromRequestHeaders:headers];
NSString* URLString = [[request URL] absoluteString];
NSString* contentType = headers[@"Content-Type"];
NSString* base64Data = [[request HTTPBody] base64EncodedStringWithOptions:0];

// Here |headerString| is already properly escaped when returned from
// -JSONForJavaScriptFromRequestHeaders:.
return
[NSString stringWithFormat:
@"__crPostRequestWorkaround.runPostRequest(%@, %@, %@, %@)",
EscapeAndQuoteStringForJavaScript(URLString), headerString,
EscapeAndQuoteStringForJavaScript(base64Data),
EscapeAndQuoteStringForJavaScript(contentType)];
}

- (NSString*)JSONForJavaScriptFromRequestHeaders:(NSDictionary*)headers {
if (headers) {
NSData* headerData =
[NSJSONSerialization dataWithJSONObject:headers options:0 error:nil];
if (headerData) {
// This string is properly escaped by NSJSONSerialization. It needs to
// have no quotes since JavaScripts takes this parameter as an
// Object<string, string>.
return [[[NSString alloc] initWithData:headerData
encoding:NSUTF8StringEncoding] autorelease];
}
}
return @"{}";
}

@end
105 changes: 105 additions & 0 deletions ios/web/web_state/js/crw_js_post_request_loader_unittest.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2015 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/web/web_state/js/crw_js_post_request_loader.h"

#import <WebKit/WebKit.h>

#import "base/mac/foundation_util.h"
#import "base/mac/scoped_nsobject.h"
#include "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#include "ios/web/public/test/web_test_util.h"
#import "ios/web/public/web_view_creation_util.h"
#import "ios/web/test/web_test.h"
#import "ios/web/web_state/ui/crw_wk_script_message_router.h"
#import "testing/gtest_mac.h"
#import "third_party/ocmock/OCMock/OCMock.h"

namespace base {
namespace {

typedef web::WebTest CRWJSPOSTRequestLoaderTest;

// This script takes a JavaScript blob and converts it to a base64-encoded
// string asynchronously, then is sent to XHRSendHandler message handler.
NSString* const kBlobToBase64StringScript =
@"var blobToBase64 = function(x) {"
" var reader = new window.FileReader();"
" reader.readAsDataURL(x);"
" reader.onloadend = function() {"
" base64data = reader.result;"
" window.webkit.messageHandlers.XHRSendHandler.postMessage(base64data);"
" };"
"};";

// Tests that the POST request is correctly executed through XMLHttpRequest.
TEST_F(CRWJSPOSTRequestLoaderTest, LoadsCorrectHTML) {
CR_TEST_REQUIRES_WK_WEB_VIEW();

// Set up necessary objects.
scoped_nsobject<CRWJSPOSTRequestLoader> loader(
[[CRWJSPOSTRequestLoader alloc] init]);
scoped_nsobject<WKWebView> web_view(
web::CreateWKWebView(CGRectZero, GetBrowserState()));
WKUserContentController* contentController =
web_view.get().configuration.userContentController;
scoped_nsobject<CRWWKScriptMessageRouter> messageRouter(
[[CRWWKScriptMessageRouter alloc]
initWithUserContentController:contentController]);

// Override XMLHttpRequest.send() to call kBlobToBase64StringScript.
__block BOOL overrideSuccessfull = NO;
NSString* JS = [kBlobToBase64StringScript stringByAppendingString:@";\
XMLHttpRequest.prototype.send = function(x) { blobToBase64(x); };"];
[web_view evaluateJavaScript:JS
completionHandler:^(id, NSError*) {
overrideSuccessfull = YES;
}];
base::test::ios::WaitUntilCondition(^bool {
return overrideSuccessfull;
});

NSString* post_body = @"123";

// Adds XHRSendHandler handler that checks that the POST request body is
// correct. Sets |complete| flag upon completion.
__block BOOL complete = NO;
void (^XHRSendHandler)(WKScriptMessage*) = ^(WKScriptMessage* message) {
NSString* body = base::mac::ObjCCast<NSString>(message.body);
NSArray* components = [body componentsSeparatedByString:@","];
EXPECT_EQ(components.count, 2u);
EXPECT_NSEQ(components[0], @"data:;base64");
NSData* expectedData = [post_body dataUsingEncoding:NSUTF8StringEncoding];
EXPECT_NSEQ(components[1], [expectedData base64EncodedStringWithOptions:0]);
complete = YES;
};

[messageRouter setScriptMessageHandler:XHRSendHandler
name:@"XHRSendHandler"
webView:web_view];

// Construct and perform the POST request.
NSURL* url = [NSURL URLWithString:@"http://google.com"];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
request.HTTPBody = [post_body dataUsingEncoding:NSUTF8StringEncoding];
[loader loadPOSTRequest:request
inWebView:web_view
messageRouter:messageRouter
completionHandler:^(NSError*){
}];

// Wait until the JavaScript message handler is called.
base::test::ios::WaitUntilCondition(^bool {
return complete;
});

// Clean up installed script handler.
[messageRouter removeScriptMessageHandlerForName:@"XHRSendHandler"
webView:web_view];
}

} // namespace
} // namespace base
Loading

0 comments on commit b35e6bf

Please sign in to comment.