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

#include "extensions/browser/extension_api_frame_id_map.h"

#include <string_view>
#include <tuple>
#include <utility>

#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/uuid.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_host.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/common/constants.h"

namespace extensions {

const int ExtensionApiFrameIdMap::kInvalidFrameId = -1;
const int ExtensionApiFrameIdMap::kTopFrameId = 0;

ExtensionApiFrameIdMap::FrameData::FrameData()
    : frame_id(kInvalidFrameId),
      parent_frame_id(kInvalidFrameId),
      tab_id(extension_misc::kUnknownTabId),
      window_id(extension_misc::kUnknownWindowId),
      frame_type(api::extension_types::FrameType::kOutermostFrame),
      document_lifecycle(api::extension_types::DocumentLifecycle::kActive) {}

ExtensionApiFrameIdMap::FrameData::FrameData(
    int frame_id,
    int parent_frame_id,
    int tab_id,
    int window_id,
    const DocumentId& document_id,
    const DocumentId& parent_document_id,
    api::extension_types::FrameType frame_type,
    api::extension_types::DocumentLifecycle document_lifecycle)
    : frame_id(frame_id),
      parent_frame_id(parent_frame_id),
      tab_id(tab_id),
      window_id(window_id),
      document_id(document_id),
      parent_document_id(parent_document_id),
      frame_type(frame_type),
      document_lifecycle(document_lifecycle) {}

ExtensionApiFrameIdMap::FrameData::~FrameData() = default;

ExtensionApiFrameIdMap::FrameData::FrameData(
    const ExtensionApiFrameIdMap::FrameData& other) = default;
ExtensionApiFrameIdMap::FrameData& ExtensionApiFrameIdMap::FrameData::operator=(
    const ExtensionApiFrameIdMap::FrameData& other) = default;

ExtensionApiFrameIdMap::ExtensionApiFrameIdMap() = default;

ExtensionApiFrameIdMap::~ExtensionApiFrameIdMap() = default;

// static
ExtensionApiFrameIdMap* ExtensionApiFrameIdMap::Get() {
  // The map is accessed on the IO and UI thread, so construct it once and never
  // delete it.
  static base::NoDestructor<ExtensionApiFrameIdMap> instance;
  return instance.get();
}

// static
int ExtensionApiFrameIdMap::GetFrameId(
    content::RenderFrameHost* render_frame_host) {
  if (!render_frame_host) {
    return kInvalidFrameId;
  }
  if (!render_frame_host->IsInPrimaryMainFrame()) {
    return render_frame_host->GetFrameTreeNodeId().value();
  }
  return kTopFrameId;
}

// static
int ExtensionApiFrameIdMap::GetFrameId(
    content::NavigationHandle* navigation_handle) {
  return navigation_handle->IsInPrimaryMainFrame()
             ? kTopFrameId
             : navigation_handle->GetFrameTreeNodeId().value();
}

// static
int ExtensionApiFrameIdMap::GetParentFrameId(
    content::RenderFrameHost* render_frame_host) {
  return render_frame_host
             ? GetFrameId(render_frame_host->GetParentOrOuterDocument())
             : kInvalidFrameId;
}

// static
int ExtensionApiFrameIdMap::GetParentFrameId(
    content::NavigationHandle* navigation_handle) {
  return GetFrameId(navigation_handle->GetParentFrameOrOuterDocument());
}

// static
content::RenderFrameHost* ExtensionApiFrameIdMap::GetRenderFrameHostById(
    content::WebContents* web_contents,
    int frame_id) {
  // Although it is technically possible to map |frame_id| to a RenderFrameHost
  // without WebContents, we choose to not do that because in the extension API
  // frameIds are only guaranteed to be meaningful in combination with a tabId.
  if (!web_contents) {
    return nullptr;
  }

  if (frame_id == kInvalidFrameId) {
    return nullptr;
  }

  if (frame_id == kTopFrameId) {
    return web_contents->GetPrimaryMainFrame();
  }

  DCHECK_GE(frame_id, 1);

  // Unfortunately, extension APIs do not know which process to expect for a
  // given frame ID, so we must use an unsafe API here that could return a
  // different RenderFrameHost than the caller may have expected (e.g., one that
  // changed after a cross-process navigation).
  content::RenderFrameHost* render_frame_host =
      web_contents->UnsafeFindFrameByFrameTreeNodeId(
          content::FrameTreeNodeId(frame_id));

  // Fail if the frame is not active or in prerendering (e.g. in the
  // back/forward cache).
  if (!render_frame_host ||
      (!render_frame_host->IsActive() &&
       !render_frame_host->IsInLifecycleState(
           content::RenderFrameHost::LifecycleState::kPrerendering))) {
    return nullptr;
  }

  return render_frame_host;
}

content::RenderFrameHost*
ExtensionApiFrameIdMap::GetRenderFrameHostByDocumentId(
    const DocumentId& document_id) {
  auto iter = document_id_map_.find(document_id);
  if (iter == document_id_map_.end()) {
    return nullptr;
  }
  return &iter->second->render_frame_host();
}

content::RenderFrameHost* ExtensionApiFrameIdMap::GetRenderFrameHostByFrameId(
    int frame_id) {
  // Frame_id values of 0 are not guaranteed to be unique. Values less than 0
  // are invalid.
  CHECK_GE(frame_id, 1);

  content::RenderFrameHost* render_frame_host = nullptr;
  for (const auto& iter : document_id_map_) {
    if (frame_id ==
        ExtensionApiFrameIdMap::GetFrameId(&iter.second->render_frame_host())) {
      render_frame_host = &iter.second->render_frame_host();
      break;
    }
  }

  // Fail if the frame is not active or in prerendering (e.g. in the
  // back/forward cache).
  if (!render_frame_host ||
      (!render_frame_host->IsActive() &&
       !render_frame_host->IsInLifecycleState(
           content::RenderFrameHost::LifecycleState::kPrerendering))) {
    return nullptr;
  }
  return render_frame_host;
}

ExtensionApiFrameIdMap::DocumentId ExtensionApiFrameIdMap::DocumentIdFromString(
    const std::string& document_id) {
  if (document_id.length() != 32) {
    return DocumentId();
  }

  std::string_view string_piece(document_id);
  uint64_t high = 0;
  uint64_t low = 0;
  if (!base::HexStringToUInt64(string_piece.substr(0, 16), &high) ||
      !base::HexStringToUInt64(string_piece.substr(16, 16), &low)) {
    return DocumentId();
  }

  std::optional<base::UnguessableToken> token =
      base::UnguessableToken::Deserialize(high, low);
  if (!token.has_value()) {
    return DocumentId();
  }
  return token.value();
}

ExtensionApiFrameIdMap::FrameData ExtensionApiFrameIdMap::KeyToValue(
    content::GlobalRenderFrameHostId key,
    bool require_live_frame) const {
  return KeyToValue(content::RenderFrameHost::FromID(key), require_live_frame);
}

ExtensionApiFrameIdMap::FrameData ExtensionApiFrameIdMap::KeyToValue(
    content::RenderFrameHost* render_frame_host,
    bool require_live_frame) const {
  if (!render_frame_host ||
      (require_live_frame && !render_frame_host->IsRenderFrameLive())) {
    return FrameData();
  }

  int tab_id = extension_misc::kUnknownTabId;
  int window_id = extension_misc::kUnknownWindowId;
  // The browser client can be null in unittests.
  if (ExtensionsBrowserClient::Get()) {
    ExtensionsBrowserClient::Get()->GetTabAndWindowIdForWebContents(
        content::WebContents::FromRenderFrameHost(render_frame_host), &tab_id,
        &window_id);
  }

  return FrameData(
      GetFrameId(render_frame_host), GetParentFrameId(render_frame_host),
      tab_id, window_id, GetDocumentId(render_frame_host),
      GetDocumentId(render_frame_host->GetParentOrOuterDocument()),
      GetFrameType(render_frame_host), GetDocumentLifecycle(render_frame_host));
}

ExtensionApiFrameIdMap::FrameData ExtensionApiFrameIdMap::GetFrameData(
    content::GlobalRenderFrameHostId render_frame_host_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  auto frame_id_iter = deleted_frame_data_map_.find(render_frame_host_id);
  if (frame_id_iter != deleted_frame_data_map_.end()) {
    return frame_id_iter->second;
  }

  return KeyToValue(render_frame_host_id, true /* require_live_frame */);
}

ExtensionApiFrameIdMap::DocumentId ExtensionApiFrameIdMap::GetDocumentId(
    content::RenderFrameHost* render_frame_host) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  // This check allows callers to pass in the result from
  // GetParentOrOuterDocument() without needing to check whether the resulting
  // frame exists.
  if (!render_frame_host) {
    return DocumentId();
  }
  return ExtensionDocumentUserData::GetOrCreateForCurrentDocument(
             render_frame_host)
      ->document_id();
}

