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

#include "components/sessions/content/content_serialized_navigation_builder.h"

#include <string>

#include "base/check_op.h"
#include "base/containers/span.h"
#include "components/sessions/content/content_record_password_state.h"
#include "components/sessions/content/content_serialized_navigation_driver.h"
#include "components/sessions/content/extended_info_handler.h"
#include "components/sessions/content/navigation_task_id.h"
#include "components/sessions/core/serialized_navigation_entry.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/favicon_status.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_entry_restore_context.h"
#include "content/public/common/referrer.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "third_party/blink/public/common/page_state/page_state.h"

namespace sessions {

// static
SerializedNavigationEntry
ContentSerializedNavigationBuilder::FromNavigationEntry(
    int index,
    content::NavigationEntry* entry,
    SerializationOptions serialization_options) {
  SerializedNavigationEntry navigation;
  navigation.index_ = index;
  navigation.unique_id_ = entry->GetUniqueID();
  navigation.referrer_url_ = entry->GetReferrer().url;
  navigation.referrer_policy_ = static_cast<int>(entry->GetReferrer().policy);
  navigation.virtual_url_ = entry->GetVirtualURL();
  navigation.title_ = entry->GetTitle();
  if (!(serialization_options & SerializationOptions::EXCLUDE_PAGE_STATE))
    navigation.encoded_page_state_ = entry->GetPageState().ToEncodedData();
  navigation.transition_type_ = entry->GetTransitionType();
  navigation.has_post_data_ = entry->GetHasPostData();
  navigation.post_id_ = entry->GetPostID();
  navigation.original_request_url_ = entry->GetOriginalRequestURL();
  navigation.is_overriding_user_agent_ = entry->GetIsOverridingUserAgent();
  navigation.timestamp_ = entry->GetTimestamp();
  navigation.is_restored_ = entry->IsRestored();
  if (entry->GetFavicon().valid)
    navigation.favicon_url_ = entry->GetFavicon().url;
  navigation.http_status_code_ = entry->GetHttpStatusCode();
  navigation.redirect_chain_ = entry->GetRedirectChain();
  navigation.password_state_ = GetPasswordStateFromNavigation(entry);
  navigation.task_id_ = NavigationTaskId::Get(entry)->id();
  navigation.parent_task_id_ = NavigationTaskId::Get(entry)->parent_id();
  navigation.root_task_id_ = NavigationTaskId::Get(entry)->root_id();

  for (const auto& handler_entry :
       ContentSerializedNavigationDriver::GetInstance()
           ->GetAllExtendedInfoHandlers()) {
    ExtendedInfoHandler* handler = handler_entry.second.get();
    DCHECK(handler);
    std::string value = handler->GetExtendedInfo(entry);
    if (!value.empty())
      navigation.extended_info_map_[handler_entry.first] = value;
  }

  return navigation;
}

// static
std::unique_ptr<content::NavigationEntry>
ContentSerializedNavigationBuilder::ToNavigationEntry(
    const SerializedNavigationEntry* navigation,
    content::BrowserContext* browser_context,
    content::NavigationEntryRestoreContext* restore_context) {
  DCHECK(navigation);
  DCHECK(browser_context);
  DCHECK(restore_context);

  // The initial values of the NavigationEntry are usually temporary - they will
  // normally get clobbered by one of the SetPageState calls below.
  //
  // This means that things like |navigation->referrer_url| are ignored
  // in favor of using the data stored in |navigation->encoded_page_state|.
  //
  // We still use the URL and referrer from |navigation| here for the temporary
  // values, though, in case the PageState fails to decode and clobber them.
  // This allows us to load the original URL and referrer even if other state is
  // lost.
  GURL temporary_url = navigation->virtual_url_;
  content::Referrer temporary_referrer(
      navigation->referrer_url(),
      content::Referrer::ConvertToPolicy(navigation->referrer_policy()));
  std::optional<url::Origin> temporary_initiator_origin;
  std::optional<GURL> temporary_initiator_base_url;

  std::unique_ptr<content::NavigationEntry> entry(
      content::NavigationController::CreateNavigationEntry(
          temporary_url, temporary_referrer, temporary_initiator_origin,
          temporary_initiator_base_url,
          // Use a transition type of reload so that we don't incorrectly
          // increase the typed count.
          ui::PAGE_TRANSITION_RELOAD, false,
          // The extra headers are not sync'ed across sessions.
          std::string(), browser_context,
          nullptr /* blob_url_loader_factory */));

  // In some cases the |encoded_page_state| might be empty - we
  // need to gracefully handle such data when it is deserialized.
  //
  // One case is tests for "foreign" session restore entries, such as
  // SessionRestoreTest.RestoreForeignTab.  We hypothesise that old session
  // restore entries might also contain an empty |encoded_page_state|.
  if (navigation->encoded_page_state_.empty()) {
    // Ensure that the deserialized/restored content::NavigationEntry (and
    // the content::FrameNavigationEntry underneath) has a valid PageState.
    entry->SetPageState(
        blink::PageState::CreateFromURL(navigation->virtual_url_),
        restore_context);

    // The |navigation|-based referrer set below might be inconsistent with the
    // referrer embedded inside the PageState set above.  Nevertheless, to
    // minimize changes to behavior of old session restore entries, we restore
    // the deserialized referrer here.
    //
    // TODO(lukasza): Consider including the |deserialized_referrer| in the
    // PageState set above + drop the SetReferrer call below.  This will
    // slightly change the legacy behavior, but will make PageState and
    // Referrer consistent.
    entry->SetReferrer(temporary_referrer);
  } else {
    // Note that PageState covers some of the values inside |navigation| (e.g.
    // URL, Referrer).  Calling SetPageState will clobber these values in
    // content::NavigationEntry (and FrameNavigationEntry(s) below), as long as
    // the PageState successfully decodes.
    entry->SetPageState(blink::PageState::CreateFromEncodedData(
                            navigation->encoded_page_state_),
                        restore_context);

    // In theory the referrer information in the PageState should exactly match
    // the `navigation`-level data, but there are sometimes discrepancies in
    // practice (e.g. see https://crbug.com/1362322).
    //
    // TODO(crbug.com/40871594): Reintroduce DCHECKs that verify
    // consistency between `navigation->referrer_url()` and
    // `entry->GetReferrer().url` (and between referrer policies restored in
    // `entry` and remembered in `navigation`).
  }

  entry->SetTitle(navigation->title_);
  entry->SetHasPostData(navigation->has_post_data_);
  entry->SetPostID(navigation->post_id_);
  entry->SetOriginalRequestURL(navigation->original_request_url_);
  entry->SetIsOverridingUserAgent(navigation->is_overriding_user_agent_);
  entry->SetTimestamp(navigation->timestamp_);
  entry->SetHttpStatusCode(navigation->http_status_code_);
  entry->SetRedirectChain(navigation->redirect_chain_);
  entry->SetVirtualURL(navigation->virtual_url_);
  sessions::NavigationTaskId* navigation_task_id =
      sessions::NavigationTaskId::Get(entry.get());
  navigation_task_id->set_id(navigation->task_id());
  navigation_task_id->set_parent_id(navigation->parent_task_id());
  navigation_task_id->set_root_id(navigation->root_task_id());

  const ContentSerializedNavigationDriver::ExtendedInfoHandlerMap&
      extended_info_handlers = ContentSerializedNavigationDriver::GetInstance()
                                   ->GetAllExtendedInfoHandlers();
  for (const auto& [key, extended_info] : navigation->extended_info_map_) {
    auto it = extended_info_handlers.find(key);
    if (it == extended_info_handlers.end()) {
      continue;
    }
    ExtendedInfoHandler* extended_info_handler = it->second.get();
    DCHECK(extended_info_handler);
    extended_info_handler->RestoreExtendedInfo(extended_info, entry.get());
  }

  // This field should have the default value.
  DCHECK_EQ(SerializedNavigationEntry::STATE_INVALID,
            navigation->blocked_state_);

  return entry;
}

// static
std::vector<std::unique_ptr<content::NavigationEntry>>
ContentSerializedNavigationBuilder::ToNavigationEntries(
    base::span<const SerializedNavigationEntry> navigations,
    content::BrowserContext* browser_context) {
  std::unique_ptr<content::NavigationEntryRestoreContext> restore_context =
      content::NavigationEntryRestoreContext::Create();
  std::vector<std::unique_ptr<content::NavigationEntry>> entries;
  entries.reserve(navigations.size());
  for (const auto& navigation : navigations) {
    entries.push_back(
        ToNavigationEntry(&navigation, browser_context, restore_context.get()));
  }
  return entries;
}

}  // namespace sessions
