App-bound JavaScript and Navigation failures should have specific error codes.
<rdar://problem/64940268> and https://bugs.webkit.org/show_bug.cgi?id=213808

Reviewed by Tim Hatcher.
(Informally by Kate Cheney)

Source/WebCore:

Covered by API tests.

* bindings/js/ExceptionDetails.h:

Source/WebKit:

* UIProcess/API/Cocoa/WKError.h:
* UIProcess/API/Cocoa/WKError.mm:
(localizedDescriptionForErrorCode):

* UIProcess/API/Cocoa/WKWebView.mm:
(nsErrorFromExceptionDetails):

* UIProcess/Cocoa/WebPageProxyCocoa.mm:
(WebKit::WebPageProxy::errorForUnpermittedAppBoundDomainNavigation):

* UIProcess/WebPageProxy.cpp:
(WebKit::WebPageProxy::decidePolicyForNavigationAction):
* UIProcess/WebPageProxy.h:

* WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::runJavaScript):

Tools:

* TestWebKitAPI/Tests/WebKitCocoa/InAppBrowserPrivacy.mm:
(TEST):


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@263774 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 830ba19..8c9d033 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,15 @@
+2020-06-30  Brady Eidson  <beidson@apple.com>
+
+        App-bound JavaScript and Navigation failures should have specific error codes.
+        <rdar://problem/64940268> and https://bugs.webkit.org/show_bug.cgi?id=213808
+
+        Reviewed by Tim Hatcher.
+        (Informally by Kate Cheney)
+
+        Covered by API tests.
+
+        * bindings/js/ExceptionDetails.h:
+
 2020-06-30  Mark Lam  <mark.lam@apple.com>
 
         Add handling for a case of OOME in CSSTokenizer and CSSParser.
diff --git a/Source/WebCore/bindings/js/ExceptionDetails.h b/Source/WebCore/bindings/js/ExceptionDetails.h
index bd3bd73..6033f80 100644
--- a/Source/WebCore/bindings/js/ExceptionDetails.h
+++ b/Source/WebCore/bindings/js/ExceptionDetails.h
@@ -33,6 +33,7 @@
     enum class Type : uint8_t {
         Script,
         InvalidTargetFrame,
+        AppBoundDomain,
     };
 
     String message;
@@ -53,7 +54,8 @@
     using values = EnumValues<
         WebCore::ExceptionDetails::Type,
         WebCore::ExceptionDetails::Type::Script,
-        WebCore::ExceptionDetails::Type::InvalidTargetFrame
+        WebCore::ExceptionDetails::Type::InvalidTargetFrame,
+        WebCore::ExceptionDetails::Type::AppBoundDomain
     >;
 };
 }
diff --git a/Source/WebKit/ChangeLog b/Source/WebKit/ChangeLog
index 479097ab..93a76d4 100644
--- a/Source/WebKit/ChangeLog
+++ b/Source/WebKit/ChangeLog
@@ -1,3 +1,28 @@
+2020-06-30  Brady Eidson  <beidson@apple.com>
+
+        App-bound JavaScript and Navigation failures should have specific error codes.
+        <rdar://problem/64940268> and https://bugs.webkit.org/show_bug.cgi?id=213808
+
+        Reviewed by Tim Hatcher.
+        (Informally by Kate Cheney)
+
+        * UIProcess/API/Cocoa/WKError.h:
+        * UIProcess/API/Cocoa/WKError.mm:
+        (localizedDescriptionForErrorCode):
+
+        * UIProcess/API/Cocoa/WKWebView.mm:
+        (nsErrorFromExceptionDetails):
+
+        * UIProcess/Cocoa/WebPageProxyCocoa.mm:
+        (WebKit::WebPageProxy::errorForUnpermittedAppBoundDomainNavigation):
+
+        * UIProcess/WebPageProxy.cpp:
+        (WebKit::WebPageProxy::decidePolicyForNavigationAction):
+        * UIProcess/WebPageProxy.h:
+
+        * WebProcess/WebPage/WebPage.cpp:
+        (WebKit::WebPage::runJavaScript):
+
 2020-06-30  Per Arne Vollan  <pvollan@apple.com>
 
         [macOS] Connections to the preference daemon are established before entering the sandbox
diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKError.h b/Source/WebKit/UIProcess/API/Cocoa/WKError.h
index 81a6691..a0a8a7d 100644
--- a/Source/WebKit/UIProcess/API/Cocoa/WKError.h
+++ b/Source/WebKit/UIProcess/API/Cocoa/WKError.h
@@ -45,6 +45,8 @@
  @constant WKErrorContentRuleListStoreVersionMismatch  Indicates that the WKUserContentRuleList version did not match the latest.
  @constant WKErrorAttributedStringContentFailedToLoad  Indicates that the attributed string content failed to load.
  @constant WKErrorAttributedStringContentLoadTimedOut  Indicates that loading attributed string content timed out.
+ @constant WKErrorNavigationAppBoundDomain  Indicates that a navigation failed due to an app-bound domain restriction.
+ @constant WKErrorJavaScriptAppBoundDomain  Indicates that JavaScript execution failed due to an app-bound domain restriction.
  */
 typedef NS_ENUM(NSInteger, WKErrorCode) {
     WKErrorUnknown = 1,
@@ -59,6 +61,8 @@
     WKErrorAttributedStringContentFailedToLoad WK_API_AVAILABLE(macos(10.15), ios(13.0)),
     WKErrorAttributedStringContentLoadTimedOut WK_API_AVAILABLE(macos(10.15), ios(13.0)),
     WKErrorJavaScriptInvalidFrameTarget WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA)),
+    WKErrorNavigationAppBoundDomain WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA)),
+    WKErrorJavaScriptAppBoundDomain WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA)),
 } WK_API_AVAILABLE(macos(10.10), ios(8.0));
 
 NS_ASSUME_NONNULL_END
diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKError.mm b/Source/WebKit/UIProcess/API/Cocoa/WKError.mm
index eec76da..ef05e0a 100644
--- a/Source/WebKit/UIProcess/API/Cocoa/WKError.mm
+++ b/Source/WebKit/UIProcess/API/Cocoa/WKError.mm
@@ -76,6 +76,12 @@
 
     case WKErrorJavaScriptInvalidFrameTarget:
         return WEB_UI_STRING("JavaScript execution targeted an invalid frame", "WKErrorJavaScriptInvalidFrameTarget description");
+
+    case WKErrorNavigationAppBoundDomain:
+        return WEB_UI_STRING("Attempted to navigate away from an app-bound domain or navigate after using restricted APIs", "WKErrorNavigationAppBoundDomain description");
+
+    case WKErrorJavaScriptAppBoundDomain:
+        return WEB_UI_STRING("JavaScript execution targeted a frame that is not in an app-bound domain", "WKErrorJavaScriptAppBoundDomain description");
     }
 }
 
diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm b/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
index 4a338ec..23b7c19 100644
--- a/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
+++ b/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
@@ -876,6 +876,9 @@
     case WebCore::ExceptionDetails::Type::Script:
         errorCode = WKErrorJavaScriptExceptionOccurred;
         break;
+    case WebCore::ExceptionDetails::Type::AppBoundDomain:
+        errorCode = WKErrorJavaScriptAppBoundDomain;
+        break;
     }
 
     [userInfo setObject:localizedDescriptionForErrorCode(errorCode) forKey:NSLocalizedDescriptionKey];
diff --git a/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm b/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm
index 72e4833..c6c4b69 100644
--- a/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm
+++ b/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm
@@ -41,6 +41,7 @@
 #import "WebPasteboardProxy.h"
 #import "WebProcessProxy.h"
 #import "WebsiteDataStore.h"
+#import "WKErrorInternal.h"
 #import <WebCore/DragItem.h>
 #import <WebCore/LocalCurrentGraphicsContext.h>
 #import <WebCore/NotImplemented.h>
@@ -301,6 +302,11 @@
 
     send(Messages::WebPage::InsertDictatedTextAsync { text, replacementRange, dictationAlternatives, WTFMove(options) });
 }
+
+ResourceError WebPageProxy::errorForUnpermittedAppBoundDomainNavigation(const URL& url)
+{
+    return { WKErrorDomain, WKErrorNavigationAppBoundDomain, url, localizedDescriptionForErrorCode(WKErrorNavigationAppBoundDomain) };
+}
     
 #if ENABLE(APPLE_PAY)
 
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index 7a747255..89f4a8dd 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -5204,16 +5204,18 @@
             }
             receivedNavigationPolicyDecision(policyAction, navigation.get(), processSwapRequestedByClient, frame, WTFMove(policies), WTFMove(sender));
         };
