chromium/content/browser/renderer_host/navigation_controller_android.cc

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/renderer_host/navigation_controller_android.h"

#include <stdint.h>

#include <string>

#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/containers/flat_map.h"
#include "base/debug/crash_logging.h"
#include "base/debug/dump_without_crashing.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "content/browser/android/additional_navigation_params_utils.h"
#include "content/browser/renderer_host/navigation_controller_impl.h"
#include "content/browser/renderer_host/navigation_entry_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/ssl_host_state_delegate.h"
#include "content/public/common/referrer.h"
#include "content/public/common/resource_request_body_android.h"
#include "content/public/common/url_constants.h"
#include "net/base/data_url.h"
#include "ui/gfx/android/java_bitmap.h"
#include "url/android/gurl_android.h"
#include "url/gurl.h"
#include "url/origin.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "content/public/android/content_jni_headers/NavigationControllerImpl_jni.h"

using base::android::AttachCurrentThread;
using base::android::ConvertJavaStringToUTF16;
using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertUTF16ToJavaString;
using base::android::JavaParamRef;
using base::android::JavaRef;
using base::android::ScopedJavaLocalRef;

namespace {

const char kMapDataKey[] = "map_data_key";

// static
static base::android::ScopedJavaLocalRef<jobject>
JNI_NavigationControllerImpl_CreateJavaNavigationEntry(
    JNIEnv* env,
    content::NavigationEntry* entry,
    int index) {
  DCHECK(entry);

  // Get the details of the current entry
  ScopedJavaLocalRef<jobject> j_url(
      url::GURLAndroid::FromNativeGURL(env, entry->GetURL()));
  ScopedJavaLocalRef<jobject> j_virtual_url(
      url::GURLAndroid::FromNativeGURL(env, entry->GetVirtualURL()));
  ScopedJavaLocalRef<jobject> j_original_url(
      url::GURLAndroid::FromNativeGURL(env, entry->GetOriginalRequestURL()));
  ScopedJavaLocalRef<jstring> j_title(
      ConvertUTF16ToJavaString(env, entry->GetTitle()));
  ScopedJavaLocalRef<jobject> j_bitmap;
  const content::FaviconStatus& status = entry->GetFavicon();
  if (status.valid && status.image.ToSkBitmap()->computeByteSize() > 0) {
    j_bitmap = gfx::ConvertToJavaBitmap(*status.image.ToSkBitmap(),
                                        gfx::OomBehavior::kReturnNullOnOom);
  }
  jlong j_timestamp = entry->GetTimestamp().InMillisecondsSinceUnixEpoch();

  return content::Java_NavigationControllerImpl_createNavigationEntry(
      env, index, j_url, j_virtual_url, j_original_url, j_title, j_bitmap,
      entry->GetTransitionType(), j_timestamp, entry->IsInitialEntry());
}

static void JNI_NavigationControllerImpl_AddNavigationEntryToHistory(
    JNIEnv* env,
    const JavaRef<jobject>& history,
    content::NavigationEntry* entry,
    int index) {
  content::Java_NavigationControllerImpl_addToNavigationHistory(
      env, history,
      JNI_NavigationControllerImpl_CreateJavaNavigationEntry(env, entry,
                                                             index));
}

class MapData : public base::SupportsUserData::Data {
 public:
  MapData() = default;

  MapData(const MapData&) = delete;
  MapData& operator=(const MapData&) = delete;

  ~MapData() override = default;

  static MapData* Get(content::NavigationEntry* entry) {
    MapData* map_data = static_cast<MapData*>(entry->GetUserData(kMapDataKey));
    if (map_data)
      return map_data;
    auto map_data_ptr = std::make_unique<MapData>();
    map_data = map_data_ptr.get();
    entry->SetUserData(kMapDataKey, std::move(map_data_ptr));
    return map_data;
  }

  base::flat_map<std::string, std::u16string>& map() { return map_; }

  // base::SupportsUserData::Data:
  std::unique_ptr<Data> Clone() override {
    std::unique_ptr<MapData> clone = std::make_unique<MapData>();
    clone->map_ = map_;
    return clone;
  }

