chromium/ios/chrome/browser/itunes_urls/model/itunes_urls_handler_tab_helper.mm

// 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)