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

// Implements the Chrome Extensions WebNavigation API.

#include "chrome/browser/extensions/api/web_navigation/web_navigation_api.h"

#include <memory>

#include "base/lazy_instance.h"
#include "chrome/browser/extensions/api/web_navigation/frame_navigation_state.h"
#include "chrome/browser/extensions/api/web_navigation/web_navigation_api_constants.h"
#include "chrome/browser/extensions/api/web_navigation/web_navigation_event_router.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/api/web_navigation.h"
#include "content/public/browser/navigation_details.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/event_router.h"
#include "extensions/browser/extension_api_frame_id_map.h"
#include "extensions/buildflags/buildflags.h"
#include "pdf/buildflags.h"

#if BUILDFLAG(ENABLE_PDF)
#include "components/pdf/common/pdf_util.h"  // nogncheck
#include "pdf/pdf_features.h"
#endif  // BUILDFLAG(ENABLE_PDF)

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace GetFrame = extensions::api::web_navigation::GetFrame;
namespace GetAllFrames = extensions::api::web_navigation::GetAllFrames;

namespace extensions {

namespace web_navigation = api::web_navigation;

// API functions -----------------------------------------------------

ExtensionFunction::ResponseAction WebNavigationGetFrameFunction::Run() {
  std::optional<GetFrame::Params> params = GetFrame::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  int tab_id = api::tabs::TAB_ID_NONE;
  int frame_id = -1;

  content::RenderFrameHost* render_frame_host = nullptr;
  if (params->details.document_id) {
    ExtensionApiFrameIdMap::DocumentId document_id =
        ExtensionApiFrameIdMap::DocumentIdFromString(
            *params->details.document_id);
    if (!document_id) {
      return RespondNow(Error("Invalid documentId."));
    }

    // Note that we will globally find a RenderFrameHost but validate that
    // we are in the right context still as we may be in the wrong profile
    // or in incognito mode.
    render_frame_host =
        ExtensionApiFrameIdMap::Get()->GetRenderFrameHostByDocumentId(
            document_id);

    if (!render_frame_host) {
      return RespondNow(WithArguments(base::Value()));
    }

    content::WebContents* web_contents =
        content::WebContents::FromRenderFrameHost(render_frame_host);
    // We found the RenderFrameHost through a generic lookup so we must test to
    // see if the WebContents is actually in our BrowserContext.
    if (!ExtensionTabUtil::IsWebContentsInContext(
            web_contents, browser_context(), include_incognito_information())) {
      return RespondNow(WithArguments(base::Value()));
    }

    tab_id = ExtensionTabUtil::GetTabId(web_contents);
    frame_id = ExtensionApiFrameIdMap::GetFrameId(render_frame_host);

    // If the provided tab_id and frame_id do not match the calculated ones
    // return.
    if ((params->details.tab_id && *params->details.tab_id != tab_id) ||
        (params->details.frame_id && *params->details.frame_id != frame_id)) {
      return RespondNow(WithArguments(base::Value()));
    }
  } else {
    // If documentId is not provided, tab_id and frame_id must be. Return early
    // if not.
    if (!params->details.tab_id || !params->details.frame_id) {
      return RespondNow(Error(
          "Either documentId or both tabId and frameId must be specified."));
    }

    tab_id = *params->details.tab_id;
    frame_id = *params->details.frame_id;

    content::WebContents* web_contents = nullptr;
    if (!ExtensionTabUtil::GetTabById(tab_id, browser_context(),
                                      include_incognito_information(),
                                      &web_contents) ||
        !web_contents) {
      return RespondNow(WithArguments(base::Value()));
    }

    render_frame_host = ExtensionApiFrameIdMap::Get()->GetRenderFrameHostById(
        web_contents, frame_id);
  }

  auto* frame_navigation_state =
      render_frame_host
          ? FrameNavigationState::GetForCurrentDocument(render_frame_host)
          : nullptr;
  if (!frame_navigation_state) {
    return RespondNow(WithArguments(base::Value()));
  }

  GURL frame_url = frame_navigation_state->GetUrl();
  if (!FrameNavigationState::IsValidUrl(frame_url)) {
    return RespondNow(WithArguments(base::Value()));
  }

  GetFrame::Results::Details frame_details;
  frame_details.url = frame_url.spec();
  frame_details.error_occurred =
      frame_navigation_state->GetErrorOccurredInFrame();
  frame_details.parent_frame_id =
      ExtensionApiFrameIdMap::GetParentFrameId(render_frame_host);
  frame_details.document_id =
      ExtensionApiFrameIdMap::GetDocumentId(render_frame_host).ToString();
  // Only set the parentDocumentId value if we have a parent.
  if (content::RenderFrameHost* parent_frame_host =
          render_frame_host->GetParentOrOuterDocument()) {
    frame_details.parent_document_id =
        ExtensionApiFrameIdMap::GetDocumentId(parent_frame_host).ToString();
  }
  frame_details.frame_type =
      ExtensionApiFrameIdMap::GetFrameType(render_frame_host);
  frame_details.document_lifecycle =
      ExtensionApiFrameIdMap::GetDocumentLifecycle(render_frame_host);

  return RespondNow(ArgumentList(GetFrame::Results::Create(frame_details)));
}

ExtensionFunction::ResponseAction WebNavigationGetAllFramesFunction::Run() {
  std::optional<GetAllFrames::Params> params =
      GetAllFrames::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);
  int tab_id = params->details.tab_id;

