blob: a56796cbb9a38c5f9494ef06fcbcb70eafdf90e3 [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"
#import "ios/chrome/browser/app_launcher/app_launcher_tab_helper_delegate.h"
#import "ios/chrome/browser/chrome_url_util.h"
#import "ios/chrome/browser/web/app_launcher_abuse_detector.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
linkTapped:(BOOL)linkTapped {
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);
};
// 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;
web_state_.SetNavigationManager(std::make_unique<FakeNavigationManager>());
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,
/*source_url=*/GURL::EmptyGURL(), target_frame_is_main,
has_user_gesture);
return tab_helper_->ShouldAllowRequest([NSURLRequest requestWithURL:url],
request_info);
}
web::TestWebState web_state_;
FakeAppLauncherAbuseDetector* abuse_detector_ = nil;
FakeAppLauncherTabHelperDelegate* delegate_ = nil;
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_EQ(0U, delegate_.countOfAppsLaunched);
}
// Tests that invalid Urls are completely blocked.
TEST_F(AppLauncherTabHelperTest, InvalidUrls) {
EXPECT_FALSE(TestShouldAllowRequest(@"",
/*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_FALSE(TestShouldAllowRequest(@"u2f-x-callback://test",
/*target_frame_is_main=*/true,
/*has_user_gesture=*/false));
EXPECT_EQ(0U, delegate_.countOfAppsLaunched);
}
// 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);
}
} // namespace