 private:
  base::flat_map<std::string, std::u16string> map_;
};

}  // namespace

namespace content {

NavigationControllerAndroid::NavigationControllerAndroid(
    NavigationControllerImpl* navigation_controller)
    : navigation_controller_(navigation_controller) {
  JNIEnv* env = AttachCurrentThread();
  obj_.Reset(env, Java_NavigationControllerImpl_create(
                      env, reinterpret_cast<intptr_t>(this))
                      .obj());
}

NavigationControllerAndroid::~NavigationControllerAndroid() {
  Java_NavigationControllerImpl_destroy(AttachCurrentThread(), obj_);
}

base::android::ScopedJavaLocalRef<jobject>
NavigationControllerAndroid::GetJavaObject() {
  return base::android::ScopedJavaLocalRef<jobject>(obj_);
}

jboolean NavigationControllerAndroid::CanGoBack(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  return navigation_controller_->CanGoBack();
}

jboolean NavigationControllerAndroid::CanGoForward(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  return navigation_controller_->CanGoForward();
}

jboolean NavigationControllerAndroid::CanGoToOffset(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jint offset) {
  return navigation_controller_->CanGoToOffsetWithSkipping(offset);
}

void NavigationControllerAndroid::GoBack(JNIEnv* env,
                                         const JavaParamRef<jobject>& obj) {
  navigation_controller_->GoBack();
}

void NavigationControllerAndroid::GoForward(JNIEnv* env,
                                            const JavaParamRef<jobject>& obj) {
  navigation_controller_->GoForward();
}

void NavigationControllerAndroid::GoToOffset(JNIEnv* env,
                                             const JavaParamRef<jobject>& obj,
                                             jint offset) {
  navigation_controller_->GoToOffsetWithSkipping(offset);
}

jboolean NavigationControllerAndroid::IsInitialNavigation(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  return navigation_controller_->IsInitialNavigation();
}

void NavigationControllerAndroid::LoadIfNecessary(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  navigation_controller_->LoadIfNecessary();
}

void NavigationControllerAndroid::ContinuePendingReload(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  navigation_controller_->ContinuePendingReload();
}

void NavigationControllerAndroid::Reload(JNIEnv* env,
                                         const JavaParamRef<jobject>& obj,
                                         jboolean check_for_repost) {
  SCOPED_CRASH_KEY_BOOL("nav_reentrancy_caller2", "Reload_check",
                        (bool)check_for_repost);
  navigation_controller_->Reload(ReloadType::NORMAL, check_for_repost);
}

void NavigationControllerAndroid::ReloadBypassingCache(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jboolean check_for_repost) {
  SCOPED_CRASH_KEY_BOOL("nav_reentrancy_caller2", "ReloadB_check",
                        (bool)check_for_repost);
  navigation_controller_->Reload(ReloadType::BYPASSING_CACHE, check_for_repost);
}

jboolean NavigationControllerAndroid::NeedsReload(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  return navigation_controller_->NeedsReload();
}

void NavigationControllerAndroid::SetNeedsReload(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  navigation_controller_->SetNeedsReload();
}

void NavigationControllerAndroid::CancelPendingReload(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  navigation_controller_->CancelPendingReload();
}

void NavigationControllerAndroid::GoToNavigationIndex(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jint index) {
  navigation_controller_->GoToIndex(index);
}

base::android::ScopedJavaLocalRef<jobject> NavigationControllerAndroid::LoadUrl(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    const JavaParamRef<jstring>& url,
    jint load_url_type,
    jint transition_type,
    const JavaParamRef<jstring>& j_referrer_url,
    jint referrer_policy,
    jint ua_override_option,
    const JavaParamRef<jstring>& extra_headers,
    const JavaParamRef<jobject>& j_post_data,
    const JavaParamRef<jstring>& base_url_for_data_url,
    const JavaParamRef<jstring>& virtual_url_for_special_cases,
    const JavaParamRef<jstring>& data_url_as_string,
    jboolean can_load_local_resources,
    jboolean is_renderer_initiated,
    jboolean should_replace_current_entry,
    const JavaParamRef<jobject>& j_initiator_origin,
    jboolean has_user_gesture,
    jboolean should_clear_history_list,
    const base::android::JavaParamRef<jobject>& j_additional_navigation_params,
    jlong input_start,
    jlong navigation_ui_data_ptr,
    jboolean is_pdf) {
  DCHECK(url);
  NavigationController::LoadURLParams params(
      GURL(ConvertJavaStringToUTF8(env, url)));
  // Wrap the raw pointer in case on an early return.
  std::unique_ptr<NavigationUIData> navigation_ui_data = base::WrapUnique(
      reinterpret_cast<NavigationUIData*>(navigation_ui_data_ptr));
  params.load_type =
      static_cast<NavigationController::LoadURLType>(load_url_type);
  params.transition_type = ui::PageTransitionFromInt(transition_type);
  params.override_user_agent =
      static_cast<NavigationController::UserAgentOverrideOption>(
          ua_override_option);
  params.can_load_local_resources = can_load_local_resources;
  params.is_renderer_initiated = is_renderer_initiated;
  params.should_replace_current_entry = should_replace_current_entry;
  params.has_user_gesture = has_user_gesture;
  params.should_clear_history_list = should_clear_history_list;
  params.is_pdf = is_pdf;

  if (j_additional_navigation_params) {
    params.initiator_frame_token =
        GetInitiatorFrameTokenFromJavaAdditionalNavigationParams(
            env, j_additional_navigation_params);
    params.initiator_process_id =
        GetInitiatorProcessIdFromJavaAdditionalNavigationParams(
            env, j_additional_navigation_params);

    // If the attribution src token exists, then an impression exists with this
    // navigation.
    if (GetAttributionSrcTokenFromJavaAdditionalNavigationParams(
            env, j_additional_navigation_params)
            .has_value()) {
      blink::Impression impression;
      impression.attribution_src_token =
          GetAttributionSrcTokenFromJavaAdditionalNavigationParams(
              env, j_additional_navigation_params)
              .value();
      params.impression = impression;
    }
  }

  if (extra_headers)
    params.extra_headers = ConvertJavaStringToUTF8(env, extra_headers);

  params.post_data = ExtractResourceRequestBodyFromJavaObject(env, j_post_data);

  if (base_url_for_data_url) {
    params.base_url_for_data_url =
        GURL(ConvertJavaStringToUTF8(env, base_url_for_data_url));
  }

  if (virtual_url_for_special_cases) {
    params.virtual_url_for_special_cases =
        GURL(ConvertJavaStringToUTF8(env, virtual_url_for_special_cases));
  }

  if (data_url_as_string) {
    // Treat |data_url_as_string| as if we were intending to put it into a GURL
    // field. Note that kMaxURLChars is only enforced when serializing URLs
    // for IPC.
    GURL data_url = GURL(ConvertJavaStringToUTF8(env, data_url_as_string));
    DCHECK(data_url.SchemeIs(url::kDataScheme));
    DCHECK(params.url.SchemeIs(url::kDataScheme));
#if DCHECK_IS_ON()
    {
      std::string mime_type, charset, data;
      DCHECK(net::DataURL::Parse(params.url, &mime_type, &charset, &data));
      DCHECK(data.empty());
    }
#endif
    std::string s = data_url.spec();
    params.data_url_as_string =
        base::MakeRefCounted<base::RefCountedString>(std::move(s));
  }

  if (j_referrer_url) {
    params.referrer =
        Referrer(GURL(ConvertJavaStringToUTF8(env, j_referrer_url)),
                 Referrer::ConvertToPolicy(referrer_policy));
  }

  if (j_initiator_origin) {
    params.initiator_origin = url::Origin::FromJavaObject(j_initiator_origin);
  }

  if (input_start != 0)
    params.input_start = base::TimeTicks::FromUptimeMillis(input_start);

  params.navigation_ui_data = std::move(navigation_ui_data);

  base::WeakPtr<NavigationHandle> handle =
      navigation_controller_->LoadURLWithParams(params);

  if (!handle) {
    return nullptr;
  }

  return base::android::ScopedJavaLocalRef<jobject>(
      handle->GetJavaNavigationHandle());
}

void NavigationControllerAndroid::ClearHistory(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  // TODO(creis): Do callers of this need to know if it fails?
  if (navigation_controller_->CanPruneAllButLastCommitted())
    navigation_controller_->PruneAllButLastCommitted();
}

jint NavigationControllerAndroid::GetNavigationHistory(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    const JavaParamRef<jobject>& history) {
  // Iterate through navigation entries to populate the list
  int count = navigation_controller_->GetEntryCount();
  for (int i = 0; i < count; ++i) {
    JNI_NavigationControllerImpl_AddNavigationEntryToHistory(
        env, history, navigation_controller_->GetEntryAtIndex(i), i);
  }

  return navigation_controller_->GetCurrentEntryIndex();
}

void NavigationControllerAndroid::GetDirectedNavigationHistory(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    const JavaParamRef<jobject>& history,
    jboolean is_forward,
    jint max_entries) {
  // Iterate through navigation entries to populate the list
  int count = navigation_controller_->GetEntryCount();
  int num_added = 0;
  int increment_value = is_forward ? 1 : -1;
  for (int i = navigation_controller_->GetCurrentEntryIndex() + increment_value;
       i >= 0 && i < count; i += increment_value) {
    if (num_added >= max_entries)
      break;

    JNI_NavigationControllerImpl_AddNavigationEntryToHistory(
        env, history, navigation_controller_->GetEntryAtIndex(i), i);
    num_added++;
  }
}

void NavigationControllerAndroid::ClearSslPreferences(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  SSLHostStateDelegate* delegate =
      navigation_controller_->GetBrowserContext()->GetSSLHostStateDelegate();
  if (delegate)
    delegate->Clear(base::NullCallback());
}

bool NavigationControllerAndroid::GetUseDesktopUserAgent(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  NavigationEntry* entry = navigation_controller_->GetLastCommittedEntry();
  return entry && entry->GetIsOverridingUserAgent();
}

void NavigationControllerAndroid::SetUseDesktopUserAgent(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jboolean enabled,
    jboolean reload_on_state_change,
    jint source) {
  SCOPED_CRASH_KEY_BOOL("nav_reentrancy_caller2", "SetUA_enabled",
                        (bool)enabled);
  if (GetUseDesktopUserAgent(env, obj) == enabled)
    return;

  if (navigation_controller_->in_navigate_to_pending_entry() &&
      reload_on_state_change) {
    // Sometimes it's possible to call this function in response to a
    // navigation to a pending entry. In this case, we should avoid triggering
    // another navigation synchronously, as it will crash due to navigation
    // re-entrancy checks. To do that, post a task to update the UA and
    // reload asynchronously.
    // TODO(crbug.com/40841494): Figure out the case that leads to this
    // situation and avoid calling this function entirely in that case. For now,
    // do a do a DumpWithoutCrashing so that we can investigate.
    GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(
            &NavigationControllerAndroid::SetUseDesktopUserAgentInternal,
            weak_factory_.GetWeakPtr(), enabled, reload_on_state_change));
    LOG(WARNING) << "NavigationControllerAndroid::SetUseDesktopUserAgent "
                 << "triggers re-entrant navigation, override: "
                 << (bool)enabled << ", source: " << (int)source;
    SCOPED_CRASH_KEY_NUMBER("SetUseDesktopUserAgent", "caller", (int)source);
    base::debug::DumpWithoutCrashing();
  } else {
    SetUseDesktopUserAgentInternal(enabled, reload_on_state_change);
  }
}

void NavigationControllerAndroid::SetUseDesktopUserAgentInternal(
    bool enabled,
    bool reload_on_state_change) {
  // Make sure the navigation entry actually exists.
  NavigationEntry* entry = navigation_controller_->GetLastCommittedEntry();
  // TODO(crbug.com/40063008): Early return for initial NavigationEntries as a
  // workaround. Currently, doing a reload while on the initial NavigationEntry
  // might result in committing an unrelated pending NavigationEntry and
  // mistakenly marking that entry as an initial NavigationEntry. That will
  // cause problems, such as the URL bar showing about:blank instead of the URL
  // of the NavigationEntry. To prevent that happening in this case, skip
  // reloading initial NavigationEntries entirely. This is a short-term fix,
  // while we work on a long-term fix to no longer mistakenly mark the unrelated
  // pending NavigationEntry as the initial NavigationEntry.
  if (!entry || entry->IsInitialEntry()) {
    return;
  }

  // Set the flag in the NavigationEntry.
  entry->SetIsOverridingUserAgent(enabled);
  navigation_controller_->delegate()->UpdateOverridingUserAgent();

  // Send the override to the renderer.
  if (reload_on_state_change) {
    // Reloading the page will send the override down as part of the
    // navigation IPC message.
    navigation_controller_->LoadOriginalRequestURL();
  }
}

base::android::ScopedJavaLocalRef<jobject>
NavigationControllerAndroid::GetEntryAtIndex(JNIEnv* env,
                                             const JavaParamRef<jobject>& obj,
                                             int index) {
  if (index < 0 || index >= navigation_controller_->GetEntryCount())
    return base::android::ScopedJavaLocalRef<jobject>();

  NavigationEntry* entry = navigation_controller_->GetEntryAtIndex(index);
  return JNI_NavigationControllerImpl_CreateJavaNavigationEntry(env, entry,
                                                                index);
}

base::android::ScopedJavaLocalRef<jobject>
NavigationControllerAndroid::GetVisibleEntry(JNIEnv* env,
                                             const JavaParamRef<jobject>& obj) {
  NavigationEntry* entry = navigation_controller_->GetVisibleEntry();

  if (!entry)
    return base::android::ScopedJavaLocalRef<jobject>();

  return JNI_NavigationControllerImpl_CreateJavaNavigationEntry(env, entry,
                                                                /*index=*/-1);
}

base::android::ScopedJavaLocalRef<jobject>
NavigationControllerAndroid::GetPendingEntry(JNIEnv* env,
                                             const JavaParamRef<jobject>& obj) {
  NavigationEntry* entry = navigation_controller_->GetPendingEntry();

  if (!entry)
    return base::android::ScopedJavaLocalRef<jobject>();

  return JNI_NavigationControllerImpl_CreateJavaNavigationEntry(
      env, entry, navigation_controller_->GetPendingEntryIndex());
}

jint NavigationControllerAndroid::GetLastCommittedEntryIndex(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  return navigation_controller_->GetLastCommittedEntryIndex();
}

jboolean NavigationControllerAndroid::RemoveEntryAtIndex(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jint index) {
  return navigation_controller_->RemoveEntryAtIndex(index);
}

void NavigationControllerAndroid::PruneForwardEntries(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj) {
  return navigation_controller_->PruneForwardEntries();
}

ScopedJavaLocalRef<jstring> NavigationControllerAndroid::GetEntryExtraData(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jint index,
    const JavaParamRef<jstring>& jkey) {
  if (index < 0 || index >= navigation_controller_->GetEntryCount())
    return ScopedJavaLocalRef<jstring>();

  std::string key = base::android::ConvertJavaStringToUTF8(env, jkey);
  MapData* map_data =
      MapData::Get(navigation_controller_->GetEntryAtIndex(index));
  auto iter = map_data->map().find(key);
  return ConvertUTF16ToJavaString(
      env, iter == map_data->map().end() ? std::u16string() : iter->second);
}

void NavigationControllerAndroid::SetEntryExtraData(
    JNIEnv* env,
    const JavaParamRef<jobject>& obj,
    jint index,
    const JavaParamRef<jstring>& jkey,
    const JavaParamRef<jstring>& jvalue) {
  if (index < 0 || index >= navigation_controller_->GetEntryCount())
    return;

  std::string key = base::android::ConvertJavaStringToUTF8(env, jkey);
  std::u16string value = base::android::ConvertJavaStringToUTF16(env, jvalue);
  MapData* map_data =
      MapData::Get(navigation_controller_->GetEntryAtIndex(index));
  map_data->map()[key] = value;
}

}  // namespace content