  content::WebContents* web_contents = nullptr;
  if (!ExtensionTabUtil::GetTabById(tab_id, browser_context(),
                                    include_incognito_information(),
                                    &web_contents) ||
      !web_contents) {
    return RespondNow(WithArguments(base::Value()));
  }

  std::vector<GetAllFrames::Results::DetailsType> result_list;

  // We currently do not expose back/forward cached frames in the GetAllFrames
  // API, but we do explicitly include prerendered frames.
  web_contents
      ->ForEachRenderFrameHostWithAction(
          [web_contents,
           &result_list](content::RenderFrameHost* render_frame_host) {
            // Don't expose inner WebContents for the getFrames API.
            if (content::WebContents::FromRenderFrameHost(render_frame_host) !=
                web_contents) {
              return content::RenderFrameHost::FrameIterationAction::
                  kSkipChildren;
            }

#if BUILDFLAG(ENABLE_PDF)
            if (chrome_pdf::features::IsOopifPdfEnabled()) {
              // Don't expose any child frames of the PDF extension frame, such
              // as the PDF content frame.
              content::RenderFrameHost* parent = render_frame_host->GetParent();
              if (parent &&
                  IsPdfExtensionOrigin(parent->GetLastCommittedOrigin())) {
                return content::RenderFrameHost::FrameIterationAction::
                    kSkipChildren;
              }
            }
#endif  // BUILDFLAG(ENABLE_PDF)

            auto* navigation_state =
                FrameNavigationState::GetForCurrentDocument(render_frame_host);

            if (!navigation_state ||
                !FrameNavigationState::IsValidUrl(navigation_state->GetUrl())) {
              return content::RenderFrameHost::FrameIterationAction::kContinue;
            }

            // Skip back/forward cached frames.
            if (render_frame_host->IsInLifecycleState(
                    content::RenderFrameHost::LifecycleState::
                        kInBackForwardCache)) {
              return content::RenderFrameHost::FrameIterationAction::
                  kSkipChildren;
            }

            GetAllFrames::Results::DetailsType frame;
            frame.url = navigation_state->GetUrl().spec();
            frame.frame_id =
                ExtensionApiFrameIdMap::GetFrameId(render_frame_host);
            frame.parent_frame_id =
                ExtensionApiFrameIdMap::GetParentFrameId(render_frame_host);
            frame.document_id =
                ExtensionApiFrameIdMap::GetDocumentId(render_frame_host)
                    .ToString();
            // Only set the parentDocumentId value if we have a parent.
            if (content::RenderFrameHost* parent_frame_host =
                    render_frame_host->GetParentOrOuterDocument()) {
              frame.parent_document_id =
                  ExtensionApiFrameIdMap::GetDocumentId(parent_frame_host)
                      .ToString();
            }
            frame.frame_type =
                ExtensionApiFrameIdMap::GetFrameType(render_frame_host);
            frame.document_lifecycle =
                ExtensionApiFrameIdMap::GetDocumentLifecycle(render_frame_host);
            frame.process_id =
                render_frame_host->GetProcess()->GetDeprecatedID();
            frame.error_occurred = navigation_state->GetErrorOccurredInFrame();
            result_list.push_back(std::move(frame));
            return content::RenderFrameHost::FrameIterationAction::kContinue;
          });

  return RespondNow(ArgumentList(GetAllFrames::Results::Create(result_list)));
}

WebNavigationAPI::WebNavigationAPI(content::BrowserContext* context)
    : browser_context_(context) {
  EventRouter* event_router = EventRouter::Get(browser_context_);
  event_router->RegisterObserver(this,
                                 web_navigation::OnBeforeNavigate::kEventName);
  event_router->RegisterObserver(this, web_navigation::OnCommitted::kEventName);
  event_router->RegisterObserver(this, web_navigation::OnCompleted::kEventName);
  event_router->RegisterObserver(
      this, web_navigation::OnCreatedNavigationTarget::kEventName);
  event_router->RegisterObserver(
      this, web_navigation::OnDOMContentLoaded::kEventName);
  event_router->RegisterObserver(
      this, web_navigation::OnHistoryStateUpdated::kEventName);
  event_router->RegisterObserver(this,
                                 web_navigation::OnErrorOccurred::kEventName);
  event_router->RegisterObserver(
      this, web_navigation::OnReferenceFragmentUpdated::kEventName);
  event_router->RegisterObserver(this,
                                 web_navigation::OnTabReplaced::kEventName);
}

WebNavigationAPI::~WebNavigationAPI() = default;

void WebNavigationAPI::Shutdown() {
  EventRouter::Get(browser_context_)->UnregisterObserver(this);
}

static base::LazyInstance<BrowserContextKeyedAPIFactory<WebNavigationAPI>>::
    DestructorAtExit g_web_navigation_api_factory = LAZY_INSTANCE_INITIALIZER;

// static
BrowserContextKeyedAPIFactory<WebNavigationAPI>*
WebNavigationAPI::GetFactoryInstance() {
  return g_web_navigation_api_factory.Pointer();
}

void WebNavigationAPI::OnListenerAdded(const EventListenerInfo& details) {
  web_navigation_event_router_ = std::make_unique<WebNavigationEventRouter>(
      Profile::FromBrowserContext(browser_context_));
  EventRouter::Get(browser_context_)->UnregisterObserver(this);
}

}  // namespace extensions
