Commit 86249714 authored by Mike Dougherty's avatar Mike Dougherty Committed by Commit Bot

[iOS] Add WebStatePolicyDecider::PolicyDecision::CancelAndDisplayError

Returning a PolicyDecision which was created with CancelAndDisplayError
from ShouldAllowRequest will cancel the request and display the error to
the user. This can be used to display to the user why a request was
cancelled.

Bug: 1065161
Change-Id: I4418cb099aa7fab01a3bca6fa08d3c5e67fce986
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2140958
Commit-Queue: Mike Dougherty <michaeldo@chromium.org>
Reviewed-by: default avatarGauthier Ambard <gambard@chromium.org>
Reviewed-by: default avatarEugene But <eugenebut@chromium.org>
Cr-Commit-Position: refs/heads/master@{#759567}
parent a061d48a
......@@ -533,6 +533,7 @@ test("ios_web_inttests") {
deps = [
":web",
"//base/test:test_support",
"//ios/net",
"//ios/testing:embedded_test_server_support",
"//ios/testing:http_server_bundle_data",
"//ios/web:resources_grit",
......
......@@ -28,12 +28,27 @@ class WebStatePolicyDecider {
// A policy decision which cancels the navigation.
static PolicyDecision Cancel();
// A policy decision which cancels the navigation and displays |error|.
// NOTE: The |error| will only be displayed if the associated navigation is
// being loaded in the main frame.
static PolicyDecision CancelAndDisplayError(NSError* error);
// Whether or not the navigation will continue.
bool ShouldAllowNavigation() const;
// Whether or not the navigation will be cancelled.
bool ShouldCancelNavigation() const;
// Whether or not an error should be displayed. Always returns false if
// |ShouldAllowNavigation| is true.
// NOTE: Will return true when the receiver is created with
// |CancelAndDisplayError| even though an error will only end up being
// displayed if the associated navigation is occurring in the main frame.
bool ShouldDisplayError() const;
// The error to display when |ShouldDisplayError| is true.
NSError* GetDisplayError() const;
private:
// The decisions which can be taken for a given navigation.
enum class Decision {
......@@ -41,13 +56,21 @@ class WebStatePolicyDecider {
kAllow,
// Cancel the navigation.
kCancel
kCancel,
// Cancel the navigation and display an error.
kCancelAndDisplayError,
};
PolicyDecision(Decision decision) : decision(decision) {}
PolicyDecision(Decision decision, NSError* error)
: decision(decision), error(error) {}
// The decision to be taken for a given navigation.
Decision decision = Decision::kAllow;
// An error associated with the navigation. This error will be displayed if
// |decision| is |kCancelAndDisplayError|.
NSError* error = nil;
};
// Data Transfer Object for the additional information about navigation
......
......@@ -8,10 +8,13 @@
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#include "base/test/scoped_feature_list.h"
#include "ios/testing/embedded_test_server_handlers.h"
#include "ios/web/common/features.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#include "ios/web/public/navigation/reload_type.h"
#include "ios/web/public/navigation/web_state_policy_decider.h"
#include "ios/web/public/security/security_style.h"
#include "ios/web/public/security/ssl_status.h"
#include "ios/web/public/test/element_selector.h"
......@@ -49,6 +52,42 @@ bool WaitForErrorText(WebState* web_state, const GURL& url) {
/*is_post=*/false, /*is_otr=*/false,
/*cert_status=*/0));
}
// The error domain and code presented by |TestWebStatePolicyDecider| for
// cancelled navigations.
const char kCancelledNavigationErrorDomain[] = "Error domain";
const int kCancelledNavigationErrorCode = 123;
// A WebStatePolicyDecider which cancels requests to URLs of the form
// "/echo-query?blocked" and displays an error.
class TestWebStatePolicyDecider : public WebStatePolicyDecider {
public:
explicit TestWebStatePolicyDecider(WebState* web_state)
: WebStatePolicyDecider(web_state) {}
~TestWebStatePolicyDecider() override = default;
// WebStatePolicyDecider overrides
PolicyDecision ShouldAllowRequest(NSURLRequest* request,
const RequestInfo& request_info) override {
// Don't block the error page itself.
// TODO(crbug.com/1069151): ShouldAllowRequest shouldn't be called for the
// presentation of error pages.
if ([request.URL.scheme isEqualToString:@"file"]) {
return PolicyDecision::Allow();
}
if ([request.URL.path isEqualToString:@"/echo-query"] &&
[request.URL.query isEqualToString:@"blocked"]) {
NSError* error =
[NSError errorWithDomain:base::SysUTF8ToNSString(
kCancelledNavigationErrorDomain)
code:kCancelledNavigationErrorCode
userInfo:nil];
return PolicyDecision::CancelAndDisplayError(error);
}
return PolicyDecision::Allow();
}
};
} // namespace
// Test fixture for error page testing. Error page simply renders the arguments
......@@ -342,4 +381,66 @@ TEST_F(ErrorPageTest, URLAndVirtualURLAfterError) {
EXPECT_EQ(virtual_url, manager->GetLastCommittedItem()->GetVirtualURL());
}
// Tests that an error page is displayed when a WebStatePolicyDecider returns a
// PolicyDecision created with PolicyDecision::CancelAndDisplayError() from
// WebStatePolicyDecider::ShouldAllowRequest() and that the error page loads
// correctly when navigating forward to the error page.
TEST_F(ErrorPageTest, ShouldAllowRequestCancelAndDisplayErrorForwardNav) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(web::features::kUseJSForErrorPage);
TestWebStatePolicyDecider policy_decider(web_state());
// Load successful page.
GURL allowed_url = server_.GetURL("/echo-query?allowed");
test::LoadUrl(web_state(), allowed_url);
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), "allowed"));
// Load page which is blocked.
GURL blocked_url = server_.GetURL("/echo-query?blocked");
test::LoadUrl(web_state(), blocked_url);
std::string error_text = testing::GetErrorText(
web_state(), blocked_url, kCancelledNavigationErrorDomain,
/*error_code=*/kCancelledNavigationErrorCode,
/*is_post=*/false, /*is_otr=*/false,
/*cert_status=*/0);
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), error_text));
// Go back/forward to validate going forward to error page.
web_state()->GetNavigationManager()->GoBack();
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), "allowed"));
web_state()->GetNavigationManager()->GoForward();
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), error_text));
}
// Tests that an error page is displayed when a WebStatePolicyDecider returns a
// PolicyDecision created with PolicyDecision::CancelAndDisplayError() from
// WebStatePolicyDecider::ShouldAllowRequest() and that the error page loads
// correctly when navigating back to the error page.
TEST_F(ErrorPageTest, ShouldAllowRequestCancelAndDisplayErrorBackNav) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(web::features::kUseJSForErrorPage);
TestWebStatePolicyDecider policy_decider(web_state());
// Load page which is blocked.
GURL blocked_url = server_.GetURL("/echo-query?blocked");
test::LoadUrl(web_state(), blocked_url);
std::string error_text = testing::GetErrorText(
web_state(), blocked_url, kCancelledNavigationErrorDomain,
/*error_code=*/kCancelledNavigationErrorCode,
/*is_post=*/false, /*is_otr=*/false,
/*cert_status=*/0);
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), error_text));
// Load successful page.
GURL allowed_url = server_.GetURL("/echo-query?allowed");
test::LoadUrl(web_state(), allowed_url);
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), "allowed"));
// Go back/forward to validate going back to error page.
web_state()->GetNavigationManager()->GoBack();
ASSERT_TRUE(test::WaitForWebViewContainingText(web_state(), error_text));
}
} // namespace web
......@@ -199,6 +199,8 @@ class CRWWebControllerTest : public WebTestWithWebController {
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
......@@ -1049,6 +1051,25 @@ TEST_F(CRWWebControllerPolicyDeciderTest, CancelRequest) {
url_request, WKNavigationActionPolicyCancel));
}
// Tests that navigations are cancelled if |ShouldAllowRequest| returns a
// PolicyDecision which returns true from |ShouldBlockNavigation()|.
TEST_F(CRWWebControllerPolicyDeciderTest, CancelRequestAndDisplayError) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(web::features::kUseJSForErrorPage);
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:
......
......@@ -13,8 +13,11 @@
#include "base/path_service.h"
#include "base/scoped_observer.h"
#include "base/strings/stringprintf.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/net/protocol_handler_util.h"
#include "ios/testing/embedded_test_server_handlers.h"
#include "ios/web/common/features.h"
#include "ios/web/navigation/wk_navigation_util.h"
......@@ -36,6 +39,7 @@
#import "ios/web/web_state/ui/crw_web_controller.h"
#import "ios/web/web_state/web_state_impl.h"
#import "net/base/mac/url_conversions.h"
#import "net/base/net_errors.h"
#include "net/http/http_response_headers.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
......@@ -1998,6 +2002,64 @@ TEST_F(WebStateObserverTest, DisallowRequest) {
EXPECT_FALSE(web_state()->GetNavigationManager()->GetVisibleItem());
}
// Tests rejecting the navigation from ShouldAllowRequest with an error. The
// load should stop, and an error page should be loaded.
TEST_F(WebStateObserverTest, DisallowRequestAndShowError) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(web::features::kUseJSForErrorPage);
EXPECT_CALL(observer_, DidStartLoading(web_state()));
WebStatePolicyDecider::RequestInfo expected_request_info(
ui::PageTransition::PAGE_TRANSITION_TYPED,
/*target_main_frame=*/true, /*has_user_gesture=*/false);
NSError* error = [NSError errorWithDomain:net::kNSErrorDomain
code:net::ERR_BLOCKED_BY_ADMINISTRATOR
userInfo:nil];
EXPECT_CALL(*decider_,
ShouldAllowRequest(_, RequestInfoMatch(expected_request_info)))
.WillOnce(Return(
WebStatePolicyDecider::PolicyDecision::CancelAndDisplayError(error)));
// Allow error page to load
// TODO(crbug.com/1069151): ShouldAllowRequest shouldn't be called for the
// presentation of error pages.
WebStatePolicyDecider::RequestInfo expected_request_info_2(
ui::PageTransition::PAGE_TRANSITION_CLIENT_REDIRECT,
/*target_main_frame=*/true, /*has_user_gesture=*/false);
EXPECT_CALL(*decider_,
ShouldAllowRequest(_, RequestInfoMatch(expected_request_info_2)))
.WillOnce(Return(WebStatePolicyDecider::PolicyDecision::Allow()));
EXPECT_CALL(observer_, DidStartNavigation(web_state(), _));
EXPECT_CALL(*decider_, ShouldAllowResponse(_, /*for_main_frame=*/true))
.WillOnce(Return(true));
EXPECT_CALL(observer_, DidFinishNavigation(web_state(), _));
EXPECT_CALL(observer_, TitleWasSet(web_state()));
EXPECT_CALL(observer_, DidStopLoading(web_state()));
EXPECT_CALL(observer_,
PageLoaded(web_state(), PageLoadCompletionStatus::FAILURE));
// TODO(crbug.com/1071117): DidStartNavigation is over-triggered when
// |web::features::kUseJSForErrorPage| is enabled.
EXPECT_CALL(observer_, DidStartNavigation(web_state(), _));
EXPECT_CALL(observer_, DidFinishNavigation(web_state(), _));
EXPECT_CALL(observer_, DidStartNavigation(web_state(), _));
EXPECT_CALL(observer_, DidFinishNavigation(web_state(), _));
EXPECT_CALL(observer_,
PageLoaded(web_state(), PageLoadCompletionStatus::SUCCESS));
GURL url = test_server_->GetURL("/echo");
test::LoadUrl(web_state(), url);
EXPECT_TRUE(test::WaitForWebViewContainingText(
web_state(), testing::GetErrorText(web_state(), url,
base::SysNSStringToUTF8(error.domain),
/*error_code=*/error.code,
/*is_post=*/false, /*is_otr=*/false,
/*cert_status=*/0)));
// The URL of the error page should remain the URL of the blocked page.
EXPECT_EQ(url.spec(), web_state()->GetVisibleURL());
}
// Tests rejecting the navigation from ShouldAllowResponse. PageLoaded callback
// is not called.
TEST_F(WebStateObserverTest, DisallowResponse) {
......
......@@ -17,14 +17,22 @@ namespace web {
WebStatePolicyDecider::PolicyDecision
WebStatePolicyDecider::PolicyDecision::Allow() {
return WebStatePolicyDecider::PolicyDecision(
WebStatePolicyDecider::PolicyDecision::Decision::kAllow);
WebStatePolicyDecider::PolicyDecision::Decision::kAllow, /*error=*/nil);
}
// static
WebStatePolicyDecider::PolicyDecision
WebStatePolicyDecider::PolicyDecision::Cancel() {
return WebStatePolicyDecider::PolicyDecision(
WebStatePolicyDecider::PolicyDecision::Decision::kCancel);
WebStatePolicyDecider::PolicyDecision::Decision::kCancel, /*error=*/nil);
}
// static
WebStatePolicyDecider::PolicyDecision
WebStatePolicyDecider::PolicyDecision::CancelAndDisplayError(NSError* error) {
return WebStatePolicyDecider::PolicyDecision(
WebStatePolicyDecider::PolicyDecision::Decision::kCancelAndDisplayError,
error);
}
bool WebStatePolicyDecider::PolicyDecision::ShouldAllowNavigation() const {
......@@ -35,6 +43,15 @@ bool WebStatePolicyDecider::PolicyDecision::ShouldCancelNavigation() const {
return !ShouldAllowNavigation();
}
bool WebStatePolicyDecider::PolicyDecision::ShouldDisplayError() const {
return decision == WebStatePolicyDecider::PolicyDecision::Decision::
kCancelAndDisplayError;
}
NSError* WebStatePolicyDecider::PolicyDecision::GetDisplayError() const {
return error;
}
WebStatePolicyDecider::WebStatePolicyDecider(WebState* web_state)
: web_state_(web_state) {
DCHECK(web_state_);
......
......@@ -21,6 +21,8 @@ TEST_F(WebStatePolicyDeciderTest, PolicyDecisionAllow) {
web::WebStatePolicyDecider::PolicyDecision::Allow();
EXPECT_TRUE(policy_decision.ShouldAllowNavigation());
EXPECT_FALSE(policy_decision.ShouldCancelNavigation());
EXPECT_FALSE(policy_decision.ShouldDisplayError());
EXPECT_NSEQ(nil, policy_decision.GetDisplayError());
}
// Tests that PolicyDecision::Cancel() creates a PolicyDecision with expected
......@@ -30,4 +32,20 @@ TEST_F(WebStatePolicyDeciderTest, PolicyDecisionCancel) {
web::WebStatePolicyDecider::PolicyDecision::Cancel();
EXPECT_FALSE(policy_decision.ShouldAllowNavigation());
EXPECT_TRUE(policy_decision.ShouldCancelNavigation());
EXPECT_FALSE(policy_decision.ShouldDisplayError());
EXPECT_NSEQ(nil, policy_decision.GetDisplayError());
}
// Tests that PolicyDecision::CancelAndDisplayError() creates a PolicyDecision
// with expected state.
TEST_F(WebStatePolicyDeciderTest, PolicyDecisionCancelAndDisplayError) {
NSError* error = [NSError errorWithDomain:@"ErrorDomain"
code:123
userInfo:nil];
web::WebStatePolicyDecider::PolicyDecision policy_decision =
web::WebStatePolicyDecider::PolicyDecision::CancelAndDisplayError(error);
EXPECT_FALSE(policy_decision.ShouldAllowNavigation());
EXPECT_TRUE(policy_decision.ShouldCancelNavigation());
EXPECT_TRUE(policy_decision.ShouldDisplayError());
EXPECT_NSEQ(error, policy_decision.GetDisplayError());
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment