// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/itunes_urls/model/itunes_urls_handler_tab_helper.h"
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
#import <vector>
#import "base/check.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/string_split.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/shared/public/commands/web_content_commands.h"
#import "ios/web/public/browser_state.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/navigation/web_state_policy_decider.h"
#import "net/base/apple/url_conversions.h"
#import "net/base/url_util.h"
#import "url/gurl.h"
namespace {
// The hosts for iTunes appstore URLs (Legacy itunes link maker links).
const char kLegacyITunesUrlHost[] = "itunes.apple.com";
const char kLegacyITunesGlobalUrlHost[] = "geo.itunes.apple.com";
// The host for iOS applications URLs generated by itunes link maker.
const char kAppUrlHost[] = "apps.apple.com";
const char kITunesProductIdPrefix[] = "id";
const char kITunesAppPathIdentifier[] = "app";
const size_t kITunesUrlPathMinComponentsCount = 2;
const size_t kITunesUrlRegionComponentDefaultIndex = 0;
const size_t kITunesUrlMediaTypeComponentDefaultIndex = 1;
// Returns true, it the given `url` is iTunes product URL.
// iTunes URL should start with apple host and has product id.
bool IsITunesProductUrl(const GURL& url) {
if (!url.SchemeIsHTTPOrHTTPS() ||
!(strcmp(url.host().c_str(), kLegacyITunesUrlHost) == 0 ||
strcmp(url.host().c_str(), kLegacyITunesGlobalUrlHost) == 0 ||
strcmp(url.host().c_str(), kAppUrlHost) == 0))
return false;
std::string file_name = url.ExtractFileName();
// The first `kITunesProductIdLength` characters must be
// `kITunesProductIdPrefix`, followed by the app ID.
return base::StartsWith(file_name, kITunesProductIdPrefix);
}
// Extracts iTunes product parameters from the given `url` to be used with the
// StoreKit launcher.
NSDictionary* ExtractITunesProductParameters(const GURL& url) {
NSMutableDictionary<NSString*, NSString*>* params_dictionary =
[[NSMutableDictionary alloc] init];
std::string product_id =
url.ExtractFileName().substr(strlen(kITunesProductIdPrefix));
params_dictionary[SKStoreProductParameterITunesItemIdentifier] =
base::SysUTF8ToNSString(product_id);
for (net::QueryIterator it(url); !it.IsAtEnd(); it.Advance()) {
params_dictionary[base::SysUTF8ToNSString(it.GetKey())] =
base::SysUTF8ToNSString(it.GetValue());
}
return params_dictionary;
}
} // namespace
ITunesUrlsHandlerTabHelper::~ITunesUrlsHandlerTabHelper() = default;
ITunesUrlsHandlerTabHelper::ITunesUrlsHandlerTabHelper(web::WebState* web_state)
: web::WebStatePolicyDecider(web_state) {}
// static
bool ITunesUrlsHandlerTabHelper::CanHandleUrl(const GURL& url) {
if (!IsITunesProductUrl(url))
return false;
// Valid iTunes URL structure:
// HOST/OPTIONAL_REGION_CODE/MEDIA_TYPE/OPTIONAL_MEDIA_NAME/ID?PARAMETERS
// Check the URL media type, to determine if it is supported.
std::vector<std::string> path_components = base::SplitString(
url.path(), "/", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (path_components.size() < kITunesUrlPathMinComponentsCount)
return false;
size_t media_type_index = kITunesUrlMediaTypeComponentDefaultIndex;
DCHECK(media_type_index > 0);
// If there is no region code in the URL then media type has to appear
// earlier in the URL.
if (path_components[kITunesUrlRegionComponentDefaultIndex].size() != 2)
media_type_index--;
return path_components[media_type_index] == kITunesAppPathIdentifier;
}
void ITunesUrlsHandlerTabHelper::ShouldAllowRequest(
NSURLRequest* request,
web::WebStatePolicyDecider::RequestInfo request_info,
web::WebStatePolicyDecider::PolicyDecisionCallback callback) {
// Don't Handle URLS in Off The record mode as this will open StoreKit with
// Users' iTunes account. Also don't Handle navigations in iframe because they
// may be spam, and they will be handled by other policy deciders.
if (web_state()->GetBrowserState()->IsOffTheRecord() ||
!request_info.target_frame_is_main) {
return std::move(callback).Run(
web::WebStatePolicyDecider::PolicyDecision::Allow());
}
GURL request_url = net::GURLWithNSURL(request.URL);
if (!CanHandleUrl(request_url)) {
return std::move(callback).Run(
web::WebStatePolicyDecider::PolicyDecision::Allow());
}
HandleITunesUrl(request_url);
std::move(callback).Run(web::WebStatePolicyDecider::PolicyDecision::Cancel());
}
void ITunesUrlsHandlerTabHelper::SetWebContentsHandler(
id<WebContentCommands> handler) {
web_content_handler_ = handler;
}
#pragma mark - Private
void ITunesUrlsHandlerTabHelper::HandleITunesUrl(const GURL& url) {
if (web_content_handler_) {
base::RecordAction(
base::UserMetricsAction("ITunesLinksHandler_StoreKitLaunched"));
[web_content_handler_
showAppStoreWithParameters:ExtractITunesProductParameters(url)];
}
}
WEB_STATE_USER_DATA_KEY_IMPL(ITunesUrlsHandlerTabHelper)