blob: d9f7f89cd05ad65baa6cd3d8dc3d6a9263899fbc [file] [log] [blame]
// Copyright 2018 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/chrome/browser/app_launcher/app_launcher_tab_helper.h"
#include <memory>
#include "base/compiler_specific.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_task_environment.h"
#include "base/time/default_clock.h"
#include "components/reading_list/core/reading_list_entry.h"
#include "components/reading_list/core/reading_list_model_impl.h"
#import "ios/chrome/browser/app_launcher/app_launcher_abuse_detector.h"
#include "ios/chrome/browser/app_launcher/app_launcher_flags.h"
#import "ios/chrome/browser/app_launcher/app_launcher_tab_helper_delegate.h"
#include "ios/chrome/browser/browser_state/test_chrome_browser_state.h"
#import "ios/chrome/browser/chrome_url_util.h"
#include "ios/chrome/browser/reading_list/reading_list_model_factory.h"
#import "ios/chrome/browser/tabs/legacy_tab_helper.h"
#import "ios/chrome/browser/web/tab_id_tab_helper.h"
#import "ios/web/public/test/fakes/test_navigation_manager.h"
#import "ios/web/public/test/fakes/test_web_state.h"
#include "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#include "testing/platform_test.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
// An object that conforms to AppLauncherTabHelperDelegate for testing.
@interface FakeAppLauncherTabHelperDelegate
: NSObject<AppLauncherTabHelperDelegate>
// URL of the last launched application.
@property(nonatomic, assign) GURL lastLaunchedAppURL;
// Number of times an app was launched.
@property(nonatomic, assign) NSUInteger countOfAppsLaunched;
// Number of times the repeated launches alert has been shown.
@property(nonatomic, assign) NSUInteger countOfAlertsShown;
// Simulates the user tapping the accept button when prompted via
// |-appLauncherTabHelper:showAlertOfRepeatedLaunchesWithCompletionHandler|.
@property(nonatomic, assign) BOOL simulateUserAcceptingPrompt;
@end
@implementation FakeAppLauncherTabHelperDelegate
@synthesize lastLaunchedAppURL = _lastLaunchedAppURL;
@synthesize countOfAppsLaunched = _countOfAppsLaunched;
@synthesize countOfAlertsShown = _countOfAlertsShown;
@synthesize simulateUserAcceptingPrompt = _simulateUserAcceptingPrompt;
- (BOOL)appLauncherTabHelper:(AppLauncherTabHelper*)tabHelper
launchAppWithURL:(const GURL&)URL
linkTransition:(BOOL)linkTransition {
self.countOfAppsLaunched++;
self.lastLaunchedAppURL = URL;
return YES;
}
- (void)appLauncherTabHelper:(AppLauncherTabHelper*)tabHelper
showAlertOfRepeatedLaunchesWithCompletionHandler:
(ProceduralBlockWithBool)completionHandler {
self.countOfAlertsShown++;
completionHandler(self.simulateUserAcceptingPrompt);
}
@end
// An AppLauncherAbuseDetector for testing.
@interface FakeAppLauncherAbuseDetector : AppLauncherAbuseDetector
@property(nonatomic, assign) ExternalAppLaunchPolicy policy;
@end
@implementation FakeAppLauncherAbuseDetector
@synthesize policy = _policy;
- (ExternalAppLaunchPolicy)launchPolicyForURL:(const GURL&)URL
fromSourcePageURL:(const GURL&)sourcePageURL {
return self.policy;
}
@end
namespace {
// A fake NavigationManager to be used by the WebState object for the
// AppLauncher.
class FakeNavigationManager : public web::TestNavigationManager {
public:
FakeNavigationManager() = default;
// web::NavigationManager implementation.
void DiscardNonCommittedItems() override {}
DISALLOW_COPY_AND_ASSIGN(FakeNavigationManager);
};
std::unique_ptr<KeyedService> BuildReadingListModel(
web::BrowserState* context) {
ios::ChromeBrowserState* browser_state =
ios::ChromeBrowserState::FromBrowserState(context);
std::unique_ptr<ReadingListModelImpl> reading_list_model(
new ReadingListModelImpl(nullptr, browser_state->GetPrefs(),
base::DefaultClock::GetInstance()));
return reading_list_model;
}
// Test fixture for AppLauncherTabHelper class.
class AppLauncherTabHelperTest : public PlatformTest {
protected:
AppLauncherTabHelperTest()
: abuse_detector_([[FakeAppLauncherAbuseDetector alloc] init]),
delegate_([[FakeAppLauncherTabHelperDelegate alloc] init]) {
AppLauncherTabHelper::CreateForWebState(&web_state_, abuse_detector_,
delegate_);
// Allow is the default policy for this test.
abuse_detector_.policy = ExternalAppLaunchPolicyAllow;
auto navigation_manager = std::make_unique<FakeNavigationManager>();
navigation_manager_ = navigation_manager.get();
web_state_.SetNavigationManager(std::move(navigation_manager));
web_state_.SetCurrentURL(GURL("https://chromium.org"));
tab_helper_ = AppLauncherTabHelper::FromWebState(&web_state_);
}
bool TestShouldAllowRequest(NSString* url_string,
bool target_frame_is_main,
bool has_user_gesture) WARN_UNUSED_RESULT {
NSURL* url = [NSURL URLWithString:url_string];
web::WebStatePolicyDecider::RequestInfo request_info(
ui::PageTransition::PAGE_TRANSITION_LINK, target_frame_is_main,
has_user_gesture);
return tab_helper_->ShouldAllowRequest([NSURLRequest requestWithURL:url],
request_info);
}
// Initialize reading list model and its required tab helpers.
void InitializeReadingListModel() {
TestChromeBrowserState::Builder test_cbs_builder;
chrome_browser_state_ = test_cbs_builder.Build();
web_state_.SetBrowserState(chrome_browser_state_.get());
ReadingListModelFactory::GetInstance()->SetTestingFactoryAndUse(
chrome_browser_state_.get(),
base::BindRepeating(&BuildReadingListModel));
TabIdTabHelper::CreateForWebState(&web_state_);
LegacyTabHelper::CreateForWebState(&web_state_);
is_reading_list_initialized_ = true;
}
// Returns true if the |expected_read_status| matches the read status for any
// non empty source URL based on the transition type and the app policy.
bool TestReadingListUpdate(bool is_app_blocked,
bool is_link_transition,
bool expected_read_status) {
// Make sure reading list model is initialized.
if (!is_reading_list_initialized_)
InitializeReadingListModel();
web_state_.SetCurrentURL(GURL("https://chromium.org"));
GURL pending_url("http://google.com");
navigation_manager_->AddItem(pending_url, ui::PAGE_TRANSITION_LINK);
web::NavigationItem* item = navigation_manager_->GetItemAtIndex(0);
navigation_manager_->SetPendingItem(item);
item->SetOriginalRequestURL(pending_url);
ReadingListModel* model = ReadingListModelFactory::GetForBrowserState(
chrome_browser_state_.get());
EXPECT_TRUE(model->DeleteAllEntries());
model->AddEntry(pending_url, "unread", reading_list::ADDED_VIA_CURRENT_APP);
abuse_detector_.policy = is_app_blocked ? ExternalAppLaunchPolicyBlock
: ExternalAppLaunchPolicyAllow;
ui::PageTransition transition_type =
is_link_transition
? ui::PageTransition::PAGE_TRANSITION_LINK
: ui::PageTransition::PAGE_TRANSITION_CLIENT_REDIRECT;
NSURL* url = [NSURL
URLWithString:@"itms-apps://itunes.apple.com/us/app/appname/id123"];
web::WebStatePolicyDecider::RequestInfo request_info(
transition_type,
/*target_frame_is_main=*/true, /*has_user_gesture=*/true);
EXPECT_FALSE(tab_helper_->ShouldAllowRequest(
[NSURLRequest requestWithURL:url], request_info));
const ReadingListEntry* entry = model->GetEntryByURL(pending_url);
return entry->IsRead() == expected_read_status;
}
base::test::ScopedTaskEnvironment scoped_task_environment;
web::TestWebState web_state_;
FakeNavigationManager* navigation_manager_ = nullptr;
std::unique_ptr<TestChromeBrowserState> chrome_browser_state_ = nil;
FakeAppLauncherAbuseDetector* abuse_detector_ = nil;
FakeAppLauncherTabHelperDelegate* delegate_ = nil;
bool is_reading_list_initialized_ = false;
AppLauncherTabHelper* tab_helper_;
};
// Tests that a valid URL launches app.
TEST_F(AppLauncherTabHelperTest, AbuseDetectorPolicyAllowedForValidUrl) {
abuse_detector_.policy = ExternalAppLaunchPolicyAllow;
EXPECT_FALSE(TestShouldAllowRequest(@"valid://1234",
/*target_frame_is_main=*/true,
/*has_user_gesture=*/false));
EXPECT_EQ(1U, delegate_.countOfAppsLaunched);
EXPECT_EQ(GURL("valid://1234"), delegate_.lastLaunchedAppURL);
}
// Tests that a valid URL does not launch app when launch policy is to block.
TEST_F(AppLauncherTabHelperTest, AbuseDetectorPolicyBlockedForValidUrl) {
abuse_detector_.policy = ExternalAppLaunchPolicyBlock;
EXPECT_FALSE(TestShouldAllowRequest(@"valid://1234",
/*target_frame_is_main=*/true,
/*has_user_gesture=*/false));
EXPECT_EQ(0U, delegate_.countOfAlertsShown);
EXPECT_EQ(0U, delegate_.countOfAppsLaunched);
}
// Tests that a valid URL shows an alert and launches app when launch policy is
// to prompt and user accepts.
TEST_F(AppLauncherTabHelperTest, ValidUrlPromptUserAccepts) {
abuse_detector_.policy = ExternalAppLaunchPolicyPrompt;
delegate_.simulateUserAcceptingPrompt = YES;
EXPECT_FALSE(TestShouldAllowRequest(@"valid://1234",
/*target_frame_is_main=*/true,
/*has_user_gesture=*/false));
EXPECT_EQ(1U, delegate_.countOfAlertsShown);
EXPECT_EQ(1U, delegate_.countOfAppsLaunched);
EXPECT_EQ(GURL("valid://1234"), delegate_.lastLaunchedAppURL);
}
// Tests that a valid URL does not launch app when launch policy is to prompt
// and user rejects.
TEST_F(AppLauncherTabHelperTest, ValidUrlPromptUserRejects) {
abuse_detector_.policy = ExternalAppLaunchPolicyPrompt;
delegate_.simulateUserAcceptingPrompt = NO;
EXPECT_FALSE(TestShouldAllowRequest(@"valid://1234",
/*target_frame_is_main=*/true,
/*has_user_gesture=*/false));
EXPECT_EQ(0U, delegate_.countOfAppsLaunched);
}
// Tests that ShouldAllowRequest only launches apps for App Urls in main frame,
// or iframe when there was a recent user interaction.
TEST_F(AppLauncherTabHelperTest, ShouldAllowRequestWithAppUrl) {
NSString* url_string = @"itms-apps://itunes.apple.com/us/app/appname/id123";
EXPECT_FALSE(TestShouldAllowRequest(url_string, /*target_frame_is_main=*/true,
/*has_user_gesture=*/false));
EXPECT_EQ(1U, delegate_.countOfAppsLaunched);
EXPECT_FALSE(TestShouldAllowRequest(url_string, /*target_frame_is_main=*/true,
/*has_user_gesture=*/true));
EXPECT_EQ(2U, delegate_.countOfAppsLaunched);
EXPECT_FALSE(TestShouldAllowRequest(url_string,
/*target_frame_is_main=*/false,
/*has_user_gesture=*/false));
EXPECT_EQ(2U, delegate_.countOfAppsLaunched);
EXPECT_FALSE(TestShouldAllowRequest(url_string,
/*target_frame_is_main=*/false,
/*has_user_gesture=*/true));
EXPECT_EQ(3U, delegate_.countOfAppsLaunched);
}
// Tests that ShouldAllowRequest always allows requests and does not launch
// apps for non App Urls.
TEST_F(AppLauncherTabHelperTest, ShouldAllowRequestWithNonAppUrl) {
EXPECT_TRUE(TestShouldAllowRequest(
@"http://itunes.apple.com/us/app/appname/id123",
/*target_frame_is_main=*/true, /*has_user_gesture=*/false));
EXPECT_TRUE(TestShouldAllowRequest(@"file://a/b/c",
/*target_frame_is_main=*/true,
/*has_user_gesture=*/true));
EXPECT_TRUE(TestShouldAllowRequest(@"about://test",
/*target_frame_is_main=*/false,
/*has_user_gesture=*/false));
EXPECT_TRUE(TestShouldAllowRequest(@"data://test",
/*target_frame_is_main=*/false,
/*has_user_gesture=*/true));
EXPECT_TRUE(TestShouldAllowRequest(@"blob://test",
/*target_frame_is_main=*/false,
/*has_user_gesture=*/true));
EXPECT_EQ(0U, delegate_.countOfAppsLaunched);
}
// Tests that invalid Urls are completely blocked.
TEST_F(AppLauncherTabHelperTest, InvalidUrls) {
EXPECT_FALSE(TestShouldAllowRequest(/*url_string=*/@"",
/*target_frame_is_main=*/true,
/*has_user_gesture=*/false));
EXPECT_FALSE(TestShouldAllowRequest(@"invalid",
/*target_frame_is_main=*/true,
/*has_user_gesture=*/false));
EXPECT_EQ(0U, delegate_.countOfAppsLaunched);
}
// Tests that URLs with schemes that might be a security risk are blocked.
TEST_F(AppLauncherTabHelperTest, InsecureUrls) {
EXPECT_FALSE(TestShouldAllowRequest(@"app-settings://",
/*target_frame_is_main=*/true,
/*has_user_gesture=*/false));
EXPECT_EQ(0U, delegate_.countOfAppsLaunched);
}
// Tests that URLs with U2F schemes are handled correctly.
// This test is using https://chromeiostesting-dot-u2fdemo.appspot.com URL which
// is a whitelisted URL for the purpose of testing, but the test doesn't send
// any request to the server.
TEST_F(AppLauncherTabHelperTest, U2FUrls) {
// Add required tab helpers for the U2F check.
TabIdTabHelper::CreateForWebState(&web_state_);
LegacyTabHelper::CreateForWebState(&web_state_);
std::unique_ptr<web::NavigationItem> item = web::NavigationItem::Create();
// "u2f-x-callback" scheme should only be created by the browser. External
// URLs with that scheme should be blocked to prevent malicious sites from
// bypassing the browser origin/security check for u2f schemes.
item->SetURL(GURL("https://chromeiostesting-dot-u2fdemo.appspot.com"));
navigation_manager_->SetLastCommittedItem(item.get());
EXPECT_FALSE(TestShouldAllowRequest(@"u2f-x-callback://chromium.test",
/*target_frame_is_main=*/true,
/*has_user_gesture=*/false));
EXPECT_EQ(0U, delegate_.countOfAppsLaunched);
// Source URL is not trusted, so u2f scheme should not be allowed.
item->SetURL(GURL("https://chromium.test"));
navigation_manager_->SetLastCommittedItem(item.get());
EXPECT_FALSE(TestShouldAllowRequest(@"u2f://chromium.test",
/*target_frame_is_main=*/true,
/*has_user_gesture=*/false));
EXPECT_EQ(0U, delegate_.countOfAppsLaunched);
// Source URL is trusted, so u2f scheme should be allowed and an external app
// is launched via URL with u2f-x-callback scheme.
item->SetURL(GURL("https://chromeiostesting-dot-u2fdemo.appspot.com"));
navigation_manager_->SetLastCommittedItem(item.get());
EXPECT_FALSE(TestShouldAllowRequest(@"u2f://chromium.test",
/*target_frame_is_main=*/true,
/*has_user_gesture=*/false));
EXPECT_EQ(1U, delegate_.countOfAppsLaunched);
EXPECT_TRUE(delegate_.lastLaunchedAppURL.SchemeIs("u2f-x-callback"));
}
// Tests that URLs with Chrome Bundle schemes are blocked on iframes.
TEST_F(AppLauncherTabHelperTest, ChromeBundleUrlScheme) {
// Get the test bundle URL Scheme.
NSString* scheme = [[ChromeAppConstants sharedInstance] getBundleURLScheme];
NSString* url = [NSString stringWithFormat:@"%@://www.google.com", scheme];
EXPECT_FALSE(TestShouldAllowRequest(url,
/*target_frame_is_main=*/false,
/*has_user_gesture=*/false));
EXPECT_EQ(0U, delegate_.countOfAppsLaunched);
EXPECT_FALSE(TestShouldAllowRequest(url,
/*target_frame_is_main=*/false,
/*has_user_gesture=*/true));
EXPECT_EQ(0U, delegate_.countOfAppsLaunched);
// Chrome Bundle URL scheme is only allowed from main frames.
EXPECT_FALSE(TestShouldAllowRequest(url,
/*target_frame_is_main=*/true,
/*has_user_gesture=*/false));
EXPECT_EQ(1U, delegate_.countOfAppsLaunched);
}
// Tests that ShouldAllowRequest updates the reading list correctly, when there
// is a valid app URL to be launches successfully.
// TODO(crbug.com/850760): Remove this test, once the new AppLauncherRefresh
// logic is always enabled.
TEST_F(AppLauncherTabHelperTest, UpdatingTheReadingList) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndDisableFeature(kAppLauncherRefresh);
// Reading list isn't expected to be updated if there was no app launch.
EXPECT_TRUE(TestReadingListUpdate(/*is_app_blocked=*/true,
/*is_link_transition*/ false,
/*expected_read_status*/ false));
EXPECT_EQ(0U, delegate_.countOfAppsLaunched);
// Reading list to be updated when app launch is successful.
EXPECT_TRUE(TestReadingListUpdate(/*is_app_blocked=*/false,
/*is_link_transition*/ false,
/*expected_read_status*/ true));
EXPECT_EQ(1U, delegate_.countOfAppsLaunched);
// Transition type doesn't affect the reading list status
EXPECT_TRUE(TestReadingListUpdate(/*is_app_blocked=*/false,
/*is_link_transition*/ true,
/*expected_read_status*/ true));
EXPECT_EQ(2U, delegate_.countOfAppsLaunched);
}
// Tests that ShouldAllowRequest updates the reading list correctly for non-link
// transitions regardless of the app launching success when AppLauncherRefresh
// flag is enabled.
TEST_F(AppLauncherTabHelperTest, UpdatingTheReadingListWithAppLauncherRefresh) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(kAppLauncherRefresh);
// Update reading list if the transition is not a link transition.
EXPECT_TRUE(TestReadingListUpdate(/*is_app_blocked=*/true,
/*is_link_transition*/ false,
/*expected_read_status*/ true));
EXPECT_EQ(0U, delegate_.countOfAppsLaunched);
EXPECT_TRUE(TestReadingListUpdate(/*is_app_blocked=*/false,
/*is_link_transition*/ false,
/*expected_read_status*/ true));
EXPECT_EQ(1U, delegate_.countOfAppsLaunched);
// Don't update reading list if the transition is a link transition.
EXPECT_TRUE(TestReadingListUpdate(/*is_app_blocked=*/true,
/*is_link_transition*/ true,
/*expected_read_status*/ false));
EXPECT_EQ(1U, delegate_.countOfAppsLaunched);
EXPECT_TRUE(TestReadingListUpdate(/*is_app_blocked=*/false,
/*is_link_transition*/ true,
/*expected_read_status*/ false));
EXPECT_EQ(2U, delegate_.countOfAppsLaunched);
}
} // namespace