-        
+
+#if PLATFORM(COCOA)
         if (policyAction != PolicyAction::Ignore) {
             if (!setIsNavigatingToAppBoundDomainAndCheckIfPermitted(frame->isMainFrame(), navigation->currentRequest().url(), isAppBoundDomain)) {
-                auto error = ResourceError { String { }, 0, navigation->currentRequest().url(), "App-bound domain failure"_s };
+                auto error = errorForUnpermittedAppBoundDomainNavigation(navigation->currentRequest().url());
                 m_navigationClient->didFailProvisionalNavigationWithError(*this, FrameInfoData { frameInfo }, navigation.get(), error, userDataObject);
                 RELEASE_LOG_ERROR_IF_ALLOWED(Loading, "Ignoring request to load this main resource because it is attempting to navigate away from an app-bound domain or navigate after using restricted APIs");
                 completionHandler(PolicyAction::Ignore);
                 return;
             }
         }
+#endif
 
         if (!m_pageClient)
             return completionHandler(policyAction);
diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h
index ae195bd..3c35aea 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.h
+++ b/Source/WebKit/UIProcess/WebPageProxy.h
@@ -1760,6 +1760,10 @@
 
     Optional<NavigatingToAppBoundDomain> isNavigatingToAppBoundDomain() const { return m_isNavigatingToAppBoundDomain; }
 
+#if PLATFORM(COCOA)
+    WebCore::ResourceError errorForUnpermittedAppBoundDomainNavigation(const URL&);
+#endif
+
     void disableServiceWorkerEntitlementInNetworkProcess();
     void clearServiceWorkerEntitlementOverride(CompletionHandler<void()>&&);
         
diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.cpp b/Source/WebKit/WebProcess/WebPage/WebPage.cpp
index 1bfe3bf..779668b 100644
--- a/Source/WebKit/WebProcess/WebPage/WebPage.cpp
+++ b/Source/WebKit/WebProcess/WebPage/WebPage.cpp
@@ -3458,7 +3458,7 @@
         send(Messages::WebPageProxy::ScriptValueCallback(dataReference, details, callbackID));
     };
     if (shouldEnableInAppBrowserPrivacyProtections()) {
-        send(Messages::WebPageProxy::ScriptValueCallback({ }, ExceptionDetails { "Unable to execute JavaScript"_s }, callbackID));
+        send(Messages::WebPageProxy::ScriptValueCallback({ }, ExceptionDetails { "Unable to execute JavaScript in a frame that is not in an app-bound domain"_s, 0, 0, ExceptionDetails::Type::AppBoundDomain }, callbackID));
         if (auto* document = m_page->mainFrame().document())
             document->addConsoleMessage(MessageSource::Security, MessageLevel::Warning, "Ignoring user script injection for non-app bound domain.");
         RELEASE_LOG_ERROR_IF_ALLOWED(Loading, "runJavaScript: Ignoring user script injection for non app-bound domain");
diff --git a/Tools/ChangeLog b/Tools/ChangeLog
index 2dedf53..7060fc6 100644
--- a/Tools/ChangeLog
+++ b/Tools/ChangeLog
@@ -1,3 +1,14 @@
+2020-06-30  Brady Eidson  <beidson@apple.com>
+
+        App-bound JavaScript and Navigation failures should have specific error codes.
+        <rdar://problem/64940268> and https://bugs.webkit.org/show_bug.cgi?id=213808
+
+        Reviewed by Tim Hatcher.
+        (Informally by Kate Cheney)
+
+        * TestWebKitAPI/Tests/WebKitCocoa/InAppBrowserPrivacy.mm:
+        (TEST):
+
 2020-06-30  Peng Liu  <peng.liu6@apple.com>
 
         Enable the support of FULLSCREEN_API in WebKitTestRunner
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/InAppBrowserPrivacy.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/InAppBrowserPrivacy.mm
index 6753b2f..44c3b36 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/InAppBrowserPrivacy.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/InAppBrowserPrivacy.mm
@@ -149,6 +149,7 @@
     [webView evaluateJavaScript:@"window.wkUserScriptInjected" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
         EXPECT_FALSE(result);
         EXPECT_TRUE(!!error);
+        EXPECT_EQ(error.code, WKErrorJavaScriptAppBoundDomain);
         isDone = true;
     }];
 
@@ -188,6 +189,7 @@
     [webView evaluateJavaScript:@"window.wkUserScriptInjected" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
         EXPECT_FALSE(result);
         EXPECT_TRUE(!!error);
+        EXPECT_EQ(error.code, WKErrorJavaScriptAppBoundDomain);
         isDone = true;
     }];
 
@@ -242,6 +244,7 @@
     [webView2 evaluateJavaScript:@"window.wkUserScriptInjected" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
         EXPECT_FALSE(result);
         EXPECT_TRUE(!!error);
+        EXPECT_EQ(error.code, WKErrorJavaScriptAppBoundDomain);
         cleanUpInAppBrowserPrivacyTestSettings();
         isDone = true;
     }];
