Commit 43d515d3 authored by Ella Ge's avatar Ella Ge Committed by Commit Bot

Quality Enforcement on 404 page

This CL adds the TWA quality enforcement on 404 pages.
When the page is in scope(verified origin) is 404, it will show a toast
and run a CustomTabsCallback to notify the client app the issue.

This CL also changes one of the test pages in TrustedWebActivityTest.
The test was 404 because the test file is not included in the
chrome_public_test_apk package in chrome/android/BUILD.gn. So change
to another similar test page.

Bug: 1109609
Change-Id: I2dff19f115a4efc9f42c9acf7663a0b8988bf4dc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2311010
Commit-Queue: Ella Ge <eirage@chromium.org>
Reviewed-by: default avatarPeter Conn <peconn@chromium.org>
Cr-Commit-Position: refs/heads/master@{#792758}
parent d4d1a59f
...@@ -167,6 +167,7 @@ chrome_java_sources = [ ...@@ -167,6 +167,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/browserservices/ManageTrustedWebActivityDataActivity.java", "java/src/org/chromium/chrome/browser/browserservices/ManageTrustedWebActivityDataActivity.java",
"java/src/org/chromium/chrome/browser/browserservices/OriginVerifier.java", "java/src/org/chromium/chrome/browser/browserservices/OriginVerifier.java",
"java/src/org/chromium/chrome/browser/browserservices/PostMessageHandler.java", "java/src/org/chromium/chrome/browser/browserservices/PostMessageHandler.java",
"java/src/org/chromium/chrome/browser/browserservices/QualityEnforcer.java",
"java/src/org/chromium/chrome/browser/browserservices/Relationship.java", "java/src/org/chromium/chrome/browser/browserservices/Relationship.java",
"java/src/org/chromium/chrome/browser/browserservices/SessionDataHolder.java", "java/src/org/chromium/chrome/browser/browserservices/SessionDataHolder.java",
"java/src/org/chromium/chrome/browser/browserservices/SessionHandler.java", "java/src/org/chromium/chrome/browser/browserservices/SessionHandler.java",
......
...@@ -19,6 +19,7 @@ chrome_junit_test_java_sources = [ ...@@ -19,6 +19,7 @@ chrome_junit_test_java_sources = [
"junit/src/org/chromium/chrome/browser/browserservices/ClearDataDialogResultRecorderTest.java", "junit/src/org/chromium/chrome/browser/browserservices/ClearDataDialogResultRecorderTest.java",
"junit/src/org/chromium/chrome/browser/browserservices/ClientAppBroadcastReceiverTest.java", "junit/src/org/chromium/chrome/browser/browserservices/ClientAppBroadcastReceiverTest.java",
"junit/src/org/chromium/chrome/browser/browserservices/ClientAppDataRegisterTest.java", "junit/src/org/chromium/chrome/browser/browserservices/ClientAppDataRegisterTest.java",
"junit/src/org/chromium/chrome/browser/browserservices/QualityEnforcerUnitTest.java",
"junit/src/org/chromium/chrome/browser/browserservices/SessionDataHolderTest.java", "junit/src/org/chromium/chrome/browser/browserservices/SessionDataHolderTest.java",
"junit/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityClientTest.java", "junit/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityClientTest.java",
"junit/src/org/chromium/chrome/browser/browserservices/digitalgoods/DigitalGoodsConverterTest.java", "junit/src/org/chromium/chrome/browser/browserservices/digitalgoods/DigitalGoodsConverterTest.java",
......
...@@ -71,6 +71,7 @@ chrome_test_java_sources = [ ...@@ -71,6 +71,7 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkPersonalizedSigninPromoTest.java", "javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkPersonalizedSigninPromoTest.java",
"javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkTest.java", "javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkTest.java",
"javatests/src/org/chromium/chrome/browser/browserservices/OriginVerifierTest.java", "javatests/src/org/chromium/chrome/browser/browserservices/OriginVerifierTest.java",
"javatests/src/org/chromium/chrome/browser/browserservices/QualityEnforcerTest.java",
"javatests/src/org/chromium/chrome/browser/browserservices/RunningInChromeTest.java", "javatests/src/org/chromium/chrome/browser/browserservices/RunningInChromeTest.java",
"javatests/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityClientLocationDelegationTest.java", "javatests/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityClientLocationDelegationTest.java",
"javatests/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityClientTest.java", "javatests/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityClientTest.java",
......
// Copyright 2020 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.
package org.chromium.chrome.browser.browserservices;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.browser.customtabs.CustomTabsSessionToken;
import org.chromium.base.ContextUtils;
import org.chromium.base.Promise;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browserservices.ui.controller.Verifier;
import org.chromium.chrome.browser.browserservices.ui.controller.trustedwebactivity.ClientPackageNameProvider;
import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
import org.chromium.chrome.browser.customtabs.content.TabObserverRegistrar;
import org.chromium.chrome.browser.customtabs.content.TabObserverRegistrar.CustomTabTabObserver;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.ui.widget.Toast;
import javax.inject.Inject;
/**
* This class enforces a quality bar on the websites shown inside Trusted Web Activities. For
* example triggering if a link from a page on the verified origin 404s.
*
* The current plan for when QualityEnforcer is triggered is to finish Chrome and send a signal
* back to the TWA shell, causing it to crash. The purpose of this is to bring TWAs in line with
* native applications - if a native application tries to start an Activity that doesn't exist, it
* will crash. We should hold web apps to the same standard.
*/
@ActivityScope
public class QualityEnforcer {
@VisibleForTesting
static final String NOTIFY = "quality_enforcement.notify";
private static final String KEY_CRASH_REASON = "crash_reason";
private static final String KEY_SUCCESS = "success";
private final Verifier mVerifier;
private final CustomTabsConnection mConnection;
private final CustomTabsSessionToken mSessionToken;
private final ClientPackageNameProvider mClientPackageNameProvider;
private boolean mOriginVerified;
private final CustomTabTabObserver mTabObserver = new CustomTabTabObserver() {
@Override
public void onDidFinishNavigation(Tab tab, NavigationHandle navigation) {
if (!navigation.hasCommitted() || !navigation.isInMainFrame()
|| navigation.isSameDocument()) {
return;
}
String newUrl = tab.getOriginalUrl();
if (isNavigationInScope(newUrl) && navigation.httpStatusCode() == 404) {
String message = ContextUtils.getApplicationContext().getString(
R.string.twa_quality_enforcement_violation_error,
navigation.httpStatusCode(), newUrl);
trigger(message);
}
}
@Override
public void onObservingDifferentTab(@NonNull Tab tab) {
// On tab switches, update the stored verification state.
isNavigationInScope(tab.getOriginalUrl());
}
};
@Inject
public QualityEnforcer(TabObserverRegistrar tabObserverRegistrar,
BrowserServicesIntentDataProvider intentDataProvider, CustomTabsConnection connection,
Verifier verifier, ClientPackageNameProvider clientPackageNameProvider) {
mVerifier = verifier;
mConnection = connection;
mSessionToken = intentDataProvider.getSession();
mClientPackageNameProvider = clientPackageNameProvider;
// Initialize the value to true before the first navigation.
mOriginVerified = true;
tabObserverRegistrar.registerActivityTabObserver(mTabObserver);
}
private void trigger(String message) {
showErrorToast(message);
Bundle args = new Bundle();
args.putString(KEY_CRASH_REASON, message);
mConnection.sendExtraCallbackWithResult(mSessionToken, NOTIFY, args);
}
private void showErrorToast(String message) {
Context context = ContextUtils.getApplicationContext();
PackageManager pm = context.getPackageManager();
// Only shows the toast when the TWA client app does not have installer info, i.e. install
// via adb instead of a store.
if (pm.getInstallerPackageName(mClientPackageNameProvider.get()) == null) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
}
}
/*
* Updates whether the current url is verified and returns whether the source and destination
* are both on the verified origin.
*/
private boolean isNavigationInScope(String newUrl) {
if (newUrl.equals("")) return false;
boolean wasVerified = mOriginVerified;
Promise<Boolean> result = mVerifier.verify(newUrl);
mOriginVerified = !result.isFulfilled() || result.getResult();
return wasVerified && mOriginVerified;
}
}
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package org.chromium.chrome.browser.browserservices.ui.trustedwebactivity; package org.chromium.chrome.browser.browserservices.ui.trustedwebactivity;
import org.chromium.chrome.browser.browserservices.BrowserServicesIntentDataProvider; import org.chromium.chrome.browser.browserservices.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.QualityEnforcer;
import org.chromium.chrome.browser.browserservices.TrustedWebActivityUmaRecorder; import org.chromium.chrome.browser.browserservices.TrustedWebActivityUmaRecorder;
import org.chromium.chrome.browser.browserservices.ui.SharedActivityCoordinator; import org.chromium.chrome.browser.browserservices.ui.SharedActivityCoordinator;
import org.chromium.chrome.browser.browserservices.ui.controller.CurrentPageVerifier; import org.chromium.chrome.browser.browserservices.ui.controller.CurrentPageVerifier;
...@@ -46,7 +47,7 @@ public class TrustedWebActivityCoordinator { ...@@ -46,7 +47,7 @@ public class TrustedWebActivityCoordinator {
TrustedWebActivityUmaRecorder umaRecorder, TrustedWebActivityUmaRecorder umaRecorder,
ActivityLifecycleDispatcher lifecycleDispatcher, TwaRegistrar twaRegistrar, ActivityLifecycleDispatcher lifecycleDispatcher, TwaRegistrar twaRegistrar,
ClientPackageNameProvider clientPackageNameProvider, ClientPackageNameProvider clientPackageNameProvider,
CustomTabsConnection customTabsConnection) { CustomTabsConnection customTabsConnection, QualityEnforcer enforcer) {
// We don't need to do anything with most of the classes above, we just need to resolve them // We don't need to do anything with most of the classes above, we just need to resolve them
// so they start working. // so they start working.
mSharedActivityCoordinator = sharedActivityCoordinator; mSharedActivityCoordinator = sharedActivityCoordinator;
......
// Copyright 2020 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.
package org.chromium.chrome.browser.browserservices;
import static org.chromium.chrome.browser.browserservices.TrustedWebActivityTestUtil.createSession;
import static org.chromium.chrome.browser.browserservices.TrustedWebActivityTestUtil.spoofVerification;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.test.InstrumentationRegistry;
import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsSession;
import androidx.browser.customtabs.TrustedWebUtils;
import androidx.test.filters.MediumTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
import org.chromium.base.ContextUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.browser.customtabs.CustomTabActivityTestRule;
import org.chromium.chrome.browser.customtabs.CustomTabsTestUtils;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.net.test.EmbeddedTestServerRule;
import java.util.concurrent.TimeoutException;
/**
* Tests for {@link QualityEnforcer}
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class QualityEnforcerTest {
private static final String TEST_PAGE = "/chrome/test/data/android/google.html";
// A not exist test page to triger 404.
private static final String TEST_PAGE_404 = "/chrome/test/data/android/404.html";
private static final String PACKAGE_NAME =
ContextUtils.getApplicationContext().getPackageName();
private final CustomTabActivityTestRule mCustomTabActivityTestRule =
new CustomTabActivityTestRule();
private final EmbeddedTestServerRule mEmbeddedTestServerRule = new EmbeddedTestServerRule();
@Rule
public RuleChain mRuleChain = RuleChain.emptyRuleChain()
.around(mCustomTabActivityTestRule)
.around(mEmbeddedTestServerRule);
private String mTestPage;
private String mTestPage404;
CallbackHelper mCallbackHelper = new CallbackHelper();
CustomTabsCallback mCallback = new CustomTabsCallback() {
@Override
public Bundle extraCallbackWithResult(String callbackName, Bundle args) {
if (callbackName.equals(QualityEnforcer.NOTIFY)) {
mCallbackHelper.notifyCalled();
}
return Bundle.EMPTY;
}
};
@Before
public void setUp() {
// Native needs to be initialized to start the test server.
LibraryLoader.getInstance().ensureInitialized();
mEmbeddedTestServerRule.setServerUsesHttps(true); // TWAs only work with HTTPS.
mTestPage = mEmbeddedTestServerRule.getServer().getURL(TEST_PAGE);
mTestPage404 = mEmbeddedTestServerRule.getServer().getURL(TEST_PAGE_404);
}
@Test
@MediumTest
public void notifiedWhenLaunch404() throws TimeoutException {
launch(mTestPage404);
mCallbackHelper.waitForFirst();
}
@Test
@MediumTest
public void notifiedWhenNavigate404() throws TimeoutException {
launch(mTestPage);
mCustomTabActivityTestRule.loadUrl(mTestPage404);
mCallbackHelper.waitForFirst();
}
public void launch(String testPage) throws TimeoutException {
CustomTabsSession session = CustomTabsTestUtils.bindWithCallback(mCallback).session;
Intent intent = new CustomTabsIntent.Builder(session).build().intent;
intent.setComponent(new ComponentName(
InstrumentationRegistry.getTargetContext(), ChromeLauncherActivity.class));
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(testPage));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true);
spoofVerification(PACKAGE_NAME, testPage);
createSession(intent, PACKAGE_NAME);
mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
}
}
...@@ -160,7 +160,7 @@ public class TrustedWebActivityTest { ...@@ -160,7 +160,7 @@ public class TrustedWebActivityTest {
final String pageWithThemeColor = mEmbeddedTestServerRule.getServer().getURL( final String pageWithThemeColor = mEmbeddedTestServerRule.getServer().getURL(
"/chrome/test/data/android/theme_color_test.html"); "/chrome/test/data/android/theme_color_test.html");
final String pageWithoutThemeColor = final String pageWithoutThemeColor =
mEmbeddedTestServerRule.getServer().getURL("/chrome/test/data/simple.html"); mEmbeddedTestServerRule.getServer().getURL("/chrome/test/data/android/about.html");
// Navigate to page with a theme color so that we can later wait for the status bar color to // Navigate to page with a theme color so that we can later wait for the status bar color to
// change back to the intent color. // change back to the intent color.
......
// Copyright 2020 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.
package org.chromium.chrome.browser.browserservices;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.shadows.ShadowToast;
import org.chromium.base.ContextUtils;
import org.chromium.base.Promise;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browserservices.ui.controller.Verifier;
import org.chromium.chrome.browser.browserservices.ui.controller.trustedwebactivity.ClientPackageNameProvider;
import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider;
import org.chromium.chrome.browser.customtabs.CustomTabsConnection;
import org.chromium.chrome.browser.customtabs.content.TabObserverRegistrar;
import org.chromium.chrome.browser.customtabs.content.TabObserverRegistrar.CustomTabTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.test.util.browser.Features;
import org.chromium.content_public.browser.NavigationHandle;
/**
* Tests for {@link QualityEnforcer}.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class QualityEnforcerUnitTest {
private static final String TRUSTED_ORIGIN_PAGE = "https://www.origin1.com/page1";
private static final String UNTRUSTED_PAGE = "https://www.origin2.com/page1";
private static final int HTTP_STATUS_SUCCESS = 200;
private static final int HTTP_ERROR_NOT_FOUND = 404;
@Rule
public TestRule mFeaturesProcessor = new Features.JUnitProcessor();
@Mock
private CustomTabIntentDataProvider mIntentDataProvider;
@Mock
private CustomTabsConnection mCustomTabsConnection;
@Mock
private Verifier mVerifier;
@Mock
private ClientPackageNameProvider mClientPackageNameProvider;
@Mock
private TabObserverRegistrar mTabObserverRegistrar;
@Captor
private ArgumentCaptor<CustomTabTabObserver> mTabObserverCaptor;
@Mock
private Tab mTab;
private ShadowPackageManager mShadowPackageManager;
private QualityEnforcer mQualityEnforcer;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
doNothing()
.when(mTabObserverRegistrar)
.registerActivityTabObserver(mTabObserverCaptor.capture());
when(mVerifier.verify(TRUSTED_ORIGIN_PAGE)).thenReturn(Promise.fulfilled(true));
when(mVerifier.verify(UNTRUSTED_PAGE)).thenReturn(Promise.fulfilled(false));
mQualityEnforcer = new QualityEnforcer(mTabObserverRegistrar, mIntentDataProvider,
mCustomTabsConnection, mVerifier, mClientPackageNameProvider);
}
@Test
public void trigger_navigateTo404() {
navigateToUrl(TRUSTED_ORIGIN_PAGE, HTTP_ERROR_NOT_FOUND);
verifyTriggered404();
}
@Test
public void notTrigger_navigationSuccess() {
navigateToUrl(TRUSTED_ORIGIN_PAGE, HTTP_STATUS_SUCCESS);
verifyNotTriggered();
}
@Test
public void notTrigger_navigateTo404NotVerifiedSite() {
navigateToUrl(UNTRUSTED_PAGE, HTTP_ERROR_NOT_FOUND);
verifyNotTriggered();
}
@Test
public void notTrigger_navigateFromNotVerifiedToVerified404() {
navigateToUrl(UNTRUSTED_PAGE, HTTP_STATUS_SUCCESS);
navigateToUrl(TRUSTED_ORIGIN_PAGE, HTTP_ERROR_NOT_FOUND);
verifyNotTriggered();
}
@Test
public void trigger_notVerifiedToVerifiedThen404() {
navigateToUrl(UNTRUSTED_PAGE, HTTP_STATUS_SUCCESS);
navigateToUrl(TRUSTED_ORIGIN_PAGE, HTTP_STATUS_SUCCESS);
navigateToUrl(TRUSTED_ORIGIN_PAGE, HTTP_ERROR_NOT_FOUND);
verifyTriggered404();
}
private void verifyTriggered404() {
Assert.assertEquals(ContextUtils.getApplicationContext().getString(
R.string.twa_quality_enforcement_violation_error,
HTTP_ERROR_NOT_FOUND, TRUSTED_ORIGIN_PAGE),
ShadowToast.getTextOfLatestToast());
verify(mCustomTabsConnection)
.sendExtraCallbackWithResult(any(), eq(QualityEnforcer.NOTIFY), any());
}
private void verifyNotTriggered() {
verify(mCustomTabsConnection, never())
.sendExtraCallbackWithResult(any(), eq(QualityEnforcer.NOTIFY), any());
}
private void navigateToUrl(String url, int httpStatusCode) {
when(mTab.getOriginalUrl()).thenReturn(url);
NavigationHandle navigation =
new NavigationHandle(0 /* navigationHandleProxy */, url, true /* isMainFrame */,
false /* isSameDocument */, false /* isRendererInitiated */);
navigation.didFinish(url, false /* isErrorPage */, true /* hasCommitted */,
false /* isFragmentNavigation */, false /* isDownload */,
false /* isValidSearchFormUrl */, 0 /* pageTransition */, 0 /* errorCode*/,
httpStatusCode);
for (CustomTabTabObserver tabObserver : mTabObserverCaptor.getAllValues()) {
tabObserver.onDidFinishNavigation(mTab, navigation);
}
}
}
...@@ -3278,6 +3278,9 @@ To change this setting, <ph name="BEGIN_LINK">&lt;resetlink&gt;</ph>reset sync<p ...@@ -3278,6 +3278,9 @@ To change this setting, <ph name="BEGIN_LINK">&lt;resetlink&gt;</ph>reset sync<p
<message name="IDS_TWA_CLEAR_DATA_SITE_SELECTION_TITLE" desc="Title of screen showing the sites linked to TWA, allowing the user to clean data in any of them."> <message name="IDS_TWA_CLEAR_DATA_SITE_SELECTION_TITLE" desc="Title of screen showing the sites linked to TWA, allowing the user to clean data in any of them.">
Linked sites Linked sites
</message> </message>
<message name="IDS_TWA_QUALITY_ENFORCEMENT_VIOLATION_ERROR" desc="Text shown on a toast when TWA violation.">
TWA error: <ph name="ERROR_CODE">%1$s<ex>404</ex></ph> on <ph name="VIOLATED_URL">%2$s<ex>https://google.com/</ex></ph>
</message>
<message name="IDS_WEBAPP_TAP_TO_COPY_URL" desc="Message on the notification that indicates that taping it will copy a Web App's URL into the clipboard."> <message name="IDS_WEBAPP_TAP_TO_COPY_URL" desc="Message on the notification that indicates that taping it will copy a Web App's URL into the clipboard.">
Tap to copy the URL for this app Tap to copy the URL for this app
......
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