ExtensionApiFrameIdMap::DocumentId ExtensionApiFrameIdMap::GetDocumentId(
    content::NavigationHandle* navigation_handle) {
  // We can only access NavigationHandle::GetRenderFrameHost if the navigation
  // handle has committed or is waiting to commit. This is fine because
  // otherwise the documentId is useless as it will point at the old
  // document.
  if (navigation_handle->IsWaitingToCommit() ||
      navigation_handle->HasCommitted()) {
    return GetDocumentId(navigation_handle->GetRenderFrameHost());
  }
  return DocumentId();
}

// static
base::Uuid ExtensionApiFrameIdMap::GetContextId(
    content::RenderFrameHost* render_frame_host) {
  return ExtensionDocumentUserData::GetOrCreateForCurrentDocument(
             render_frame_host)
      ->context_id();
}

api::extension_types::FrameType ExtensionApiFrameIdMap::GetFrameType(
    content::RenderFrameHost* render_frame_host) {
  DCHECK(render_frame_host);
  if (!render_frame_host->GetParentOrOuterDocument()) {
    return api::extension_types::FrameType::kOutermostFrame;
  }
  if (render_frame_host->IsFencedFrameRoot()) {
    return api::extension_types::FrameType::kFencedFrame;
  }
  return api::extension_types::FrameType::kSubFrame;
}

