| // Copyright 2012 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/ui/crw_web_controller.h" |
| |
| #import <WebKit/WebKit.h> |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/mac/foundation_util.h" |
| #include "base/scoped_observation.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #import "base/test/ios/wait_util.h" |
| #include "base/test/scoped_feature_list.h" |
| #import "ios/testing/ocmock_complex_type_helper.h" |
| #import "ios/web/common/crw_content_view.h" |
| #import "ios/web/common/crw_web_view_content_view.h" |
| #include "ios/web/common/features.h" |
| #import "ios/web/common/uikit_ui_util.h" |
| #import "ios/web/js_messaging/web_view_js_utils.h" |
| #include "ios/web/navigation/block_universal_links_buildflags.h" |
| #import "ios/web/navigation/crw_wk_navigation_states.h" |
| #import "ios/web/navigation/navigation_item_impl.h" |
| #import "ios/web/navigation/navigation_manager_impl.h" |
| #import "ios/web/navigation/wk_navigation_action_policy_util.h" |
| #include "ios/web/public/deprecated/url_verification_constants.h" |
| #import "ios/web/public/download/download_controller.h" |
| #import "ios/web/public/download/download_task.h" |
| #include "ios/web/public/navigation/referrer.h" |
| #import "ios/web/public/session/crw_navigation_item_storage.h" |
| #import "ios/web/public/session/crw_session_storage.h" |
| #import "ios/web/public/test/fakes/crw_fake_web_view_content_view.h" |
| #include "ios/web/public/test/fakes/fake_browser_state.h" |
| #include "ios/web/public/test/fakes/fake_download_controller_delegate.h" |
| #import "ios/web/public/test/fakes/fake_web_client.h" |
| #import "ios/web/public/test/fakes/fake_web_state_delegate.h" |
| #include "ios/web/public/test/fakes/fake_web_state_observer.h" |
| #import "ios/web/public/test/fakes/fake_web_state_policy_decider.h" |
| #import "ios/web/public/test/web_view_content_test_util.h" |
| #include "ios/web/public/web_state_observer.h" |
| #import "ios/web/security/wk_web_view_security_util.h" |
| #import "ios/web/test/fakes/crw_fake_back_forward_list.h" |
| #import "ios/web/test/fakes/crw_fake_wk_frame_info.h" |
| #import "ios/web/test/fakes/crw_fake_wk_navigation_action.h" |
| #include "ios/web/test/test_url_constants.h" |
| #import "ios/web/test/web_test_with_web_controller.h" |
| #import "ios/web/test/wk_web_view_crash_utils.h" |
| #import "ios/web/web_state/ui/crw_web_controller.h" |
| #import "ios/web/web_state/ui/crw_web_controller_container_view.h" |
| #import "ios/web/web_state/web_state_impl.h" |
| #import "net/base/mac/url_conversions.h" |
| #include "net/cert/x509_util_apple.h" |
| #include "net/ssl/ssl_info.h" |
| #include "net/test/cert_test_util.h" |
| #include "net/test/test_data_directory.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #import "testing/gtest_mac.h" |
| #include "third_party/ocmock/OCMock/OCMock.h" |
| #include "third_party/ocmock/gtest_support.h" |
| #include "third_party/ocmock/ocmock_extensions.h" |
| #include "url/scheme_host_port.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| using base::test::ios::WaitUntilConditionOrTimeout; |
| using base::test::ios::kWaitForPageLoadTimeout; |
| using base::test::ios::kWaitForJSCompletionTimeout; |
| |
| // Subclass of WKWebView to check that the observers are removed when the web |
| // state is destroyed. |
| @interface CRWFakeWKWebViewObserverCount : WKWebView |
| |
| // Array storing the different key paths observed. |
| @property(nonatomic, strong) NSMutableArray<NSString*>* keyPaths; |
| // Whether there was observers when the WebView was stopped. |
| @property(nonatomic, assign) BOOL hadObserversWhenStopping; |
| |
| @end |
| |
| @implementation CRWFakeWKWebViewObserverCount |
| |
| - (void)stopLoading { |
| [super stopLoading]; |
| self.hadObserversWhenStopping = |
| self.hadObserversWhenStopping || self.keyPaths.count > 0; |
| } |
| |
| - (void)removeObserver:(NSObject*)observer forKeyPath:(NSString*)keyPath { |
| [super removeObserver:observer forKeyPath:keyPath]; |
| [self.keyPaths removeObject:keyPath]; |
| } |
| |
| - (void)addObserver:(NSObject*)observer |
| forKeyPath:(NSString*)keyPath |
| options:(NSKeyValueObservingOptions)options |
| context:(void*)context { |
| [super addObserver:observer |
| forKeyPath:keyPath |
| options:options |
| context:context]; |
| if (!self.keyPaths) { |
| self.keyPaths = [[NSMutableArray alloc] init]; |
| } |
| [self.keyPaths addObject:keyPath]; |
| } |
| |
| @end |
| |
| namespace web { |
| namespace { |
| |
| // Syntactically invalid URL per rfc3986. |
| const char kInvalidURL[] = "http://%3"; |
| |
| const char kTestDataURL[] = "data:text/html,"; |
| |
| const char kTestURLString[] = "http://www.google.com/"; |
| const char kTestAppSpecificURL[] = "testwebui://test/"; |
| |
| const char kTestMimeType[] = "application/vnd.test"; |
| |
| // Returns HTML for an optionally zoomable test page with |zoom_state|. |
| enum PageScalabilityType { |
| PAGE_SCALABILITY_DISABLED = 0, |
| PAGE_SCALABILITY_ENABLED, |
| }; |
| |
| } // namespace |
| |
| // Test fixture for testing CRWWebController. Stubs out web view. |
| class CRWWebControllerTest : public WebTestWithWebController { |
| protected: |
| CRWWebControllerTest() |
| : WebTestWithWebController(std::make_unique<FakeWebClient>()) {} |
| |
| void SetUp() override { |
| WebTestWithWebController::SetUp(); |
| |
| fake_wk_list_ = [[CRWFakeBackForwardList alloc] init]; |
| mock_web_view_ = CreateMockWebView(fake_wk_list_); |
| scroll_view_ = [[UIScrollView alloc] init]; |
| SetWebViewURL(@(kTestURLString)); |
| [[[mock_web_view_ stub] andReturn:scroll_view_] scrollView]; |
| |
| CRWFakeWebViewContentView* web_view_content_view = |
| [[CRWFakeWebViewContentView alloc] initWithMockWebView:mock_web_view_ |
| scrollView:scroll_view_]; |
| [web_controller() injectWebViewContentView:web_view_content_view]; |
| } |
| |
| void TearDown() override { |
| EXPECT_OCMOCK_VERIFY(mock_web_view_); |
| [web_controller() resetInjectedWebViewContentView]; |
| WebTestWithWebController::TearDown(); |
| } |
| |
| FakeWebClient* GetWebClient() override { |
| return static_cast<FakeWebClient*>( |
| WebTestWithWebController::GetWebClient()); |
| } |
| |
| // The value for web view OCMock objects to expect for |-setFrame:|. |
| CGRect GetExpectedWebViewFrame() const { |
| CGSize container_view_size = GetAnyKeyWindow().bounds.size; |
| |
| container_view_size.height -= CGRectGetHeight( |
| GetAnyKeyWindow().windowScene.statusBarManager.statusBarFrame); |
| return {CGPointZero, container_view_size}; |
| } |
| |
| void SetWebViewURL(NSString* url_string) { |
| test_url_ = [NSURL URLWithString:url_string]; |
| } |
| |
| // Creates WebView mock. |
| UIView* CreateMockWebView(CRWFakeBackForwardList* wk_list) { |
| WKWebView* result = [OCMockObject mockForClass:[WKWebView class]]; |
| |
| OCMStub([result backForwardList]).andReturn(wk_list); |
| // This uses |andDo| rather than |andReturn| since the URL it returns needs |
| // to change when |test_url_| changes. |
| OCMStub([result URL]).andDo(^(NSInvocation* invocation) { |
| [invocation setReturnValue:&test_url_]; |
| }); |
| OCMStub( |
| [result setNavigationDelegate:[OCMArg checkWithBlock:^(id delegate) { |
| navigation_delegate_ = delegate; |
| return YES; |
| }]]); |
| OCMStub([result serverTrust]); |
| OCMStub([result setUIDelegate:OCMOCK_ANY]); |
| OCMStub([result frame]).andReturn(UIScreen.mainScreen.bounds); |
| OCMStub([result setCustomUserAgent:OCMOCK_ANY]); |
| OCMStub([result customUserAgent]); |
| OCMStub([static_cast<WKWebView*>(result) loadRequest:OCMOCK_ANY]); |
| OCMStub([static_cast<WKWebView*>(result) loadFileURL:OCMOCK_ANY |
| allowingReadAccessToURL:OCMOCK_ANY]); |
| OCMStub([result setFrame:GetExpectedWebViewFrame()]); |
| OCMStub([result addObserver:OCMOCK_ANY |
| forKeyPath:OCMOCK_ANY |
| options:0 |
| context:nullptr]); |
| OCMStub([result removeObserver:OCMOCK_ANY forKeyPath:OCMOCK_ANY]); |
| OCMStub([result evaluateJavaScript:OCMOCK_ANY |
| completionHandler:OCMOCK_ANY]); |
| OCMStub([result allowsBackForwardNavigationGestures]); |
| OCMStub([result setAllowsBackForwardNavigationGestures:NO]); |
| OCMStub([result setAllowsBackForwardNavigationGestures:YES]); |
| OCMStub([result isLoading]); |
| OCMStub([result stopLoading]); |
| OCMStub([result removeFromSuperview]); |
| OCMStub([result hasOnlySecureContent]).andReturn(YES); |
| OCMStub([(WKWebView*)result title]).andReturn(@"Title"); |
| |
| return result; |
| } |
| |
| __weak id<WKNavigationDelegate> navigation_delegate_; |
| UIScrollView* scroll_view_; |
| id mock_web_view_; |
| CRWFakeBackForwardList* fake_wk_list_; |
| NSURL* test_url_; |
| }; |
| |
| // Tests that when a committed but not-yet-finished navigation is cancelled, |
| // the navigation item's ErrorRetryStateMachine is updated correctly. |
| TEST_F(CRWWebControllerTest, CancelCommittedNavigation) { |
| [[[mock_web_view_ stub] andReturnBool:NO] hasOnlySecureContent]; |
| [static_cast<WKWebView*>([[mock_web_view_ stub] andReturn:@""]) title]; |
| |
| WKNavigation* navigation = |
| static_cast<WKNavigation*>([[NSObject alloc] init]); |
| SetWebViewURL(@"http://chromium.test"); |
| [navigation_delegate_ webView:mock_web_view_ |
| didStartProvisionalNavigation:navigation]; |
| [fake_wk_list_ setCurrentURL:@"http://chromium.test"]; |
| [navigation_delegate_ webView:mock_web_view_ didCommitNavigation:navigation]; |
| NSError* error = [NSError errorWithDomain:NSURLErrorDomain |
| code:NSURLErrorCancelled |
| userInfo:nil]; |
| [navigation_delegate_ webView:mock_web_view_ |
| didFailNavigation:navigation |
| withError:error]; |
| } |
| |
| // Tests returning pending item stored in navigation context. |
| TEST_F(CRWWebControllerTest, TestPendingItem) { |
| ASSERT_FALSE([web_controller() lastPendingItemForNewNavigation]); |
| |
| // Create pending item by simulating a renderer-initiated navigation. |
| [navigation_delegate_ webView:mock_web_view_ |
| didStartProvisionalNavigation:nil]; |
| |
| NavigationItemImpl* item = [web_controller() lastPendingItemForNewNavigation]; |
| |
| ASSERT_TRUE(item); |
| EXPECT_EQ(kTestURLString, item->GetURL()); |
| } |
| |
| // Tests allowsBackForwardNavigationGestures default value and negating this |
| // property. |
| TEST_F(CRWWebControllerTest, SetAllowsBackForwardNavigationGestures) { |
| EXPECT_TRUE(web_controller().allowsBackForwardNavigationGestures); |
| web_controller().allowsBackForwardNavigationGestures = NO; |
| EXPECT_FALSE(web_controller().allowsBackForwardNavigationGestures); |
| } |
| |
| // Tests that a web view is created after calling -[ensureWebViewCreated] and |
| // check its user agent. |
| TEST_F(CRWWebControllerTest, WebViewCreatedAfterEnsureWebViewCreated) { |
| FakeWebClient* web_client = static_cast<FakeWebClient*>(GetWebClient()); |
| |
| [web_controller() removeWebView]; |
| WKWebView* web_view = [web_controller() ensureWebViewCreated]; |
| EXPECT_TRUE(web_view); |
| EXPECT_NSEQ( |
| base::SysUTF8ToNSString(web_client->GetUserAgent(UserAgentType::MOBILE)), |
| web_view.customUserAgent); |
| |
| web_client->SetDefaultUserAgent(UserAgentType::DESKTOP); |
| [web_controller() removeWebView]; |
| web_view = [web_controller() ensureWebViewCreated]; |
| EXPECT_NSEQ( |
| base::SysUTF8ToNSString(web_client->GetUserAgent(UserAgentType::DESKTOP)), |
| web_view.customUserAgent); |
| } |
| |
| // Tests that the WebView is correctly removed/added from the view hierarchy. |
| TEST_F(CRWWebControllerTest, RemoveWebViewFromViewHierarchy) { |
| // Make sure that the WebController view has a window to avoid stashing the |
| // WebView once created. |
| [GetAnyKeyWindow() addSubview:web_controller().view]; |
| |
| // Get the web view. |
| [web_controller() removeWebView]; |
| WKWebView* web_view = [web_controller() ensureWebViewCreated]; |
| |
| ASSERT_EQ(web_controller().view, web_view.superview.superview); |
| |
| [web_controller() removeWebViewFromViewHierarchy]; |
| EXPECT_EQ(nil, web_view.superview.superview); |
| |
| [web_controller() addWebViewToViewHierarchy]; |
| EXPECT_EQ(web_controller().view, web_view.superview.superview); |
| } |
| |
| // Test fixture to test JavaScriptDialogPresenter. |
| class JavaScriptDialogPresenterTest : public WebTestWithWebController { |
| protected: |
| JavaScriptDialogPresenterTest() : page_url_("https://chromium.test/") {} |
| void SetUp() override { |
| WebTestWithWebState::SetUp(); |
| LoadHtml(@"<html><body></body></html>", page_url_); |
| web_state()->SetDelegate(&web_state_delegate_); |
| } |
| void TearDown() override { |
| web_state()->SetDelegate(nullptr); |
| WebTestWithWebState::TearDown(); |
| } |
| FakeJavaScriptDialogPresenter* js_dialog_presenter() { |
| return web_state_delegate_.GetFakeJavaScriptDialogPresenter(); |
| } |
| const std::vector<std::unique_ptr<FakeJavaScriptDialog>>& |
| requested_dialogs() { |
| return js_dialog_presenter()->requested_dialogs(); |
| } |
| const GURL& page_url() { return page_url_; } |
| |
| private: |
| FakeWebStateDelegate web_state_delegate_; |
| GURL page_url_; |
| }; |
| |
| // Tests that window.alert dialog is shown. |
| TEST_F(JavaScriptDialogPresenterTest, Alert) { |
| ASSERT_TRUE(requested_dialogs().empty()); |
| |
| ExecuteJavaScript(@"alert('test')"); |
| |
| ASSERT_EQ(1U, requested_dialogs().size()); |
| auto& dialog = requested_dialogs().front(); |
| EXPECT_EQ(web_state(), dialog->web_state); |
| EXPECT_EQ(page_url(), dialog->origin_url); |
| EXPECT_EQ(JAVASCRIPT_DIALOG_TYPE_ALERT, dialog->java_script_dialog_type); |
| EXPECT_NSEQ(@"test", dialog->message_text); |
| EXPECT_FALSE(dialog->default_prompt_text); |
| } |
| |
| // Tests that window.confirm dialog is shown and its result is true. |
| TEST_F(JavaScriptDialogPresenterTest, ConfirmWithTrue) { |
| ASSERT_TRUE(requested_dialogs().empty()); |
| |
| js_dialog_presenter()->set_callback_success_argument(true); |
| |
| EXPECT_NSEQ(@YES, ExecuteJavaScript(@"confirm('test')")); |
| |
| ASSERT_EQ(1U, requested_dialogs().size()); |
| auto& dialog = requested_dialogs().front(); |
| EXPECT_EQ(web_state(), dialog->web_state); |
| EXPECT_EQ(page_url(), dialog->origin_url); |
| EXPECT_EQ(JAVASCRIPT_DIALOG_TYPE_CONFIRM, dialog->java_script_dialog_type); |
| EXPECT_NSEQ(@"test", dialog->message_text); |
| EXPECT_FALSE(dialog->default_prompt_text); |
| } |
| |
| // Tests that window.confirm dialog is shown and its result is false. |
| TEST_F(JavaScriptDialogPresenterTest, ConfirmWithFalse) { |
| ASSERT_TRUE(requested_dialogs().empty()); |
| |
| EXPECT_NSEQ(@NO, ExecuteJavaScript(@"confirm('test')")); |
| |
| ASSERT_EQ(1U, requested_dialogs().size()); |
| auto& dialog = requested_dialogs().front(); |
| EXPECT_EQ(web_state(), dialog->web_state); |
| EXPECT_EQ(page_url(), dialog->origin_url); |
| EXPECT_EQ(JAVASCRIPT_DIALOG_TYPE_CONFIRM, dialog->java_script_dialog_type); |
| EXPECT_NSEQ(@"test", dialog->message_text); |
| EXPECT_FALSE(dialog->default_prompt_text); |
| } |
| |
| // Tests that window.prompt dialog is shown. |
| TEST_F(JavaScriptDialogPresenterTest, Prompt) { |
| ASSERT_TRUE(requested_dialogs().empty()); |
| |
| js_dialog_presenter()->set_callback_user_input_argument(@"Maybe"); |
| |
| EXPECT_NSEQ(@"Maybe", ExecuteJavaScript(@"prompt('Yes?', 'No')")); |
| |
| ASSERT_EQ(1U, requested_dialogs().size()); |
| auto& dialog = requested_dialogs().front(); |
| EXPECT_EQ(web_state(), dialog->web_state); |
| EXPECT_EQ(page_url(), dialog->origin_url); |
| EXPECT_EQ(JAVASCRIPT_DIALOG_TYPE_PROMPT, dialog->java_script_dialog_type); |
| EXPECT_NSEQ(@"Yes?", dialog->message_text); |
| EXPECT_NSEQ(@"No", dialog->default_prompt_text); |
| } |
| |
| // Tests that window.alert, window.confirm and window.prompt dialogs are not |
| // shown if URL of presenting main frame is different from visible URL. |
| TEST_F(JavaScriptDialogPresenterTest, DifferentVisibleUrl) { |
| ASSERT_TRUE(requested_dialogs().empty()); |
| |
| // Change visible URL. |
| AddPendingItem(GURL("https://pending.test/"), ui::PAGE_TRANSITION_TYPED); |
| web_controller().webStateImpl->SetIsLoading(true); |
| ASSERT_NE(page_url().DeprecatedGetOriginAsURL(), |
| web_state()->GetVisibleURL().DeprecatedGetOriginAsURL()); |
| |
| ExecuteJavaScript(@"alert('test')"); |
| ASSERT_TRUE(requested_dialogs().empty()); |
| |
| EXPECT_NSEQ(@NO, ExecuteJavaScript(@"confirm('test')")); |
| ASSERT_TRUE(requested_dialogs().empty()); |
| |
| EXPECT_NSEQ([NSNull null], ExecuteJavaScript(@"prompt('Yes?', 'No')")); |
| ASSERT_TRUE(requested_dialogs().empty()); |
| } |
| |
| // Test fixture for testing visible security state. |
| typedef WebTestWithWebState CRWWebStateSecurityStateTest; |
| |
| // Tests that loading HTTP page updates the SSLStatus. |
| TEST_F(CRWWebStateSecurityStateTest, LoadHttpPage) { |
| FakeWebStateObserver observer(web_state()); |
| ASSERT_FALSE(observer.did_change_visible_security_state_info()); |
| LoadHtml(@"<html><body></body></html>", GURL("http://chromium.test")); |
| NavigationManager* nav_manager = web_state()->GetNavigationManager(); |
| NavigationItem* item = nav_manager->GetLastCommittedItem(); |
| EXPECT_EQ(SECURITY_STYLE_UNAUTHENTICATED, item->GetSSL().security_style); |
| ASSERT_TRUE(observer.did_change_visible_security_state_info()); |
| EXPECT_EQ(web_state(), |
| observer.did_change_visible_security_state_info()->web_state); |
| } |
| |
| // Real WKWebView is required for CRWWebControllerInvalidUrlTest. |
| typedef WebTestWithWebState CRWWebControllerInvalidUrlTest; |
| |
| // Tests that web controller does not navigate to about:blank if iframe src |
| // has invalid url. Web controller loads about:blank if page navigates to |
| // invalid url, but should do nothing if navigation is performed in iframe. This |
| // test prevents crbug.com/694865 regression. |
| TEST_F(CRWWebControllerInvalidUrlTest, IFrameWithInvalidURL) { |
| GURL url("http://chromium.test"); |
| ASSERT_FALSE(GURL(kInvalidURL).is_valid()); |
| LoadHtml([NSString stringWithFormat:@"<iframe src='%s'/>", kInvalidURL], url); |
| EXPECT_EQ(url, web_state()->GetLastCommittedURL()); |
| } |
| |
| // Real WKWebView is required for CRWWebControllerMessageFromIFrame. |
| typedef WebTestWithWebState CRWWebControllerMessageFromIFrame; |
| |
| // Tests that invalid message from iframe does not cause a crash. |
| TEST_F(CRWWebControllerMessageFromIFrame, InvalidMessage) { |
| static NSString* const kHTMLIFrameSendsInvalidMessage = |
| @"<body><iframe name='f'></iframe></body>"; |
| |
| LoadHtml(kHTMLIFrameSendsInvalidMessage); |
| |
| // Sending unknown command from iframe should not cause a crash. |
| ExecuteJavaScript( |
| @"var bad_message = {'command' : 'unknown.command'};" |
| "frames['f'].__gCrWeb.message.invokeOnHost(bad_message);"); |
| } |
| |
| // Real WKWebView is required for CRWWebControllerJSExecutionTest. |
| typedef WebTestWithWebController CRWWebControllerJSExecutionTest; |
| |
| // Tests that a script correctly evaluates to boolean. |
| TEST_F(CRWWebControllerJSExecutionTest, Execution) { |
| LoadHtml(@"<p></p>"); |
| EXPECT_NSEQ(@YES, ExecuteJavaScript(@"true")); |
| EXPECT_NSEQ(@NO, ExecuteJavaScript(@"false")); |
| } |
| |
| // Tests that a script is not executed on windowID mismatch. |
| TEST_F(CRWWebControllerJSExecutionTest, WindowIdMissmatch) { |
| LoadHtml(@"<p></p>"); |
| // Script is evaluated since windowID is matched. |
| ExecuteJavaScript(@"window.test1 = '1';"); |
| EXPECT_NSEQ(@"1", ExecuteJavaScript(@"window.test1")); |
| |
| // Change windowID. |
| ExecuteJavaScript(@"__gCrWeb['windowId'] = '';"); |
| |
| // Script is not evaluated because of windowID mismatch. |
| ExecuteJavaScript(@"window.test2 = '2';"); |
| EXPECT_FALSE(ExecuteJavaScript(@"window.test2")); |
| } |
| |
| // Test fixture to test decidePolicyForNavigationResponse:decisionHandler: |
| // delegate method. |
| class CRWWebControllerResponseTest : public CRWWebControllerTest { |
| protected: |
| CRWWebControllerResponseTest() {} |
| |
| void SetUp() override { |
| CRWWebControllerTest::SetUp(); |
| download_delegate_ = |
| std::make_unique<FakeDownloadControllerDelegate>(download_controller()); |
| } |
| |
| // Calls webView:decidePolicyForNavigationResponse:decisionHandler: callback |
| // and waits for decision handler call. Returns false if decision handler call |
| // times out. |
| [[nodiscard]] bool CallDecidePolicyForNavigationResponseWithResponse( |
| NSURLResponse* response, |
| BOOL for_main_frame, |
| BOOL can_show_mime_type, |
| WKNavigationResponsePolicy* out_policy) { |
| id navigation_response = |
| [OCMockObject mockForClass:[WKNavigationResponse class]]; |
| OCMStub([navigation_response response]).andReturn(response); |
| OCMStub([navigation_response isForMainFrame]).andReturn(for_main_frame); |
| OCMStub([navigation_response canShowMIMEType]) |
| .andReturn(can_show_mime_type); |
| |
| // Check whether the NavigationController has been configured to simulate |
| // a POST request (which is required to recreate a correct NSURLRequest |
| // object when mocking the new download API on iOS 15+). |
| NavigationItemImpl* pending_item = |
| web_controller() |
| .webStateImpl->GetNavigationManagerImpl() |
| .GetPendingItemInCurrentOrRestoredSession(); |
| const bool has_post_data = |
| pending_item && pending_item->GetPostData() != nil; |
| |
| // Call decidePolicyForNavigationResponse and wait for decisionHandler's |
| // callback. |
| __block bool callback_called = false; |
| if (for_main_frame) { |
| // webView:didStartProvisionalNavigation: is not called for iframes. |
| [navigation_delegate_ webView:mock_web_view_ |
| didStartProvisionalNavigation:nil]; |
| } |
| [navigation_delegate_ webView:mock_web_view_ |
| decidePolicyForNavigationResponse:navigation_response |
| decisionHandler:^(WKNavigationResponsePolicy policy) { |
| *out_policy = policy; |
| callback_called = true; |
| }]; |
| if (!WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{ |
| return callback_called; |
| })) { |
| return false; |
| } |
| |
| // When the new download API is enabled (which can only happen on iOS 15), |
| // the interaction is a bit more complex as WebKit will call additional |
| // methods on the WKNavigationDelegate before the DownloadTask is created. |
| // Mock those necessary interactions. |
| if (@available(iOS 15, *)) { |
| if (*out_policy == WKNavigationResponsePolicyDownload) { |
| DCHECK(web::features::IsNewDownloadAPIEnabled()); |
| id mock_download = [OCMockObject mockForClass:[WKDownload class]]; |
| |
| __block bool delegate_set = false; |
| __block id download_delegate = nil; |
| OCMStub([mock_download setDelegate:[OCMArg any]]) |
| .andDo(^(NSInvocation* invocation) { |
| // Using __unsafe_unretained is required to extract the parameter |
| // from the NSInvocation otherwise ARC will over-release. |
| __unsafe_unretained id argument = nil; |
| [invocation getArgument:&argument atIndex:2]; |
| download_delegate = argument; |
| delegate_set = true; |
| }); |
| |
| [navigation_delegate_ webView:mock_web_view_ |
| navigationResponse:navigation_response |
| didBecomeDownload:mock_download]; |
| |
| if (!WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{ |
| return delegate_set; |
| })) { |
| return false; |
| } |
| |
| NSMutableURLRequest* request = |
| [[NSURLRequest requestWithURL:response.URL] mutableCopy]; |
| if (has_post_data) { |
| request.HTTPMethod = @"POST"; |
| } |
| OCMStub([mock_download originalRequest]).andReturn(request); |
| OCMStub([mock_download cancel:[OCMArg any]]) |
| .andDo(^(NSInvocation* invocation) { |
| // Using __unsafe_unretained is required to extract the parameter |
| // from the NSInvocation otherwise ARC will over-release. |
| __unsafe_unretained void (^block)(NSData* data); |
| [invocation getArgument:&block atIndex:2]; |
| block(nil); |
| }); |
| |
| [download_delegate download:mock_download |
| decideDestinationUsingResponse:response |
| suggestedFilename:@"filename.txt" |
| completionHandler:^(NSURL* destination){ |
| }]; |
| } |
| } |
| |
| return true; |
| } |
| |
| // The expectation varies depending on whether the new download API is used |
| // or not (as the new download API requires CRWWKNavigationHandler to return |
| // a different policy). This method returns the expected policy for the test. |
| [[nodiscard]] static WKNavigationResponsePolicy ExpectedPolicyForDownload() { |
| if (@available(iOS 15, *)) { |
| if (web::features::IsNewDownloadAPIEnabled()) { |
| return WKNavigationResponsePolicyDownload; |
| } |
| } |
| return WKNavigationResponsePolicyCancel; |
| } |
| |
| DownloadController* download_controller() { |
| return DownloadController::FromBrowserState(GetBrowserState()); |
| } |
| |
| std::unique_ptr<FakeDownloadControllerDelegate> download_delegate_; |
| }; |
| |
| // Tests that webView:decidePolicyForNavigationResponse:decisionHandler: allows |
| // renderer-initiated navigations in main frame for http: URLs. |
| TEST_F(CRWWebControllerResponseTest, AllowRendererInitiatedResponse) { |
| // Simulate regular navigation response with text/html MIME type. |
| NSURLResponse* response = [[NSHTTPURLResponse alloc] |
| initWithURL:[NSURL URLWithString:@(kTestURLString)] |
| statusCode:200 |
| HTTPVersion:nil |
| headerFields:nil]; |
| WKNavigationResponsePolicy policy = WKNavigationResponsePolicyCancel; |
| ASSERT_TRUE(CallDecidePolicyForNavigationResponseWithResponse( |
| response, /*for_main_frame=*/YES, /*can_show_mime_type=*/YES, &policy)); |
| EXPECT_EQ(WKNavigationResponsePolicyAllow, policy); |
| |
| // Verify that download task was not created for html response. |
| ASSERT_TRUE(download_delegate_->alive_download_tasks().empty()); |
| } |
| |
| // Tests that webView:decidePolicyForNavigationResponse:decisionHandler: allows |
| // renderer-initiated navigations in iframe for data: URLs. |
| TEST_F(CRWWebControllerResponseTest, |
| AllowRendererInitiatedDataUrlResponseInIFrame) { |
| // Simulate data:// url response with text/html MIME type. |
| SetWebViewURL(@(kTestDataURL)); |
| NSURLResponse* response = [[NSHTTPURLResponse alloc] |
| initWithURL:[NSURL URLWithString:@(kTestDataURL)] |
| statusCode:200 |
| HTTPVersion:nil |
| headerFields:nil]; |
| WKNavigationResponsePolicy policy = WKNavigationResponsePolicyCancel; |
| ASSERT_TRUE(CallDecidePolicyForNavigationResponseWithResponse( |
| response, /*for_main_frame=*/NO, /*can_show_mime_type=*/YES, &policy)); |
| EXPECT_EQ(WKNavigationResponsePolicyAllow, policy); |
| |
| // Verify that download task was not created for html response. |
| ASSERT_TRUE(download_delegate_->alive_download_tasks().empty()); |
| } |
| |
| // Tests that webView:decidePolicyForNavigationResponse:decisionHandler: blocks |
| // rendering data URLs for renderer-initiated navigations in main frame to |
| // prevent abusive behavior (crbug.com/890558) and presents the download option. |
| TEST_F(CRWWebControllerResponseTest, |
| DownloadRendererInitiatedDataUrlResponseInMainFrame) { |
| // Simulate data:// url response with text/html MIME type. |
| SetWebViewURL(@(kTestDataURL)); |
| NSURLResponse* response = [[NSHTTPURLResponse alloc] |
| initWithURL:[NSURL URLWithString:@(kTestDataURL)] |
| statusCode:200 |
| HTTPVersion:nil |
| headerFields:nil]; |
| WKNavigationResponsePolicy policy = WKNavigationResponsePolicyAllow; |
| ASSERT_TRUE(CallDecidePolicyForNavigationResponseWithResponse( |
| response, /*for_main_frame=*/YES, /*can_show_mime_type=*/YES, &policy)); |
| EXPECT_EQ(ExpectedPolicyForDownload(), policy); |
| |
| // Verify that download task was created (see crbug.com/949114). |
| ASSERT_EQ(1U, download_delegate_->alive_download_tasks().size()); |
| DownloadTask* task = |
| download_delegate_->alive_download_tasks()[0].second.get(); |
| ASSERT_TRUE(task); |
| EXPECT_TRUE(task->GetIdentifier()); |
| EXPECT_EQ(kTestDataURL, task->GetOriginalUrl()); |
| EXPECT_EQ(-1, task->GetTotalBytes()); |
| EXPECT_TRUE(task->GetContentDisposition().empty()); |
| EXPECT_TRUE(task->GetMimeType().empty()); |
| EXPECT_NSEQ(@"GET", task->GetHttpMethod()); |
| } |
| |
| // Tests that webView:decidePolicyForNavigationResponse:decisionHandler: allows |
| // rendering data URLs for browser-initiated navigations in main frame. |
| TEST_F(CRWWebControllerResponseTest, |
| AllowBrowserInitiatedDataUrlResponseInMainFrame) { |
| // Simulate data:// url response with text/html MIME type. |
| GURL url(kTestDataURL); |
| AddPendingItem(url, ui::PAGE_TRANSITION_TYPED); |
| [web_controller() loadCurrentURLWithRendererInitiatedNavigation:NO]; |
| SetWebViewURL(@(kTestDataURL)); |
| NSURLResponse* response = [[NSHTTPURLResponse alloc] |
| initWithURL:[NSURL URLWithString:@(kTestDataURL)] |
| statusCode:200 |
| HTTPVersion:nil |
| headerFields:nil]; |
| WKNavigationResponsePolicy policy = WKNavigationResponsePolicyCancel; |
| ASSERT_TRUE(CallDecidePolicyForNavigationResponseWithResponse( |
| response, /*for_main_frame=*/YES, /*can_show_mime_type=*/YES, &policy)); |
| EXPECT_EQ(WKNavigationResponsePolicyAllow, policy); |
| |
| // Verify that download task was not created for html response. |
| ASSERT_TRUE(download_delegate_->alive_download_tasks().empty()); |
| } |
| |
| // Tests that webView:decidePolicyForNavigationResponse:decisionHandler: |
| // correctly uses POST HTTP method for post requests. |
| TEST_F(CRWWebControllerResponseTest, DownloadForPostRequest) { |
| // Simulate regular navigation response for post request with text/html MIME |
| // type. |
| GURL url(kTestURLString); |
| AddPendingItem(url, ui::PAGE_TRANSITION_TYPED); |
| web_controller() |
| .webStateImpl->GetNavigationManagerImpl() |
| .GetPendingItemInCurrentOrRestoredSession() |
| ->SetPostData([NSData data]); |
| [web_controller() loadCurrentURLWithRendererInitiatedNavigation:NO]; |
| NSURLResponse* response = [[NSHTTPURLResponse alloc] |
| initWithURL:[NSURL URLWithString:@(kTestURLString)] |
| statusCode:200 |
| HTTPVersion:nil |
| headerFields:nil]; |
| WKNavigationResponsePolicy policy = WKNavigationResponsePolicyAllow; |
| ASSERT_TRUE(CallDecidePolicyForNavigationResponseWithResponse( |
| response, /*for_main_frame=*/YES, /*can_show_mime_type=*/NO, &policy)); |
| EXPECT_EQ(ExpectedPolicyForDownload(), policy); |
| |
| // Verify that download task was created with POST method (crbug.com/. |
| ASSERT_EQ(1U, download_delegate_->alive_download_tasks().size()); |
| DownloadTask* task = |
| download_delegate_->alive_download_tasks()[0].second.get(); |
| ASSERT_TRUE(task); |
| EXPECT_TRUE(task->GetIdentifier()); |
| EXPECT_NSEQ(@"POST", task->GetHttpMethod()); |
| } |
| |
| // Tests that webView:decidePolicyForNavigationResponse:decisionHandler: creates |
| // the DownloadTask for NSURLResponse. |
| TEST_F(CRWWebControllerResponseTest, DownloadWithNSURLResponse) { |
| // Simulate download response. |
| int64_t content_length = 10; |
| NSURLResponse* response = |
| [[NSURLResponse alloc] initWithURL:[NSURL URLWithString:@(kTestURLString)] |
| MIMEType:@(kTestMimeType) |
| expectedContentLength:content_length |
| textEncodingName:nil]; |
| WKNavigationResponsePolicy policy = WKNavigationResponsePolicyAllow; |
| ASSERT_TRUE(CallDecidePolicyForNavigationResponseWithResponse( |
| response, /*for_main_frame=*/YES, /*can_show_mime_type=*/NO, &policy)); |
| EXPECT_EQ(ExpectedPolicyForDownload(), policy); |
| |
| // Verify that download task was created. |
| ASSERT_EQ(1U, download_delegate_->alive_download_tasks().size()); |
| DownloadTask* task = |
| download_delegate_->alive_download_tasks()[0].second.get(); |
| ASSERT_TRUE(task); |
| EXPECT_TRUE(task->GetIdentifier()); |
| EXPECT_EQ(kTestURLString, task->GetOriginalUrl()); |
| EXPECT_EQ(content_length, task->GetTotalBytes()); |
| EXPECT_EQ("", task->GetContentDisposition()); |
| EXPECT_EQ(kTestMimeType, task->GetMimeType()); |
| } |
| |
| // Tests that webView:decidePolicyForNavigationResponse:decisionHandler: creates |
| // the DownloadTask for NSHTTPURLResponse. |
| TEST_F(CRWWebControllerResponseTest, DownloadWithNSHTTPURLResponse) { |
| // Simulate download response. |
| const char kContentDisposition[] = "attachment; filename=download.test"; |
| NSURLResponse* response = [[NSHTTPURLResponse alloc] |
| initWithURL:[NSURL URLWithString:@(kTestURLString)] |
| statusCode:200 |
| HTTPVersion:nil |
| headerFields:@{ |
| @"content-disposition" : @(kContentDisposition), |
| }]; |
| WKNavigationResponsePolicy policy = WKNavigationResponsePolicyAllow; |
| ASSERT_TRUE(CallDecidePolicyForNavigationResponseWithResponse( |
| response, /*for_main_frame=*/YES, /*can_show_mime_type=*/NO, &policy)); |
| EXPECT_EQ(ExpectedPolicyForDownload(), policy); |
| |
| // Verify that download task was created. |
| ASSERT_EQ(1U, download_delegate_->alive_download_tasks().size()); |
| DownloadTask* task = |
| download_delegate_->alive_download_tasks()[0].second.get(); |
| ASSERT_TRUE(task); |
| EXPECT_TRUE(task->GetIdentifier()); |
| EXPECT_EQ(kTestURLString, task->GetOriginalUrl()); |
| EXPECT_EQ(-1, task->GetTotalBytes()); |
| EXPECT_EQ(kContentDisposition, task->GetContentDisposition()); |
| EXPECT_EQ("", task->GetMimeType()); |
| } |
| |
| // Tests that webView:decidePolicyForNavigationResponse:decisionHandler: |
| // discards pending URL. |
| TEST_F(CRWWebControllerResponseTest, DownloadDiscardsPendingUrl) { |
| GURL url(kTestURLString); |
| AddPendingItem(url, ui::PAGE_TRANSITION_TYPED); |
| |
| // Simulate download response. |
| NSURLResponse* response = |
| [[NSURLResponse alloc] initWithURL:[NSURL URLWithString:@(kTestURLString)] |
| MIMEType:@(kTestMimeType) |
| expectedContentLength:10 |
| textEncodingName:nil]; |
| WKNavigationResponsePolicy policy = WKNavigationResponsePolicyAllow; |
| ASSERT_TRUE(CallDecidePolicyForNavigationResponseWithResponse( |
| response, /*for_main_frame=*/YES, /*can_show_mime_type=*/NO, &policy)); |
| EXPECT_EQ(ExpectedPolicyForDownload(), policy); |
| |
| // Verify that download task was created and pending URL discarded. |
| ASSERT_EQ(1U, download_delegate_->alive_download_tasks().size()); |
| EXPECT_EQ("", web_state()->GetVisibleURL()); |
| } |
| |
| // Tests that webView:decidePolicyForNavigationResponse:decisionHandler: creates |
| // the DownloadTask for NSHTTPURLResponse and iframes. |
| TEST_F(CRWWebControllerResponseTest, IFrameDownloadWithNSHTTPURLResponse) { |
| // Simulate download response. |
| const char kContentDisposition[] = "attachment; filename=download.test"; |
| NSURLResponse* response = [[NSHTTPURLResponse alloc] |
| initWithURL:[NSURL URLWithString:@(kTestURLString)] |
| statusCode:200 |
| HTTPVersion:nil |
| headerFields:@{ |
| @"content-disposition" : @(kContentDisposition), |
| }]; |
| WKNavigationResponsePolicy policy = WKNavigationResponsePolicyAllow; |
| ASSERT_TRUE(CallDecidePolicyForNavigationResponseWithResponse( |
| response, /*for_main_frame=*/NO, /*can_show_mime_type=*/NO, &policy)); |
| EXPECT_EQ(ExpectedPolicyForDownload(), policy); |
| |
| // Verify that download task was created. |
| ASSERT_EQ(1U, download_delegate_->alive_download_tasks().size()); |
| DownloadTask* task = |
| download_delegate_->alive_download_tasks()[0].second.get(); |
| ASSERT_TRUE(task); |
| EXPECT_TRUE(task->GetIdentifier()); |
| EXPECT_EQ(kTestURLString, task->GetOriginalUrl()); |
| EXPECT_EQ(-1, task->GetTotalBytes()); |
| EXPECT_EQ(kContentDisposition, task->GetContentDisposition()); |
| EXPECT_EQ("", task->GetMimeType()); |
| } |
| |
| // Tests |currentURLWithTrustLevel:| method. |
| TEST_F(CRWWebControllerTest, CurrentUrlWithTrustLevel) { |
| GURL url("http://chromium.test"); |
| AddPendingItem(url, ui::PAGE_TRANSITION_TYPED); |
| |
| [[[mock_web_view_ stub] andReturnBool:NO] hasOnlySecureContent]; |
| [static_cast<WKWebView*>([[mock_web_view_ stub] andReturn:@""]) title]; |
| SetWebViewURL(@"http://chromium.test"); |
| |
| // Stub out the injection process. |
| [[mock_web_view_ stub] evaluateJavaScript:OCMOCK_ANY |
| completionHandler:OCMOCK_ANY]; |
| |
| // Simulate a page load to trigger a URL update. |
| [navigation_delegate_ webView:mock_web_view_ |
| didStartProvisionalNavigation:nil]; |
| [fake_wk_list_ setCurrentURL:@"http://chromium.test"]; |
| [navigation_delegate_ webView:mock_web_view_ didCommitNavigation:nil]; |
| |
| URLVerificationTrustLevel trust_level = kNone; |
| EXPECT_EQ(url, [web_controller() currentURLWithTrustLevel:&trust_level]); |
| EXPECT_EQ(kAbsolute, trust_level); |
| } |
| |
| // Test fixture to test decidePolicyForNavigationAction:decisionHandler: |
| // decisionHandler's callback result. |
| class CRWWebControllerPolicyDeciderTest : public CRWWebControllerTest { |
| protected: |
| void SetUp() override { |
| CRWWebControllerTest::SetUp(); |
| } |
| // Calls webView:decidePolicyForNavigationAction:preferences:decisionHandler: |
| // callback and waits for decision handler call. Returns false if decision |
| // handler policy parameter didn't match |expected_policy| or if the call |
| // timed out. |
| [[nodiscard]] bool VerifyDecidePolicyForNavigationAction( |
| NSURLRequest* request, |
| WKNavigationActionPolicy expected_policy) { |
| CRWFakeWKNavigationAction* navigation_action = |
| [[CRWFakeWKNavigationAction alloc] init]; |
| navigation_action.request = request; |
| |
| CRWFakeWKFrameInfo* frame_info = [[CRWFakeWKFrameInfo alloc] init]; |
| frame_info.mainFrame = YES; |
| navigation_action.targetFrame = frame_info; |
| |
| WKWebpagePreferences* preferences = [[WKWebpagePreferences alloc] init]; |
| |
| // Call webView:decidePolicyForNavigationAction:preferences:decisionHandler: |
| // and wait for decisionHandler's callback. |
| __block bool policy_match = false; |
| __block bool callback_called = false; |
| [navigation_delegate_ webView:mock_web_view_ |
| decidePolicyForNavigationAction:navigation_action |
| preferences:preferences |
| decisionHandler:^(WKNavigationActionPolicy policy, |
| WKWebpagePreferences* preferences) { |
| policy_match = expected_policy == policy; |
| callback_called = true; |
| }]; |
| callback_called = WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{ |
| return callback_called; |
| }); |
| |
| return policy_match; |
| } |
| |
| std::unique_ptr<BrowserState> CreateBrowserState() override { |
| return std::make_unique<FakeBrowserState>(); |
| } |
| |
| FakeBrowserState* GetFakeBrowserState() { |
| return static_cast<FakeBrowserState*>(GetBrowserState()); |
| } |
| }; |
| |
| // Tests that App specific URLs in iframes are allowed if the main frame is App |
| // specific URL. App specific pages have elevated privileges and WKWebView uses |
| // the same renderer process for all page frames. With that running App specific |
| // pages are not allowed in the same process as a web site from the internet. |
| TEST_F(CRWWebControllerPolicyDeciderTest, |
| AllowAppSpecificIFrameFromAppSpecificPage) { |
| NSURL* app_url = [NSURL URLWithString:@(kTestAppSpecificURL)]; |
| NSMutableURLRequest* app_url_request = |
| [NSMutableURLRequest requestWithURL:app_url]; |
| app_url_request.mainDocumentURL = app_url; |
| EXPECT_TRUE(VerifyDecidePolicyForNavigationAction( |
| app_url_request, WKNavigationActionPolicyAllow)); |
| } |
| |
| // Tests that URL is allowed in OffTheRecord mode when the |
| // |kBlockUniversalLinksInOffTheRecordMode| feature is disabled. |
| TEST_F(CRWWebControllerPolicyDeciderTest, AllowOffTheRecordNavigation) { |
| GetFakeBrowserState()->SetOffTheRecord(true); |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndDisableFeature( |
| web::features::kBlockUniversalLinksInOffTheRecordMode); |
| |
| NSURL* url = [NSURL URLWithString:@(kTestURLString)]; |
| NSMutableURLRequest* url_request = [NSMutableURLRequest requestWithURL:url]; |
| url_request.mainDocumentURL = url; |
| EXPECT_TRUE(VerifyDecidePolicyForNavigationAction( |
| url_request, WKNavigationActionPolicyAllow)); |
| } |
| |
| // Tests that URL is allowed in OffTheRecord mode and that universal links are |
| // blocked when the |kBlockUniversalLinksInOffTheRecordMode| feature is enabled |
| // and the BLOCK_UNIVERSAL_LINKS_IN_OFF_THE_RECORD_MODE buildflag is set. |
| TEST_F(CRWWebControllerPolicyDeciderTest, |
| AllowOffTheRecordNavigationBlockUniversalLinks) { |
| GetFakeBrowserState()->SetOffTheRecord(true); |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| web::features::kBlockUniversalLinksInOffTheRecordMode); |
| |
| NSURL* url = [NSURL URLWithString:@(kTestURLString)]; |
| NSMutableURLRequest* url_request = [NSMutableURLRequest requestWithURL:url]; |
| url_request.mainDocumentURL = url; |
| |
| WKNavigationActionPolicy expected_policy = WKNavigationActionPolicyAllow; |
| #if BUILDFLAG(BLOCK_UNIVERSAL_LINKS_IN_OFF_THE_RECORD_MODE) |
| expected_policy = kNavigationActionPolicyAllowAndBlockUniversalLinks; |
| #endif // BUILDFLAG(BLOCK_UNIVERSAL_LINKS_IN_OFF_THE_RECORD_MODE) |
| |
| EXPECT_TRUE( |
| VerifyDecidePolicyForNavigationAction(url_request, expected_policy)); |
| } |
| |
| // Tests that App specific URLs in iframes are not allowed if the main frame is |
| // not App specific URL. |
| TEST_F(CRWWebControllerPolicyDeciderTest, |
| DisallowAppSpecificIFrameFromRegularPage) { |
| NSURL* app_url = [NSURL URLWithString:@(kTestAppSpecificURL)]; |
| NSMutableURLRequest* app_url_request = |
| [NSMutableURLRequest requestWithURL:app_url]; |
| app_url_request.mainDocumentURL = [NSURL URLWithString:@(kTestURLString)]; |
| EXPECT_TRUE(VerifyDecidePolicyForNavigationAction( |
| app_url_request, WKNavigationActionPolicyCancel)); |
| } |
| |
| // Tests that blob URL navigation is allowed. |
| TEST_F(CRWWebControllerPolicyDeciderTest, BlobUrl) { |
| NSURL* blob_url = [NSURL URLWithString:@"blob://aslfkh-asdkjh"]; |
| NSMutableURLRequest* blob_url_request = |
| [NSMutableURLRequest requestWithURL:blob_url]; |
| EXPECT_TRUE(VerifyDecidePolicyForNavigationAction( |
| blob_url_request, WKNavigationActionPolicyAllow)); |
| } |
| |
| // Tests that navigations which close the WebState cancels the navigation. |
| // This occurs, for example, when a new page is opened with a link that is |
| // handled by a native application. |
| TEST_F(CRWWebControllerPolicyDeciderTest, ClosedWebState) { |
| static CRWWebControllerPolicyDeciderTest* test_fixture = nullptr; |
| test_fixture = this; |
| class CloseWebStateDelegate : public FakeWebStateDelegate { |
| public: |
| void CloseWebState(WebState* source) override { |
| test_fixture->DestroyWebState(); |
| } |
| }; |
| CloseWebStateDelegate delegate; |
| web_state()->SetDelegate(&delegate); |
| |
| FakeWebStatePolicyDecider policy_decider(web_state()); |
| policy_decider.SetShouldAllowRequest( |
| web::WebStatePolicyDecider::PolicyDecision::Cancel()); |
| |
| NSURL* url = |
| [NSURL URLWithString:@"https://itunes.apple.com/us/album/american-radio/" |
| @"1449089454?fbclid=IwAR2NKLvDGH_YY4uZbU7Cj_" |
| @"e3h7q7DvORCI4Edvi1K9LjcUHfYObmOWl-YgE"]; |
| NSMutableURLRequest* url_request = [NSMutableURLRequest requestWithURL:url]; |
| EXPECT_TRUE(VerifyDecidePolicyForNavigationAction( |
| url_request, WKNavigationActionPolicyCancel)); |
| } |
| |
| // Tests that navigations are cancelled if the web state is closed in |
| // |ShouldAllowRequest|. |
| TEST_F(CRWWebControllerPolicyDeciderTest, ClosedWebStateInShouldAllowRequest) { |
| static CRWWebControllerPolicyDeciderTest* test_fixture = nullptr; |
| test_fixture = this; |
| |
| class TestWebStatePolicyDecider : public WebStatePolicyDecider { |
| public: |
| explicit TestWebStatePolicyDecider(WebState* web_state) |
| : WebStatePolicyDecider(web_state) {} |
| ~TestWebStatePolicyDecider() override = default; |
| |
| // WebStatePolicyDecider overrides |
| void ShouldAllowRequest(NSURLRequest* request, |
| RequestInfo request_info, |
| PolicyDecisionCallback callback) override { |
| test_fixture->DestroyWebState(); |
| std::move(callback).Run(PolicyDecision::Allow()); |
| } |
| void ShouldAllowResponse(NSURLResponse* response, |
| ResponseInfo response_info, |
| PolicyDecisionCallback callback) override { |
| std::move(callback).Run(PolicyDecision::Allow()); |
| } |
| void WebStateDestroyed() override {} |
| }; |
| TestWebStatePolicyDecider policy_decider(web_state()); |
| |
| NSURL* url = [NSURL URLWithString:@(kTestURLString)]; |
| NSMutableURLRequest* url_request = [NSMutableURLRequest requestWithURL:url]; |
| EXPECT_TRUE(VerifyDecidePolicyForNavigationAction( |
| url_request, WKNavigationActionPolicyCancel)); |
| } |
| |
| // Tests that navigations are allowed if |ShouldAllowRequest| returns a |
| // PolicyDecision which returns true from |ShouldAllowNavigation()|. |
| TEST_F(CRWWebControllerPolicyDeciderTest, AllowRequest) { |
| FakeWebStatePolicyDecider policy_decider(web_state()); |
| policy_decider.SetShouldAllowRequest( |
| web::WebStatePolicyDecider::PolicyDecision::Allow()); |
| |
| NSURL* url = [NSURL URLWithString:@(kTestURLString)]; |
| NSMutableURLRequest* url_request = [NSMutableURLRequest requestWithURL:url]; |
| EXPECT_TRUE(VerifyDecidePolicyForNavigationAction( |
| url_request, WKNavigationActionPolicyAllow)); |
| } |
| |
| // Tests that navigations are cancelled if |ShouldAllowRequest| returns a |
| // PolicyDecision which returns false from |ShouldAllowNavigation()|. |
| TEST_F(CRWWebControllerPolicyDeciderTest, CancelRequest) { |
| FakeWebStatePolicyDecider policy_decider(web_state()); |
| policy_decider.SetShouldAllowRequest( |
| web::WebStatePolicyDecider::PolicyDecision::Cancel()); |
| |
| NSURL* url = [NSURL URLWithString:@(kTestURLString)]; |
| NSMutableURLRequest* url_request = [NSMutableURLRequest requestWithURL:url]; |
| EXPECT_TRUE(VerifyDecidePolicyForNavigationAction( |
| url_request, WKNavigationActionPolicyCancel)); |
| } |
| |
| // Tests that navigations are cancelled if |ShouldAllowRequest| returns a |
| // PolicyDecision which returns true from |ShouldBlockNavigation()|. |
| TEST_F(CRWWebControllerPolicyDeciderTest, CancelRequestAndDisplayError) { |
| FakeWebStatePolicyDecider policy_decider(web_state()); |
| NSError* error = [NSError errorWithDomain:@"Error domain" |
| code:123 |
| userInfo:nil]; |
| policy_decider.SetShouldAllowRequest( |
| web::WebStatePolicyDecider::PolicyDecision::CancelAndDisplayError(error)); |
| |
| NSURL* url = [NSURL URLWithString:@(kTestURLString)]; |
| NSMutableURLRequest* url_request = [NSMutableURLRequest requestWithURL:url]; |
| EXPECT_TRUE(VerifyDecidePolicyForNavigationAction( |
| url_request, WKNavigationActionPolicyCancel)); |
| } |
| |
| // Test fixture for window.open tests. |
| class WindowOpenByDomTest : public WebTestWithWebController { |
| protected: |
| WindowOpenByDomTest() : opener_url_("http://test") {} |
| |
| void SetUp() override { |
| WebTestWithWebController::SetUp(); |
| web_state()->SetDelegate(&delegate_); |
| LoadHtml(@"<html><body></body></html>", opener_url_); |
| } |
| // Executes JavaScript that opens a new window and returns evaluation result |
| // as a string. |
| id OpenWindowByDom() { |
| NSString* const kOpenWindowScript = |
| @"w = window.open('javascript:void(0);', target='_blank');" |
| "w ? w.toString() : null;"; |
| return ExecuteJavaScript(kOpenWindowScript); |
| } |
| |
| // Executes JavaScript that closes previously opened window. |
| void CloseWindow() { ExecuteJavaScript(@"w.close()"); } |
| |
| // URL of a page which opens child windows. |
| const GURL opener_url_; |
| FakeWebStateDelegate delegate_; |
| }; |
| |
| // Tests that absence of web state delegate is handled gracefully. |
| TEST_F(WindowOpenByDomTest, NoDelegate) { |
| web_state()->SetDelegate(nullptr); |
| |
| EXPECT_NSEQ([NSNull null], OpenWindowByDom()); |
| |
| EXPECT_TRUE(delegate_.child_windows().empty()); |
| EXPECT_TRUE(delegate_.popups().empty()); |
| } |
| |
| // Tests that window.open triggered by user gesture opens a new non-popup |
| // window. |
| TEST_F(WindowOpenByDomTest, OpenWithUserGesture) { |
| [web_controller() touched:YES]; |
| EXPECT_NSEQ(@"[object Window]", OpenWindowByDom()); |
| |
| ASSERT_EQ(1U, delegate_.child_windows().size()); |
| ASSERT_TRUE(delegate_.child_windows()[0]); |
| EXPECT_TRUE(delegate_.popups().empty()); |
| } |
| |
| // Tests that window.open executed w/o user gesture does not open a new window, |
| // but blocks popup instead. |
| TEST_F(WindowOpenByDomTest, BlockPopup) { |
| EXPECT_NSEQ([NSNull null], OpenWindowByDom()); |
| |
| EXPECT_TRUE(delegate_.child_windows().empty()); |
| ASSERT_EQ(1U, delegate_.popups().size()); |
| EXPECT_EQ(GURL("javascript:void(0);"), delegate_.popups()[0].url); |
| EXPECT_EQ(opener_url_, delegate_.popups()[0].opener_url); |
| } |
| |
| // Tests that window.open executed w/o user gesture opens a new window, assuming |
| // that delegate allows popups. |
| TEST_F(WindowOpenByDomTest, DontBlockPopup) { |
| delegate_.allow_popups(opener_url_); |
| EXPECT_NSEQ(@"[object Window]", OpenWindowByDom()); |
| |
| ASSERT_EQ(1U, delegate_.child_windows().size()); |
| ASSERT_TRUE(delegate_.child_windows()[0]); |
| EXPECT_TRUE(delegate_.popups().empty()); |
| } |
| |
| // Tests that window.close closes the web state. |
| TEST_F(WindowOpenByDomTest, CloseWindow) { |
| delegate_.allow_popups(opener_url_); |
| ASSERT_NSEQ(@"[object Window]", OpenWindowByDom()); |
| |
| ASSERT_EQ(1U, delegate_.child_windows().size()); |
| ASSERT_TRUE(delegate_.child_windows()[0]); |
| EXPECT_TRUE(delegate_.popups().empty()); |
| |
| delegate_.child_windows()[0]->SetDelegate(&delegate_); |
| CloseWindow(); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_TRUE(delegate_.child_windows().empty()); |
| EXPECT_TRUE(delegate_.popups().empty()); |
| } |
| |
| // Tests page title changes. |
| typedef WebTestWithWebState CRWWebControllerTitleTest; |
| |
| TEST_F(CRWWebControllerTitleTest, TitleChange) { |
| // Observes and waits for TitleWasSet call. |
| class TitleObserver : public WebStateObserver { |
| public: |
| TitleObserver() = default; |
| |
| TitleObserver(const TitleObserver&) = delete; |
| TitleObserver& operator=(const TitleObserver&) = delete; |
| |
| // Returns number of times |TitleWasSet| was called. |
| int title_change_count() { return title_change_count_; } |
| // WebStateObserver overrides: |
| void TitleWasSet(WebState* web_state) override { title_change_count_++; } |
| void WebStateDestroyed(WebState* web_state) override { NOTREACHED(); } |
| |
| private: |
| int title_change_count_ = 0; |
| }; |
| |
| TitleObserver observer; |
| base::ScopedObservation<WebState, WebStateObserver> scoped_observer( |
| &observer); |
| scoped_observer.Observe(web_state()); |
| ASSERT_EQ(0, observer.title_change_count()); |
| |
| // Expect TitleWasSet callback after the page is loaded and due to WKWebView |
| // title change KVO. Title updates happen asynchronously, so wait until the |
| // title is updated. |
| LoadHtml(@"<title>Title1</title>"); |
| EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{ |
| return web_state()->GetTitle() == u"Title1"; |
| })); |
| EXPECT_EQ(2, observer.title_change_count()); |
| |
| // Expect at least one more TitleWasSet callback after changing title via |
| // JavaScript. On iOS 10 WKWebView fires 3 callbacks after JS excucution |
| // with the following title changes: "Title2", "" and "Title2". |
| // TODO(crbug.com/696104): There should be only 2 calls of TitleWasSet. |
| // Fix expecteation when WKWebView stops sending extra KVO calls. |
| ExecuteJavaScript(@"window.document.title = 'Title2';"); |
| EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{ |
| return web_state()->GetTitle() == u"Title2"; |
| })); |
| EXPECT_GE(observer.title_change_count(), 3); |
| } |
| |
| // Tests that fragment change navigations use title from the previous page. |
| TEST_F(CRWWebControllerTitleTest, FragmentChangeNavigationsUsePreviousTitle) { |
| LoadHtml(@"<title>Title1</title>"); |
| ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{ |
| return web_state()->GetTitle() == u"Title1"; |
| })); |
| ExecuteJavaScript(@"window.location.hash = '#1'"); |
| EXPECT_EQ(u"Title1", web_state()->GetTitle()); |
| } |
| |
| // Test fixture for JavaScript execution. |
| class ScriptExecutionTest : public WebTestWithWebController { |
| protected: |
| // Calls |executeUserJavaScript:completionHandler:|, waits for script |
| // execution completion, and synchronously returns the result. |
| id ExecuteUserJavaScript(NSString* java_script, NSError** error) { |
| __block id script_result = nil; |
| __block NSError* script_error = nil; |
| __block bool script_executed = false; |
| [web_controller() |
| executeUserJavaScript:java_script |
| completionHandler:^(id local_result, NSError* local_error) { |
| script_result = local_result; |
| script_error = local_error; |
| script_executed = true; |
| }]; |
| |
| WaitForCondition(^{ |
| return script_executed; |
| }); |
| |
| if (error) { |
| *error = script_error; |
| } |
| return script_result; |
| } |
| }; |
| |
| // Tests evaluating user script on an http page. |
| TEST_F(ScriptExecutionTest, UserScriptOnHttpPage) { |
| LoadHtml(@"<html></html>", GURL(kTestURLString)); |
| NSError* error = nil; |
| EXPECT_NSEQ(@0, ExecuteUserJavaScript(@"window.w = 0;", &error)); |
| EXPECT_FALSE(error); |
| |
| EXPECT_NSEQ(@0, ExecuteJavaScript(@"window.w")); |
| } |
| |
| // Tests evaluating user script on app-specific page. Pages with app-specific |
| // URLs have elevated privileges and JavaScript execution should not be allowed |
| // for them. |
| TEST_F(ScriptExecutionTest, UserScriptOnAppSpecificPage) { |
| LoadHtml(@"<html></html>", GURL(kTestURLString)); |
| |
| // Change last committed URL to app-specific URL. |
| NavigationManagerImpl& nav_manager = |
| [web_controller() webStateImpl]->GetNavigationManagerImpl(); |
| nav_manager.AddPendingItem(GURL(kTestAppSpecificURL), Referrer(), |
| ui::PAGE_TRANSITION_TYPED, |
| NavigationInitiationType::BROWSER_INITIATED, |
| /*is_post_navigation=*/false, |
| /*is_using_https_as_default_scheme=*/false); |
| nav_manager.CommitPendingItem(); |
| |
| NSError* error = nil; |
| EXPECT_FALSE(ExecuteUserJavaScript(@"window.w = 0;", &error)); |
| ASSERT_TRUE(error); |
| EXPECT_NSEQ(kJSEvaluationErrorDomain, error.domain); |
| EXPECT_EQ(JS_EVALUATION_ERROR_CODE_REJECTED, error.code); |
| |
| EXPECT_FALSE(ExecuteJavaScript(@"window.w")); |
| } |
| |
| // Fixture class to test WKWebView crashes. |
| class CRWWebControllerWebProcessTest : public WebTestWithWebController { |
| protected: |
| void SetUp() override { |
| WebTestWithWebController::SetUp(); |
| web_view_ = BuildTerminatedWKWebView(); |
| CRWFakeWebViewContentView* webViewContentView = |
| [[CRWFakeWebViewContentView alloc] |
| initWithMockWebView:web_view_ |
| scrollView:[web_view_ scrollView]]; |
| [web_controller() injectWebViewContentView:webViewContentView]; |
| |
| // This test intentionally crashes the render process. |
| SetIgnoreRenderProcessCrashesDuringTesting(true); |
| } |
| WKWebView* web_view_; |
| }; |
| |
| // Tests that WebStateDelegate::RenderProcessGone is called when WKWebView web |
| // process has crashed. |
| TEST_F(CRWWebControllerWebProcessTest, Crash) { |
| ASSERT_TRUE([web_controller() isViewAlive]); |
| ASSERT_FALSE([web_controller() isWebProcessCrashed]); |
| ASSERT_FALSE(web_state()->IsCrashed()); |
| ASSERT_FALSE(web_state()->IsEvicted()); |
| |
| FakeWebStateObserver observer(web_state()); |
| FakeWebStateObserver* observer_ptr = &observer; |
| SimulateWKWebViewCrash(web_view_); |
| base::test::ios::WaitUntilCondition(^bool() { |
| return observer_ptr->render_process_gone_info(); |
| }); |
| EXPECT_EQ(web_state(), observer.render_process_gone_info()->web_state); |
| EXPECT_FALSE([web_controller() isViewAlive]); |
| EXPECT_TRUE([web_controller() isWebProcessCrashed]); |
| EXPECT_TRUE(web_state()->IsCrashed()); |
| EXPECT_TRUE(web_state()->IsEvicted()); |
| } |
| |
| // Tests that WebState is considered as evicted but not crashed when calling |
| // SetWebUsageEnabled(false). |
| TEST_F(CRWWebControllerWebProcessTest, Eviction) { |
| ASSERT_TRUE([web_controller() isViewAlive]); |
| ASSERT_FALSE([web_controller() isWebProcessCrashed]); |
| ASSERT_FALSE(web_state()->IsCrashed()); |
| ASSERT_FALSE(web_state()->IsEvicted()); |
| |
| web_state()->SetWebUsageEnabled(false); |
| EXPECT_FALSE([web_controller() isViewAlive]); |
| EXPECT_FALSE([web_controller() isWebProcessCrashed]); |
| EXPECT_FALSE(web_state()->IsCrashed()); |
| EXPECT_TRUE(web_state()->IsEvicted()); |
| } |
| |
| // Fixture class to test WKWebView crashes. |
| class CRWWebControllerWebViewTest : public WebTestWithWebController { |
| protected: |
| void SetUp() override { |
| WebTestWithWebController::SetUp(); |
| |
| web_view_ = [[CRWFakeWKWebViewObserverCount alloc] init]; |
| CRWFakeWebViewContentView* webViewContentView = |
| [[CRWFakeWebViewContentView alloc] |
| initWithMockWebView:web_view_ |
| scrollView:web_view_.scrollView]; |
| [web_controller() injectWebViewContentView:webViewContentView]; |
| } |
| CRWFakeWKWebViewObserverCount* web_view_; |
| }; |
| |
| // Tests that the KVO for the WebView are removed when the WebState is |
| // destroyed. See crbug.com/1002786. |
| TEST_F(CRWWebControllerWebViewTest, CheckNoKVOWhenWebStateDestroyed) { |
| // Load a first URL. |
| NSURL* URL = [NSURL URLWithString:@"about:blank"]; |
| NSURLRequest* request = [NSURLRequest requestWithURL:URL]; |
| [web_view_ loadRequest:request]; |
| base::test::ios::WaitUntilCondition(^bool() { |
| return !web_view_.loading; |
| }); |
| |
| // Destroying the WebState should call stop at a point where all observers are |
| // supposed to be removed. |
| DestroyWebState(); |
| |
| EXPECT_FALSE(web_view_.hadObserversWhenStopping); |
| } |
| |
| } // namespace web |