Add Third Party Cookie API

Bug: 11678084
Change-Id: Ib92dedda8fbb822c64c2003426f7927a4409f401
diff --git a/libs/testserver/src/android/webkit/cts/CtsTestServer.java b/libs/testserver/src/android/webkit/cts/CtsTestServer.java
index 6416b73..b34cfc8 100644
--- a/libs/testserver/src/android/webkit/cts/CtsTestServer.java
+++ b/libs/testserver/src/android/webkit/cts/CtsTestServer.java
@@ -111,7 +111,9 @@
     private static final String QUERY_REDIRECT_PATH = "/alt_redirect";
     private static final String DELAY_PREFIX = "/delayed";
     private static final String BINARY_PREFIX = "/binary";
+    private static final String SET_COOKIE_PREFIX = "/setcookie";
     private static final String COOKIE_PREFIX = "/cookie";
+    private static final String LINKED_SCRIPT_PREFIX = "/linkedscriptprefix";
     private static final String AUTH_PREFIX = "/auth";
     private static final String SHUTDOWN_PREFIX = "/shutdown";
     public static final String NOLENGTH_POSTFIX = "nolength";
@@ -354,7 +356,6 @@
         return sb.toString();
     }
 
-
     /**
      * Return an absolute URL that indirectly refers to the given asset.
      * When a client fetches this URL, the server will respond with a temporary redirect (302)
@@ -400,6 +401,43 @@
         return sb.toString();
     }
 
+    /**
+     * getSetCookieUrl returns a URL that attempts to set the cookie
+     * "key=value" when fetched.
+     * @param path a suffix to disambiguate mulitple Cookie URLs.
+     * @param key the key of the cookie.
+     * @return the url for a page that attempts to set the cookie.
+     */
+    public String getSetCookieUrl(String path, String key, String value) {
+        StringBuilder sb = new StringBuilder(getBaseUri());
+        sb.append(SET_COOKIE_PREFIX);
+        sb.append(path);
+        sb.append("?key=");
+        sb.append(key);
+        sb.append("&value=");
+        sb.append(value);
+        return sb.toString();
+    }
+
+    /**
+     * getLinkedScriptUrl returns a URL for a page with a script tag where
+     * src equals the URL passed in.
+     * @param path a suffix to disambiguate mulitple Linked Script URLs.
+     * @param url the src of the script tag.
+     * @return the url for the page with the script link in.
+     */
+    public String getLinkedScriptUrl(String path, String url) {
+        StringBuilder sb = new StringBuilder(getBaseUri());
+        sb.append(LINKED_SCRIPT_PREFIX);
+        sb.append(path);
+        sb.append("?url=");
+        try {
+            sb.append(URLEncoder.encode(url, "UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+        }
+        return sb.toString();
+    }
+
     public String getBinaryUrl(String mimeType, int contentLength) {
         StringBuilder sb = new StringBuilder(getBaseUri());
         sb.append(BINARY_PREFIX);
@@ -681,8 +719,20 @@
             }
 
             response.addHeader("Set-Cookie", "count=" + count + "; path=" + COOKIE_PREFIX);
-            response.setEntity(createEntity("<html><head><title>" + cookieString +
-                    "</title></head><body>" + cookieString + "</body></html>"));
+            response.setEntity(createPage(cookieString.toString(), cookieString.toString()));
+        } else if (path.startsWith(SET_COOKIE_PREFIX)) {
+            response = createResponse(HttpStatus.SC_OK);
+            Uri parsedUri = Uri.parse(uriString);
+            String key = parsedUri.getQueryParameter("key");
+            String value = parsedUri.getQueryParameter("value");
+            String cookie = key + "=" + value;
+            response.addHeader("Set-Cookie", cookie);
+            response.setEntity(createPage(cookie, cookie));
+        } else if (path.startsWith(LINKED_SCRIPT_PREFIX)) {
+            response = createResponse(HttpStatus.SC_OK);
+            String src = Uri.parse(uriString).getQueryParameter("url");
+            String scriptTag = "<script src=\"" + src + "\"></script>";
+            response.setEntity(createPage("LinkedScript", scriptTag));
         } else if (path.equals(USERAGENT_PATH)) {
             response = createResponse(HttpStatus.SC_OK);
             Header agentHeader = request.getFirstHeader("User-Agent");
@@ -690,8 +740,7 @@
             if (agentHeader != null) {
                 agent = agentHeader.getValue();
             }
-            response.setEntity(createEntity("<html><head><title>" + agent + "</title></head>" +
-                    "<body>" + agent + "</body></html>"));
+            response.setEntity(createPage(agent, agent));
         } else if (path.equals(TEST_DOWNLOAD_PATH)) {
             response = createTestDownloadResponse(Uri.parse(uriString));
         } else if (path.equals(SHUTDOWN_PREFIX)) {
@@ -772,12 +821,7 @@
         // Fill in error reason. Avoid use of the ReasonPhraseCatalog, which is Locale-dependent.
         String reason = getReasonString(status);
         if (reason != null) {
-            StringBuffer buf = new StringBuffer("<html><head><title>");
-            buf.append(reason);
-            buf.append("</title></head><body>");
-            buf.append(reason);
-            buf.append("</body></html>");
-            response.setEntity(createEntity(buf.toString()));
+            response.setEntity(createPage(reason, reason));
         }
         return response;
     }
@@ -796,6 +840,14 @@
         return null;
     }
 
+    /**
+     * Create a string entity for a bare bones html page with provided title and body.
+     */
+    private static StringEntity createPage(String title, String bodyContent) {
+        return createEntity("<html><head><title>" + title + "</title></head>" +
+                "<body>" + bodyContent + "</body></html>");
+    }
+
     private static HttpResponse createTestDownloadResponse(Uri uri) throws IOException {
         String downloadId = uri.getQueryParameter(DOWNLOAD_ID_PARAMETER);
         int numBytes = uri.getQueryParameter(NUM_BYTES_PARAMETER) != null
diff --git a/tests/src/android/webkit/cts/WebViewOnUiThread.java b/tests/src/android/webkit/cts/WebViewOnUiThread.java
index c1032a6..4a4d62f 100644
--- a/tests/src/android/webkit/cts/WebViewOnUiThread.java
+++ b/tests/src/android/webkit/cts/WebViewOnUiThread.java
@@ -31,6 +31,7 @@
 import android.view.ViewGroup;
 import android.view.ViewParent;
 import android.webkit.DownloadListener;
+import android.webkit.CookieManager;
 import android.webkit.ValueCallback;
 import android.webkit.WebBackForwardList;
 import android.webkit.WebChromeClient;
@@ -737,6 +738,24 @@
         });
     }
 
