Skip to content

Commit b35e6bf

Browse files
committed
Adds support for POST request with bodies on WKWebView.
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}
1 parent f57face commit b35e6bf

9 files changed

+447
-9
lines changed

ios/web/BUILD.gn

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ source_set("web") {
216216
"web_state/js/crw_js_invoke_parameter_queue.mm",
217217
"web_state/js/crw_js_plugin_placeholder_manager.h",
218218
"web_state/js/crw_js_plugin_placeholder_manager.mm",
219+
"web_state/js/crw_js_post_request_loader.h",
220+
"web_state/js/crw_js_post_request_loader.h",
219221
"web_state/js/crw_js_window_id_manager.h",
220222
"web_state/js/crw_js_window_id_manager.mm",
221223
"web_state/js/page_script_util.h",
@@ -429,6 +431,7 @@ test("ios_web_unittests") {
429431
"web_state/js/crw_js_early_script_manager_unittest.mm",
430432
"web_state/js/crw_js_injection_manager_unittest.mm",
431433
"web_state/js/crw_js_invoke_parameter_queue_unittest.mm",
434+
"web_state/js/crw_js_post_request_loader_unittest.mm",
432435
"web_state/js/crw_js_window_id_manager_unittest.mm",
433436
"web_state/js/page_script_util_unittest.mm",
434437
"web_state/ui/crw_static_file_web_view_unittest.mm",

ios/web/ios_web.gyp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@
232232
'web_state/js/credential_util.mm',
233233
'web_state/js/crw_js_early_script_manager.h',
234234
'web_state/js/crw_js_early_script_manager.mm',
235+
'web_state/js/crw_js_post_request_loader.h',
236+
'web_state/js/crw_js_post_request_loader.mm',
235237
'web_state/js/crw_js_injection_manager.mm',
236238
'web_state/js/crw_js_injection_receiver.mm',
237239
'web_state/js/crw_js_invoke_parameter_queue.h',
@@ -416,12 +418,14 @@
416418
'ios_web_js_bundle_wk',
417419
],
418420
'sources': [
421+
'web_state/js/resources/post_request.js',
419422
'web_state/js/resources/plugin_placeholder.js',
420423
'web_state/js/resources/window_id.js',
421424
'webui/resources/web_ui.js',
422425
],
423426
'link_settings': {
424427
'mac_bundle_resources': [
428+
'<(SHARED_INTERMEDIATE_DIR)/post_request.js',
425429
'<(SHARED_INTERMEDIATE_DIR)/plugin_placeholder.js',
426430
'<(SHARED_INTERMEDIATE_DIR)/window_id.js',
427431
'<(SHARED_INTERMEDIATE_DIR)/web_ui.js',

ios/web/ios_web_unittests.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
'web_state/js/crw_js_early_script_manager_unittest.mm',
6464
'web_state/js/crw_js_injection_manager_unittest.mm',
6565
'web_state/js/crw_js_invoke_parameter_queue_unittest.mm',
66+
'web_state/js/crw_js_post_request_loader_unittest.mm',
6667
'web_state/js/crw_js_window_id_manager_unittest.mm',
6768
'web_state/js/page_script_util_unittest.mm',
6869
'web_state/ui/crw_static_file_web_view_unittest.mm',
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2015 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#ifndef IOS_WEB_WEB_STATE_JS_CRW_JS_POST_REQUEST_LOADER_H_
6+
#define IOS_WEB_WEB_STATE_JS_CRW_JS_POST_REQUEST_LOADER_H_
7+
8+
#import <WebKit/WebKit.h>
9+
10+
@class CRWWKScriptMessageRouter;
11+
12+
// Class to load POST requests in a provided web view via JavaScript.
13+
@interface CRWJSPOSTRequestLoader : NSObject
14+
15+
// Asynchronously loads a POST |request| in provided |webView|.
16+
// It temporarily installs JavaScript message routers with |messageRouter| to
17+
// handle HTTP errors. The |completionHandler| is called once the request has
18+
// been executed. In case of successful request, the passed error is nil.
19+
// The |completionHandler| must not be null. The |messageRouter| and |webView|
20+
// must not be nil. The |request| must be a POST request.
21+
- (void)loadPOSTRequest:(NSURLRequest*)request
22+
inWebView:(WKWebView*)webView
23+
messageRouter:(CRWWKScriptMessageRouter*)messageRouter
24+
completionHandler:(void (^)(NSError*))completionHandler;
25+
26+
@end
27+
28+
#endif // IOS_WEB_WEB_STATE_JS_CRW_JS_POST_REQUEST_LOADER_H_
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright 2015 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#import "ios/web/web_state/js/crw_js_post_request_loader.h"
6+
7+
#include "base/json/string_escape.h"
8+
#import "base/mac/scoped_nsobject.h"
9+
#import "base/strings/sys_string_conversions.h"
10+
#import "ios/web/web_state/js/page_script_util.h"
11+
#import "ios/web/web_state/ui/crw_wk_script_message_router.h"
12+
13+
namespace {
14+
15+
// Escapes characters and encloses given string in quotes for use in JavaScript.
16+
NSString* EscapeAndQuoteStringForJavaScript(NSString* unescapedString) {
17+
std::string string = base::SysNSStringToUTF8(unescapedString);
18+
return base::SysUTF8ToNSString(base::GetQuotedJSONString(string));
19+
}
20+
21+
// JavaScript message handler name installed in WKWebView for request errors.
22+
NSString* const kErrorHandlerName = @"POSTErrorHandler";
23+
24+
// JavaScript message handler name installed in WKWebView for successful
25+
// request completion.
26+
NSString* const kSuccessHandlerName = @"POSTSuccessHandler";
27+
28+
} // namespace
29+
30+
@interface CRWJSPOSTRequestLoader () {
31+
base::scoped_nsobject<NSString> _requestScript;
32+
}
33+
34+
// JavaScript used to execute POST requests. Lazily instantiated.
35+
@property(nonatomic, copy, readonly) NSString* requestScript;
36+
37+
// Handler for UIApplicationDidReceiveMemoryWarningNotification.
38+
- (void)handleMemoryWarning;
39+
40+
// Forms a JavaScript method call to |requestScript| that executes given
41+
// |request|.
42+
- (NSString*)scriptToExecutePOSTRequest:(NSURLRequest*)request;
43+
44+
// Converts a dictionary of HTTP request headers to a JavaScript object.
45+
- (NSString*)JSONForJavaScriptFromRequestHeaders:(NSDictionary*)headers;
46+
47+
@end
48+
49+
@implementation CRWJSPOSTRequestLoader
50+
51+
- (instancetype)init {
52+
self = [super init];
53+
if (self) {
54+
[[NSNotificationCenter defaultCenter]
55+
addObserver:self
56+
selector:@selector(handleMemoryWarning)
57+
name:UIApplicationDidReceiveMemoryWarningNotification
58+
object:nil];
59+
}
60+
return self;
61+
}
62+
63+
- (void)dealloc {
64+
[[NSNotificationCenter defaultCenter] removeObserver:self];
65+
[super dealloc];
66+
}
67+
68+
- (NSString*)requestScript {
69+
if (!_requestScript) {
70+
_requestScript.reset([web::GetPageScript(@"post_request") copy]);
71+
}
72+
return _requestScript;
73+
}
74+
75+
- (void)loadPOSTRequest:(NSURLRequest*)request
76+
inWebView:(WKWebView*)webView
77+
messageRouter:(CRWWKScriptMessageRouter*)messageRouter
78+
completionHandler:(void (^)(NSError*))completionHandler {
79+
DCHECK([request.HTTPMethod isEqualToString:@"POST"]);
80+
DCHECK(webView);
81+
DCHECK(messageRouter);
82+
DCHECK(completionHandler);
83+
84+
// Install error handling and success routers.
85+
[messageRouter setScriptMessageHandler:^(WKScriptMessage* message) {
86+
// Cleaning up script handlers.
87+
[messageRouter removeScriptMessageHandlerForName:kErrorHandlerName
88+
webView:webView];
89+
[messageRouter removeScriptMessageHandlerForName:kSuccessHandlerName
90+
webView:webView];
91+
completionHandler(nil);
92+
}
93+
name:kSuccessHandlerName
94+
webView:webView];
95+
96+
[messageRouter setScriptMessageHandler:^(WKScriptMessage* message) {
97+
NSNumber* statusCode = message.body;
98+
NSError* error = [NSError errorWithDomain:NSURLErrorDomain
99+
code:statusCode.integerValue
100+
userInfo:nil];
101+
[messageRouter removeScriptMessageHandlerForName:kErrorHandlerName
102+
webView:webView];
103+
[messageRouter removeScriptMessageHandlerForName:kSuccessHandlerName
104+
webView:webView];
105+
completionHandler(error);
106+
}
107+
name:kErrorHandlerName
108+
webView:webView];
109+
110+
NSString* HTML =
111+
[NSString stringWithFormat:@"<html><script>%@%@</script></html>",
112+
self.requestScript,
113+
[self scriptToExecutePOSTRequest:request]];
114+
[webView loadHTMLString:HTML baseURL:request.URL];
115+
}
116+
117+
#pragma mark - Private methods.
118+
119+
- (void)handleMemoryWarning {
120+
// Request script can be recreated from file at any moment.
121+
_requestScript.reset();
122+
}
123+
124+
- (NSString*)scriptToExecutePOSTRequest:(NSURLRequest*)request {
125+
NSDictionary* headers = [request allHTTPHeaderFields];
126+
NSString* headerString = [self JSONForJavaScriptFromRequestHeaders:headers];
127+
NSString* URLString = [[request URL] absoluteString];
128+
NSString* contentType = headers[@"Content-Type"];
129+
NSString* base64Data = [[request HTTPBody] base64EncodedStringWithOptions:0];
130+
131+
// Here |headerString| is already properly escaped when returned from
132+
// -JSONForJavaScriptFromRequestHeaders:.
133+
return
134+
[NSString stringWithFormat:
135+
@"__crPostRequestWorkaround.runPostRequest(%@, %@, %@, %@)",
136+
EscapeAndQuoteStringForJavaScript(URLString), headerString,
137+
EscapeAndQuoteStringForJavaScript(base64Data),
138+
EscapeAndQuoteStringForJavaScript(contentType)];
139+
}
140+
141+
- (NSString*)JSONForJavaScriptFromRequestHeaders:(NSDictionary*)headers {
142+
if (headers) {
143+
NSData* headerData =
144+
[NSJSONSerialization dataWithJSONObject:headers options:0 error:nil];
145+
if (headerData) {
146+
// This string is properly escaped by NSJSONSerialization. It needs to
147+
// have no quotes since JavaScripts takes this parameter as an
148+
// Object<string, string>.
149+
return [[[NSString alloc] initWithData:headerData
150+
encoding:NSUTF8StringEncoding] autorelease];
151+
}
152+
}
153+
return @"{}";
154+
}
155+
156+
@end
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2015 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#import "ios/web/web_state/js/crw_js_post_request_loader.h"
6+
7+
#import <WebKit/WebKit.h>
8+
9+
#import "base/mac/foundation_util.h"
10+
#import "base/mac/scoped_nsobject.h"
11+
#include "base/strings/sys_string_conversions.h"
12+
#import "base/test/ios/wait_util.h"
13+
#include "ios/web/public/test/web_test_util.h"
14+
#import "ios/web/public/web_view_creation_util.h"
15+
#import "ios/web/test/web_test.h"
16+
#import "ios/web/web_state/ui/crw_wk_script_message_router.h"
17+
#import "testing/gtest_mac.h"
18+
#import "third_party/ocmock/OCMock/OCMock.h"
19+
20+
namespace base {
21+
namespace {
22+
23+
typedef web::WebTest CRWJSPOSTRequestLoaderTest;
24+
25+
// This script takes a JavaScript blob and converts it to a base64-encoded
26+
// string asynchronously, then is sent to XHRSendHandler message handler.
27+
NSString* const kBlobToBase64StringScript =
28+
@"var blobToBase64 = function(x) {"
29+
" var reader = new window.FileReader();"
30+
" reader.readAsDataURL(x);"
31+
" reader.onloadend = function() {"
32+
" base64data = reader.result;"
33+
" window.webkit.messageHandlers.XHRSendHandler.postMessage(base64data);"
34+
" };"
35+
"};";
36+
37+
// Tests that the POST request is correctly executed through XMLHttpRequest.
38+
TEST_F(CRWJSPOSTRequestLoaderTest, LoadsCorrectHTML) {
39+
CR_TEST_REQUIRES_WK_WEB_VIEW();
40+
41+
// Set up necessary objects.
42+
scoped_nsobject<CRWJSPOSTRequestLoader> loader(
43+
[[CRWJSPOSTRequestLoader alloc] init]);
44+
scoped_nsobject<WKWebView> web_view(
45+
web::CreateWKWebView(CGRectZero, GetBrowserState()));
46+
WKUserContentController* contentController =
47+
web_view.get().configuration.userContentController;
48+
scoped_nsobject<CRWWKScriptMessageRouter> messageRouter(
49+
[[CRWWKScriptMessageRouter alloc]
50+
initWithUserContentController:contentController]);
51+
52+
// Override XMLHttpRequest.send() to call kBlobToBase64StringScript.
53+
__block BOOL overrideSuccessfull = NO;
54+
NSString* JS = [kBlobToBase64StringScript stringByAppendingString:@";\
55+
XMLHttpRequest.prototype.send = function(x) { blobToBase64(x); };"];
56+
[web_view evaluateJavaScript:JS
57+
completionHandler:^(id, NSError*) {
58+
overrideSuccessfull = YES;
59+
}];
60+
base::test::ios::WaitUntilCondition(^bool {
61+
return overrideSuccessfull;
62+
});
63+
64+
NSString* post_body = @"123";
65+
66+
// Adds XHRSendHandler handler that checks that the POST request body is
67+
// correct. Sets |complete| flag upon completion.
68+
__block BOOL complete = NO;
69+
void (^XHRSendHandler)(WKScriptMessage*) = ^(WKScriptMessage* message) {
70+
NSString* body = base::mac::ObjCCast<NSString>(message.body);
71+
NSArray* components = [body componentsSeparatedByString:@","];
72+
EXPECT_EQ(components.count, 2u);
73+
EXPECT_NSEQ(components[0], @"data:;base64");
74+
NSData* expectedData = [post_body dataUsingEncoding:NSUTF8StringEncoding];
75+
EXPECT_NSEQ(components[1], [expectedData base64EncodedStringWithOptions:0]);
76+
complete = YES;
77+
};
78+
79+
[messageRouter setScriptMessageHandler:XHRSendHandler
80+
name:@"XHRSendHandler"
81+
webView:web_view];
82+
83+
// Construct and perform the POST request.
84+
NSURL* url = [NSURL URLWithString:@"http://google.com"];
85+
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
86+
request.HTTPMethod = @"POST";
87+
request.HTTPBody = [post_body dataUsingEncoding:NSUTF8StringEncoding];
88+
[loader loadPOSTRequest:request
89+
inWebView:web_view
90+
messageRouter:messageRouter
91+
completionHandler:^(NSError*){
92+
}];
93+
94+
// Wait until the JavaScript message handler is called.
95+
base::test::ios::WaitUntilCondition(^bool {
96+
return complete;
97+
});
98+
99+
// Clean up installed script handler.
100+
[messageRouter removeScriptMessageHandlerForName:@"XHRSendHandler"
101+
webView:web_view];
102+
}
103+
104+
} // namespace
105+
} // namespace base

0 commit comments

Comments
 (0)