blob: 34f92160ac08663755dd93b6941632a60b35dc37 [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/itunes_urls/itunes_urls_handler_tab_helper.h"
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
#include <vector>
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/store_kit/store_kit_tab_helper.h"
#include "ios/web/public/browser_state.h"
#import "ios/web/public/navigation_item.h"
#import "ios/web/public/navigation_manager.h"
#import "ios/web/public/web_state/web_state_policy_decider.h"
#include "net/base/filename_util.h"
#import "net/base/mac/url_conversions.h"
#include "net/base/url_util.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
DEFINE_WEB_STATE_USER_DATA_KEY(ITunesUrlsHandlerTabHelper);
namespace {
// The domain for iTunes appstore URLs.
const char kITunesUrlDomain[] = "itunes.apple.com";
const char kITunesProductIdPrefix[] = "id";
const char kITunesAppPathIdentifier[] = "app";
const size_t kITunesUrlPathMinComponentsCount = 4;
const size_t kITunesUrlRegionComponentDefaultIndex = 1;
const size_t kITunesUrlMediaTypeComponentDefaultIndex = 2;
// Records the StoreKit handling result to IOS.StoreKit.ITunesURLsHandlingResult
// UMA histogram.
void RecordStoreKitHandlingResult(ITunesUrlsStoreKitHandlingResult result) {
UMA_HISTOGRAM_ENUMERATION("IOS.StoreKit.ITunesURLsHandlingResult", result,
ITunesUrlsStoreKitHandlingResult::kCount);
}
// Returns true, it the given |url| is iTunes product URL.
// iTunes URL should start with apple domain and has product id.
bool IsITunesProductUrl(const GURL& url) {
if (!url.SchemeIsHTTPOrHTTPS() || !url.DomainIs(kITunesUrlDomain))
return false;
std::string file_name = url.ExtractFileName();
// The first |kITunesProductIdLength| characters must be
// |kITunesProductIdPrefix|, followed by the app ID.
size_t prefix_length = strlen(kITunesProductIdPrefix);
return (file_name.length() > prefix_length &&
file_name.substr(0, prefix_length) == 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;
}
// Returns true, if ITunesUrlsHandlerTabHelper can handle the given |url|.
bool CanHandleUrl(const GURL& url) {
if (!IsITunesProductUrl(url))
return false;
// Valid iTunes URL structure:
// DOMAIN/OPTIONAL_REGION_CODE/MEDIA_TYPE/MEDIA_NAME/ID?PARAMETERS
// Check the URL media type, to determine if it is supported.
base::FilePath path;
if (!net::FileURLToFilePath(url, &path))
return false;
std::vector<base::FilePath::StringType> path_components;
path.GetComponents(&path_components);
// GetComponents considers "/" as the first component.
if (path_components.size() < kITunesUrlPathMinComponentsCount)
return false;
size_t media_type_index = kITunesUrlMediaTypeComponentDefaultIndex;
DCHECK(media_type_index > 0);
// If there is no reigon 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;
}
} // namespace
ITunesUrlsHandlerTabHelper::~ITunesUrlsHandlerTabHelper() = default;
ITunesUrlsHandlerTabHelper::ITunesUrlsHandlerTabHelper(web::WebState* web_state)
: web::WebStatePolicyDecider(web_state) {}
bool ITunesUrlsHandlerTabHelper::ShouldAllowRequest(
NSURLRequest* request,
ui::PageTransition transition,
bool from_main_frame) {
// Don't Handle URLS in Off The record mode as this will open StoreKit with
// Users' iTunes account. Also don't Handle requests from iframe because they
// may be spam, and they will be handled by other policy deciders.
if (web_state()->GetBrowserState()->IsOffTheRecord() || !from_main_frame)
return true;
GURL request_url = net::GURLWithNSURL(request.URL);
if (!CanHandleUrl(request_url))
return true;
HandleITunesUrl(request_url);
return false;
}
// private
void ITunesUrlsHandlerTabHelper::HandleITunesUrl(const GURL& url) {
ITunesUrlsStoreKitHandlingResult handling_result =
ITunesUrlsStoreKitHandlingResult::kSingleAppUrlHandled;
StoreKitTabHelper* tab_helper = StoreKitTabHelper::FromWebState(web_state());
if (tab_helper) {
base::RecordAction(
base::UserMetricsAction("ITunesLinksHandler_StoreKitLaunched"));
tab_helper->OpenAppStore(ExtractITunesProductParameters(url));
} else {
handling_result = ITunesUrlsStoreKitHandlingResult::kUrlHandlingFailed;
}
RecordStoreKitHandlingResult(handling_result);
}