chromium/ios/chrome/browser/https_upgrades/model/typed_navigation_upgrade_tab_helper.mm

// Copyright 2022 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/https_upgrades/model/typed_navigation_upgrade_tab_helper.h"

#import "base/feature_list.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/string_number_conversions.h"
#import "base/task/sequenced_task_runner.h"
#import "components/security_interstitials/core/omnibox_https_upgrade_metrics.h"
#import "ios/chrome/browser/https_upgrades/model/https_upgrade_service_impl.h"
#import "ios/chrome/browser/prerender/model/prerender_service.h"
#import "ios/components/security_interstitials/https_only_mode/https_upgrade_service.h"
#import "ios/web/public/navigation/https_upgrade_type.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "net/base/apple/url_conversions.h"
#import "ui/base/window_open_disposition.h"

using security_interstitials::omnibox_https_upgrades::Event;
using security_interstitials::omnibox_https_upgrades::kEventHistogram;

namespace {

void RecordUMA(Event event) {
  base::UmaHistogramEnumeration(kEventHistogram, event);
}

}  // namespace

TypedNavigationUpgradeTabHelper::~TypedNavigationUpgradeTabHelper() = default;

TypedNavigationUpgradeTabHelper::TypedNavigationUpgradeTabHelper(
    web::WebState* web_state,
    PrerenderService* prerender_service,
    HttpsUpgradeService* service)
    : prerender_service_(prerender_service), service_(service) {
  web_state->AddObserver(this);
}

bool TypedNavigationUpgradeTabHelper::IsTimerRunningForTesting() const {
  return timer_.IsRunning();
}

void TypedNavigationUpgradeTabHelper::OnHttpsLoadTimeout(
    base::WeakPtr<web::WebState> weak_web_state) {
  DCHECK(state_ == State::kUpgraded);
  state_ = State::kStoppedWithTimeout;
  web::WebState* web_state = weak_web_state.get();
  if (web_state) {
    web_state->Stop();
  }
}

void TypedNavigationUpgradeTabHelper::FallbackToHttp(web::WebState* web_state,
                                                     const GURL& https_url) {
  const GURL http_url = service_->GetHttpUrl(https_url);
  DCHECK(http_url.is_valid());
  state_ = State::kFallbackStarted;
  // Start a new navigation to the original HTTP page.
  web::NavigationManager::WebLoadParams params(http_url);
  params.transition_type = navigation_transition_type_;
  params.is_renderer_initiated = navigation_is_renderer_initiated_;
  params.referrer = referrer_;
  // The fallback navigation is no longer considered upgraded.
  params.https_upgrade_type = web::HttpsUpgradeType::kNone;
  // Post a task to navigate to the fallback URL. We don't want to navigate
  // synchronously from a DidNavigationFinish() call.
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(
          [](base::WeakPtr<web::WebState> web_state,
             const web::NavigationManager::WebLoadParams& params) {
            if (web_state) {
              web_state->GetNavigationManager()->LoadURLWithParams(params);
            }
          },
          web_state->GetWeakPtr(), std::move(params)));
}

void TypedNavigationUpgradeTabHelper::DidStartNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  if (navigation_context->IsSameDocument()) {
    return;
  }
  if (prerender_service_ &&
      prerender_service_->IsWebStatePrerendered(web_state)) {
    return;
  }

  web::NavigationItem* item_pending =
      web_state->GetNavigationManager()->GetPendingItem();
  if (item_pending &&
      item_pending->GetHttpsUpgradeType() == web::HttpsUpgradeType::kOmnibox) {
    upgraded_https_url_ = navigation_context->GetUrl();

    // TODO(crbug.com/40230443): Remove this scheme check once fixed. Without
    // the fix, kHttpsLoadStarted bucket is mildly overcounted.
    GURL url = item_pending->GetURL();
    if (url.SchemeIs(url::kHttpsScheme) ||
        service_->IsFakeHTTPSForTesting(url)) {
      // Pending navigation may not always correspond to the initial navigation,
      // e.g. when a new navigation is started before the first one is finished,
      // but we are only using it to record metrics so this is acceptable.
      state_ = State::kUpgraded;
      RecordUMA(Event::kHttpsLoadStarted);
      // `timer_` is deleted when the tab helper is deleted, so it's safe to use
      // Unretained here.
      timer_.Start(
          FROM_HERE, service_->GetFallbackDelay(),
          base::BindOnce(&TypedNavigationUpgradeTabHelper::OnHttpsLoadTimeout,
                         base::Unretained(this), web_state->GetWeakPtr()));
    }
  }
}

void TypedNavigationUpgradeTabHelper::DidFinishNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  if (navigation_context->IsSameDocument() || state_ == State::kNone) {
    return;
  }
  if (prerender_service_ &&
      prerender_service_->IsWebStatePrerendered(web_state)) {
    return;
  }
  timer_.Stop();

  // Start a fallback navigation if the upgraded navigation failed.
  if (navigation_context->GetFailedHttpsUpgradeType() ==
      web::HttpsUpgradeType::kOmnibox) {
    RecordUMA(Event::kHttpsLoadFailedWithCertError);
    FallbackToHttp(web_state, navigation_context->GetUrl());
    return;
  }

  // Start a fallback navigation if the upgraded navigation timed out.
  if (state_ == State::kStoppedWithTimeout) {
    DCHECK(!timer_.IsRunning());
    RecordUMA(Event::kHttpsLoadTimedOut);
    // TODO(crbug.com/40875679): Cleanup this logic, we should only use
    // upgraded_https_url_ here.
    if (upgraded_https_url_.is_valid()) {
      FallbackToHttp(web_state, upgraded_https_url_);
    } else {
      FallbackToHttp(web_state, navigation_context->GetUrl());
    }
  }

  // Record success.
  if (state_ == State::kUpgraded &&
      (navigation_context->GetUrl().SchemeIs(url::kHttpsScheme) ||
       service_->IsFakeHTTPSForTesting(navigation_context->GetUrl()))) {
    RecordUMA(Event::kHttpsLoadSucceeded);
  }
  state_ = State::kNone;
}

void TypedNavigationUpgradeTabHelper::WebStateDestroyed(
    web::WebState* web_state) {
  web_state->RemoveObserver(this);
}

WEB_STATE_USER_DATA_KEY_IMPL(TypedNavigationUpgradeTabHelper)