api::extension_types::FrameType ExtensionApiFrameIdMap::GetFrameType(
    content::NavigationHandle* navigation_handle) {
  switch (navigation_handle->GetNavigatingFrameType()) {
    case content::FrameType::kSubframe:
      return api::extension_types::FrameType::kSubFrame;
    case content::FrameType::kFencedFrameRoot:
      return api::extension_types::FrameType::kFencedFrame;
    case content::FrameType::kPrimaryMainFrame:
    case content::FrameType::kPrerenderMainFrame:
    case content::FrameType::kGuestMainFrame:
      return api::extension_types::FrameType::kOutermostFrame;
  }
}

api::extension_types::DocumentLifecycle
ExtensionApiFrameIdMap::GetDocumentLifecycle(
    content::RenderFrameHost* render_frame_host) {
  DCHECK(render_frame_host);
  // We use IsInLifecycleState as opposed to GetLifecycleState with a switch
  // because we cannot call GetLifecycleState for speculative frames.
  if (render_frame_host->IsInLifecycleState(
          content::RenderFrameHost::LifecycleState::kActive)) {
    return api::extension_types::DocumentLifecycle::kActive;
  }
  if (render_frame_host->IsInLifecycleState(
          content::RenderFrameHost::LifecycleState::kInBackForwardCache)) {
    return api::extension_types::DocumentLifecycle::kCached;
  }
  if (render_frame_host->IsInLifecycleState(
          content::RenderFrameHost::LifecycleState::kPrerendering)) {
    return api::extension_types::DocumentLifecycle::kPrerender;
  }
  if (render_frame_host->IsInLifecycleState(
          content::RenderFrameHost::LifecycleState::kPendingDeletion)) {
    return api::extension_types::DocumentLifecycle::kPendingDeletion;
  }
  return api::extension_types::DocumentLifecycle::kNone;
}

api::extension_types::DocumentLifecycle
ExtensionApiFrameIdMap::GetDocumentLifecycle(
    content::NavigationHandle* navigation_handle) {
  if (content::RenderFrameHost* parent_or_outer_document =
          navigation_handle->GetParentFrameOrOuterDocument()) {
    return GetDocumentLifecycle(parent_or_outer_document);
  }
  if (navigation_handle->IsInPrerenderedMainFrame()) {
    return api::extension_types::DocumentLifecycle::kPrerender;
  } else if (navigation_handle->IsInPrimaryMainFrame()) {
    return api::extension_types::DocumentLifecycle::kActive;
  }
  return api::extension_types::DocumentLifecycle::kNone;
}

void ExtensionApiFrameIdMap::OnRenderFrameDeleted(
    content::RenderFrameHost* render_frame_host) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(render_frame_host);

  const content::GlobalRenderFrameHostId key(render_frame_host->GetGlobalId());
  // TODO(http://crbug.com/40431900): This is necessary right now because beacon
  // requests made in window.onunload may start after this has been called.
  // Delay the RemoveFrameData() call, so we will still have the frame data
  // cached when the beacon request comes in.
  deleted_frame_data_map_.insert(
      {key, KeyToValue(render_frame_host, false /* require_live_frame */)});
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(
                     [](ExtensionApiFrameIdMap* self,
                        content::GlobalRenderFrameHostId key) {
                       self->deleted_frame_data_map_.erase(key);
                     },
                     base::Unretained(this), key));
}

ExtensionApiFrameIdMap::ExtensionDocumentUserData::ExtensionDocumentUserData(
    content::RenderFrameHost* render_frame_host)
    : content::DocumentUserData<ExtensionDocumentUserData>(render_frame_host),
      document_id_(DocumentId::Create()),
      context_id_(base::Uuid::GenerateRandomV4()) {
  Get()->document_id_map_[document_id_] = this;
}

ExtensionApiFrameIdMap::ExtensionDocumentUserData::
    ~ExtensionDocumentUserData() {
  Get()->document_id_map_.erase(document_id_);
}

DOCUMENT_USER_DATA_KEY_IMPL(ExtensionApiFrameIdMap::ExtensionDocumentUserData);

}  // namespace extensions