+    public void setAcceptThirdPartyCookies(final boolean accept) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                CookieManager.getInstance().setAcceptThirdPartyCookies(mWebView, accept);
+            }
+        });
+    }
+
+    public boolean acceptThirdPartyCookies() {
+        return getValue(new ValueGetter<Boolean>() {
+            @Override
+            public Boolean capture() {
+                return CookieManager.getInstance().acceptThirdPartyCookies(mWebView);
+            }
+        });
+    }
+
     /**
      * Helper for running code on the UI thread where an exception is
      * a test failure. If this is already the UI thread then it runs
@@ -766,7 +785,7 @@
         return mWebView;
     }
 
-    private <T> T getValue(ValueGetter<T> getter) {
+    private<T> T getValue(ValueGetter<T> getter) {
         runOnUiThread(getter);
         return getter.getValue();
     }
diff --git a/tests/tests/webkit/src/android/webkit/cts/CookieManagerTest.java b/tests/tests/webkit/src/android/webkit/cts/CookieManagerTest.java
index cb3ec73..f368dc0 100644
--- a/tests/tests/webkit/src/android/webkit/cts/CookieManagerTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/CookieManagerTest.java
@@ -36,8 +36,9 @@
 
     private static final int TEST_TIMEOUT = 5000;
 
-    private WebViewOnUiThread mOnUiThread;
+    private WebView mWebView;
     private CookieManager mCookieManager;
+    private WebViewOnUiThread mOnUiThread;
 
     public CookieManagerTest() {
         super("com.android.cts.stub", CookieSyncManagerStubActivity.class);
@@ -46,9 +47,9 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        WebView webview = getActivity().getWebView();
-        if (webview != null) {
-            mOnUiThread = new WebViewOnUiThread(this, webview);
+        mWebView = getActivity().getWebView();
+        if (mWebView != null) {
+            mOnUiThread = new WebViewOnUiThread(this, mWebView);
 
             mCookieManager = CookieManager.getInstance();
             assertNotNull(mCookieManager);
@@ -310,6 +311,63 @@
         assertFalse(anyDeleted.get());
     }
 
+    public void testThirdPartyCookie() throws Throwable {
+        if (!NullWebViewUtils.isWebViewAvailable()) {
+            return;
+        }
+        CtsTestServer server = null;
+        try {
+            // In theory we need two servers to test this, one server ('the first party')
+            // which returns a response with a link to a second server ('the third party')
+            // at different origin. This second server attempts to set a cookie which should
+            // fail if AcceptThirdPartyCookie() is false.
+            // Strictly according to the letter of RFC6454 it should be possible to set this
+            // situation up with two TestServers on different ports (these count as having
+            // different origins) but Chrome is not strict about this and does not check the
+            // port. Instead we cheat making some of the urls come from localhost and some
+            // from 127.0.0.1 which count (both in theory and pratice) as having different
+            // origins.
+            server = new CtsTestServer(getActivity());
+
+            // Turn on Javascript (otherwise <script> aren't fetched spoiling the test).
+            mOnUiThread.getSettings().setJavaScriptEnabled(true);
+
+            // Turn global allow on.
+            mCookieManager.setAcceptCookie(true);
+            assertTrue(mCookieManager.acceptCookie());
+
+            // When third party cookies are disabled...
+            mOnUiThread.setAcceptThirdPartyCookies(false);
+            assertFalse(mOnUiThread.acceptThirdPartyCookies());
+
+            // ...we can't set third party cookies.
+            // First on the third party server we get a url which tries to set a cookie.
+            String cookieUrl = toThirdPartyUrl(
+                    server.getSetCookieUrl("cookie_1.js", "test1", "value1"));
+            // Then we create a url on the first party server which links to the first url.
+            String url = server.getLinkedScriptUrl("/content_1.html", cookieUrl);
+            mOnUiThread.loadUrlAndWaitForCompletion(url);
+            assertNull(mCookieManager.getCookie(cookieUrl));
+
+            // When third party cookies are enabled...
+            mOnUiThread.setAcceptThirdPartyCookies(true);
+            assertTrue(mOnUiThread.acceptThirdPartyCookies());
+
+            // ...we can set third party cookies.
+            cookieUrl = toThirdPartyUrl(
+                    server.getSetCookieUrl("/cookie_2.js", "test2", "value2"));
+            url = server.getLinkedScriptUrl("/content_2.html", cookieUrl);
+            mOnUiThread.loadUrlAndWaitForCompletion(url);
+            waitForCookie(cookieUrl);
+            String cookie = mCookieManager.getCookie(cookieUrl);
+            assertNotNull(cookie);
+            assertTrue(cookie.contains("test2"));
+        } finally {
+            if (server != null) server.shutdown();
+            mOnUiThread.getSettings().setJavaScriptEnabled(false);
+        }
+    }
+
     public void testb3167208() throws Exception {
         if (!NullWebViewUtils.isWebViewAvailable()) {
             return;
@@ -379,4 +437,13 @@
             fail("Unexpected error while running on UI thread: " + t.getMessage());
         }
     }
-}
+
+    /**
+     * Makes a url look as if it comes from a different host.
+     * @param url the url to fake.
+     * @return the resulting url after faking.
+     */
+    public String toThirdPartyUrl(String url) {
+        return url.replace("localhost", "127.0.0.1");
+    }
+ }