@@ -364,7 +367,7 @@
     NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"in-app-browser:///in-app-browser-privacy-test-user-style-sheets"]];
     [webView loadRequest:request];
     NSError *error = [delegate waitForDidFailProvisionalNavigationError];
-    EXPECT_WK_STREQ(error.localizedDescription, @"App-bound domain failure");
+    EXPECT_EQ(error.code, WKErrorNavigationAppBoundDomain);
     cleanUpInAppBrowserPrivacyTestSettings();
 }
 
@@ -387,7 +390,7 @@
     NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"in-app-browser:///in-app-browser-privacy-test-user-style-sheets"]];
     [webView loadRequest:request];
     NSError *error = [delegate waitForDidFailProvisionalNavigationError];
-    EXPECT_WK_STREQ(error.localizedDescription, @"App-bound domain failure");
+    EXPECT_EQ(error.code, WKErrorNavigationAppBoundDomain);
     cleanUpInAppBrowserPrivacyTestSettings();
 }
 
@@ -409,7 +412,7 @@
     NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"in-app-browser:///in-app-browser-privacy-test-user-style-sheets-iframe"]];
     [webView loadRequest:request];
     NSError *error = [delegate waitForDidFailProvisionalNavigationError];
-    EXPECT_WK_STREQ(error.localizedDescription, @"App-bound domain failure");
+    EXPECT_EQ(error.code, WKErrorNavigationAppBoundDomain);
     cleanUpInAppBrowserPrivacyTestSettings();
 }
 
@@ -870,7 +873,7 @@
     NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"in-app-browser:///in-app-browser-privacy-test-user-style-sheets"]];
     [webView loadRequest:request];
     NSError *error = [delegate waitForDidFailProvisionalNavigationError];
-    EXPECT_WK_STREQ(error.localizedDescription, @"App-bound domain failure");
+    EXPECT_EQ(error.code, WKErrorNavigationAppBoundDomain);
 
     // Make sure the load didn't complete by checking the background color.
     // Red would indicate it finished loading.
@@ -901,7 +904,7 @@
     request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"in-app-browser:///in-app-browser-privacy-test-user-style-sheets"]];
     [webView loadRequest:request];
     NSError *error = [delegate waitForDidFailProvisionalNavigationError];
-    EXPECT_WK_STREQ(error.localizedDescription, @"App-bound domain failure");
+    EXPECT_EQ(error.code, WKErrorNavigationAppBoundDomain);
 
     // Make sure the load didn't complete by checking the background color.
     // Red would indicate it finished loading.
@@ -980,6 +983,7 @@
     isDone = false;
     [webView evaluateJavaScript:@"window.wkUserScriptInjected" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
         EXPECT_TRUE(!!error);
+        EXPECT_EQ(error.code, WKErrorJavaScriptAppBoundDomain);
         isDone = true;
     }];
 
@@ -1017,7 +1021,7 @@
     request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"in-app-browser:///in-app-browser-privacy-test-user-style-sheets"]];
     [webView loadRequest:request];
     NSError *error = [delegate waitForDidFailProvisionalNavigationError];
-    EXPECT_WK_STREQ(error.localizedDescription, @"App-bound domain failure");
+    EXPECT_EQ(error.code, WKErrorNavigationAppBoundDomain);
 
     cleanUpInAppBrowserPrivacyTestSettings();
 }
@@ -1050,7 +1054,7 @@
     NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"in-app-browser:///in-app-browser-privacy-test-user-agent-script"]];
     [webView loadRequest:request];
     NSError *error = [delegate waitForDidFailProvisionalNavigationError];
-    EXPECT_WK_STREQ(error.localizedDescription, @"App-bound domain failure");
+    EXPECT_EQ(error.code, WKErrorNavigationAppBoundDomain);
 }
 
 TEST(InAppBrowserPrivacy, WebViewCategory)
@@ -1137,7 +1141,7 @@
 
     [webView loadHTMLString:HTML baseURL:[NSURL URLWithString:@"in-app-browser:///in-app-browser-privacy-test-user-agent-script"]];
     NSError *error = [delegate waitForDidFailProvisionalNavigationError];
-    EXPECT_WK_STREQ(error.localizedDescription, @"App-bound domain failure");
+    EXPECT_EQ(error.code, WKErrorNavigationAppBoundDomain);
 
     isDone = false;
     [webView _isForcedIntoAppBoundMode:^(BOOL isForcedIntoAppBoundMode) {