diff --git a/ios/web/BUILD.gn b/ios/web/BUILD.gn index fd859d17c5a9f..cbbab0a8b0f98 100644 --- a/ios/web/BUILD.gn +++ b/ios/web/BUILD.gn @@ -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", @@ -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", diff --git a/ios/web/ios_web.gyp b/ios/web/ios_web.gyp index 8c8b43f9daf50..2477045c18086 100644 --- a/ios/web/ios_web.gyp +++ b/ios/web/ios_web.gyp @@ -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', @@ -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', diff --git a/ios/web/ios_web_unittests.gyp b/ios/web/ios_web_unittests.gyp index 160a923c3b22a..0e790102bdf25 100644 --- a/ios/web/ios_web_unittests.gyp +++ b/ios/web/ios_web_unittests.gyp @@ -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', diff --git a/ios/web/web_state/js/crw_js_post_request_loader.h b/ios/web/web_state/js/crw_js_post_request_loader.h new file mode 100644 index 0000000000000..ffcde3bc7c060 --- /dev/null +++ b/ios/web/web_state/js/crw_js_post_request_loader.h @@ -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 + +@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_ diff --git a/ios/web/web_state/js/crw_js_post_request_loader.mm b/ios/web/web_state/js/crw_js_post_request_loader.mm new file mode 100644 index 0000000000000..4c9eea113cd58 --- /dev/null +++ b/ios/web/web_state/js/crw_js_post_request_loader.mm @@ -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 _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:@"", + 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. + return [[[NSString alloc] initWithData:headerData + encoding:NSUTF8StringEncoding] autorelease]; + } + } + return @"{}"; +} + +@end diff --git a/ios/web/web_state/js/crw_js_post_request_loader_unittest.mm b/ios/web/web_state/js/crw_js_post_request_loader_unittest.mm new file mode 100644 index 0000000000000..aa6a4d0b0068e --- /dev/null +++ b/ios/web/web_state/js/crw_js_post_request_loader_unittest.mm @@ -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 + +#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 loader( + [[CRWJSPOSTRequestLoader alloc] init]); + scoped_nsobject web_view( + web::CreateWKWebView(CGRectZero, GetBrowserState())); + WKUserContentController* contentController = + web_view.get().configuration.userContentController; + scoped_nsobject 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(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 diff --git a/ios/web/web_state/js/resources/post_request.js b/ios/web/web_state/js/resources/post_request.js new file mode 100644 index 0000000000000..7f5ff25168fb5 --- /dev/null +++ b/ios/web/web_state/js/resources/post_request.js @@ -0,0 +1,84 @@ +// 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. + +// Provides a way to implement POST requests via XMLHttpRequest. +// Works around https://bugs.webkit.org/show_bug.cgi?id=145410 on WKWebView. + +'use strict'; + +goog.provide('__crPostRequestWorkaround'); + +/** + * Namespace for this file. + */ +__crPostRequestWorkaround = {}; + +/** + * Executes a POST request with given parameters and replaces document body with + * the response. + * @param {string} url The url of the request. + * @param {Object} headers Request headers to include in POST. + * Each header value must be expressed as a key-value pair in this object. + * @param {string} body Request body encoded with Base64. + * @param {string} contentType Content-Type header value. + */ +__crPostRequestWorkaround.runPostRequest = function( + url, headers, body, contentType) { + + /** + * Converts a Base64-encoded string to a blob. + * @param {string} byteCharacters Base64-encoded data string. + * @param {string} contentType Corresponding content type. + * @return {Blob} Binary representation of byteCharacters. + */ + var base64ToBlob = function(byteCharacters, contentType) { + contentType = contentType || ''; + var sliceSize = 512; + var byteArrays = []; + var byteCount = byteCharacters.length; + for (var offset = 0; offset < byteCount; offset += sliceSize) { + var slice = byteCharacters.slice(offset, offset + sliceSize); + var byteNumbers = new Array(slice.length); + for (var i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + var byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + return new Blob(byteArrays, {type: contentType}); + } + + /** + * Creates and executes a POST request. + * @param {string} url The url of the request. + * @param {Object} headers Request headers to include in POST. + * Each header value must be expressed as a key-value pair in this object. + * @param {string} body Request body encoded with Base64. + * @param {string} contentType Content-Type header value. + */ + var createAndSendPostRequest = function(url, headers, body, contentType) { + var request = new XMLHttpRequest(); + request.open('POST', url, false); + for (var key in headers) { + if (headers.hasOwnProperty(key)) { + request.setRequestHeader(key, headers[key]); + } + } + var blob = base64ToBlob(atob(body), contentType); + request.send(blob); + if (request.status != 200) { + throw request.status; + } + return request.responseText; + } + + document.open(); + try { + document.write(createAndSendPostRequest(url, headers, body, contentType)); + window.webkit.messageHandlers.POSTSuccessHandler.postMessage(""); + } catch(error) { + window.webkit.messageHandlers.POSTErrorHandler.postMessage(error); + } + document.close(); +} diff --git a/ios/web/web_state/ui/crw_wk_script_message_router.mm b/ios/web/web_state/ui/crw_wk_script_message_router.mm index 5699abdfb9206..0e1dfbb33e0dd 100644 --- a/ios/web/web_state/ui/crw_wk_script_message_router.mm +++ b/ios/web/web_state/ui/crw_wk_script_message_router.mm @@ -100,8 +100,12 @@ - (void)userContentController:(WKUserContentController*)userContentController - (void)tryRemoveScriptMessageHandlerForName:(NSString*)messageName webView:(WKWebView*)webView { NSMapTable* webViewToHandlerMap = [_handlers objectForKey:messageName]; - if (![webViewToHandlerMap objectForKey:webView]) + id handler = [webViewToHandlerMap objectForKey:webView]; + if (!handler) return; + // Extend the lifetime of |handler| so removeScriptMessageHandlerForName: can + // be called from inside of |handler|. + [[handler retain] autorelease]; if (webViewToHandlerMap.count == 1) { [_handlers removeObjectForKey:messageName]; [_userContentController removeScriptMessageHandlerForName:messageName]; diff --git a/ios/web/web_state/ui/crw_wk_web_view_web_controller.mm b/ios/web/web_state/ui/crw_wk_web_view_web_controller.mm index fb9433e76143c..8da1e3777fb7e 100644 --- a/ios/web/web_state/ui/crw_wk_web_view_web_controller.mm +++ b/ios/web/web_state/ui/crw_wk_web_view_web_controller.mm @@ -10,7 +10,7 @@ #include "base/ios/ios_util.h" #include "base/ios/weak_nsobject.h" #include "base/json/json_reader.h" -#include "base/mac/objc_property_releaser.h" +#import "base/mac/objc_property_releaser.h" #import "base/mac/scoped_nsobject.h" #include "base/macros.h" #include "base/metrics/histogram_macros.h" @@ -39,6 +39,7 @@ #import "ios/web/web_state/crw_pass_kit_downloader.h" #import "ios/web/web_state/error_translation_util.h" #include "ios/web/web_state/frame_info.h" +#import "ios/web/web_state/js/crw_js_post_request_loader.h" #import "ios/web/web_state/js/crw_js_window_id_manager.h" #import "ios/web/web_state/ui/crw_web_controller+protected.h" #import "ios/web/web_state/ui/crw_wk_script_message_router.h" @@ -187,6 +188,9 @@ @interface CRWWKWebViewWebController () { // Handles downloading PassKit data for WKWebView. Lazy initialized. base::scoped_nsobject _passKitDownloader; + // Object for loading POST requests with body. + base::scoped_nsobject _POSTRequestLoader; + // Whether the web page is currently performing window.history.pushState or // window.history.replaceState // Set to YES on window.history.willChangeState message. To NO on @@ -239,6 +243,15 @@ @interface CRWWKWebViewWebController () { // Downloader for PassKit files. Lazy initialized. @property(nonatomic, readonly) CRWPassKitDownloader* passKitDownloader; +// Loads POST request with body in |_wkWebView| by constructing an HTML page +// that executes the request through JavaScript and replaces document with the +// result. +// Note that this approach includes multiple body encodings and decodings, plus +// the data is passed to |_wkWebView| on main thread. +// This is necessary because WKWebView ignores POST request body. +// Workaround for https://bugs.webkit.org/show_bug.cgi?id=145410 +- (void)loadPOSTRequest:(NSMutableURLRequest*)request; + // Returns the WKWebViewConfigurationProvider associated with the web // controller's BrowserState. - (web::WKWebViewConfigurationProvider&)webViewConfigurationProvider; @@ -573,20 +586,42 @@ - (void)willLoadCurrentURLInWebView { - (void)loadRequestForCurrentNavigationItem { DCHECK(self.webView && !self.nativeController); + DCHECK([self currentSessionEntry]); + + web::WKBackForwardListItemHolder* holder = + [self currentBackForwardListItemHolder]; + BOOL isFormResubmission = + (holder->navigation_type() == WKNavigationTypeFormResubmitted || + holder->navigation_type() == WKNavigationTypeFormSubmitted); + web::NavigationItemImpl* currentItem = + [self currentSessionEntry].navigationItemImpl; + NSData* POSTData = currentItem->GetPostData(); + NSMutableURLRequest* request = [self requestForCurrentNavigationItem]; + + // If the request has POST data and is not a form resubmission, configure and + // run the POST request. + if (POSTData.length && !isFormResubmission) { + [request setHTTPMethod:@"POST"]; + [request setHTTPBody:POSTData]; + [request setAllHTTPHeaderFields:[self currentHTTPHeaders]]; + [self registerLoadRequest:[self currentNavigationURL] + referrer:[self currentSessionEntryReferrer] + transition:[self currentTransition]]; + [self loadPOSTRequest:request]; + return; + } ProceduralBlock defaultNavigationBlock = ^{ [self registerLoadRequest:[self currentNavigationURL] referrer:[self currentSessionEntryReferrer] transition:[self currentTransition]]; - [self loadRequest:[self requestForCurrentNavigationItem]]; + [self loadRequest:request]; }; // If there is no corresponding WKBackForwardListItem, or the item is not in // the current WKWebView's back-forward list, navigating using WKWebView API // is not possible. In this case, fall back to the default navigation // mechanism. - web::WKBackForwardListItemHolder* holder = - [self currentBackForwardListItemHolder]; if (!holder->back_forward_list_item() || ![self isBackForwardListItemValid:holder->back_forward_list_item()]) { defaultNavigationBlock(); @@ -610,10 +645,8 @@ - (void)loadRequestForCurrentNavigationItem { // If the request is not a form submission or resubmission, or the user // doesn't need to confirm the load, then continue right away. - web::NavigationItemImpl* currentItem = - [self currentSessionEntry].navigationItemImpl; - if ((holder->navigation_type() != WKNavigationTypeFormResubmitted && - holder->navigation_type() != WKNavigationTypeFormSubmitted) || + + if (!isFormResubmission || currentItem->ShouldSkipResubmitDataConfirmation()) { webViewNavigationBlock(); return; @@ -621,6 +654,7 @@ - (void)loadRequestForCurrentNavigationItem { // If the request is form submission or resubmission, then prompt the // user before proceeding. + DCHECK(isFormResubmission); [self.delegate webController:self onFormResubmissionForRequest:nil continueBlock:webViewNavigationBlock @@ -744,6 +778,25 @@ - (int)certGroupID { } #endif +- (void)loadPOSTRequest:(NSMutableURLRequest*)request { + if (!_POSTRequestLoader) { + _POSTRequestLoader.reset([[CRWJSPOSTRequestLoader alloc] init]); + } + + CRWWKScriptMessageRouter* messageRouter = + [self webViewConfigurationProvider].GetScriptMessageRouter(); + + [_POSTRequestLoader loadPOSTRequest:request + inWebView:_wkWebView + messageRouter:messageRouter + completionHandler:^(NSError* loadError) { + if (loadError) + [self handleLoadError:loadError inMainFrame:YES]; + else + self.webStateImpl->SetContentsMimeType("text/html"); + }]; +} + - (web::WKWebViewConfigurationProvider&)webViewConfigurationProvider { DCHECK(self.webStateImpl); web::BrowserState* browserState = self.webStateImpl->GetBrowserState();