// 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 "chrome/browser/extensions/api/developer_private/inspectable_views_finder.h"

#include <set>

#include "chrome/browser/devtools/devtools_availability_checker.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/api/developer_private.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/extension_host.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/view_type_utils.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/common/manifest_handlers/incognito_info.h"
#include "extensions/common/mojom/view_type.mojom.h"
#include "url/gurl.h"

#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/app_window_registry.h"
#endif  // BUILDFLAG(ENABLE_EXTENSIONS)

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

InspectableViewsFinder::InspectableViewsFinder(Profile* profile)
    : profile_(profile) {
}

InspectableViewsFinder::~InspectableViewsFinder() = default;

api::developer_private::ViewType ConvertViewType(const mojom::ViewType type) {
  api::developer_private::ViewType developer_private_type =
      api::developer_private::ViewType::kNone;
  switch (type) {
    case mojom::ViewType::kAppWindow:
      developer_private_type = api::developer_private::ViewType::kAppWindow;
      break;
    case mojom::ViewType::kBackgroundContents:
      developer_private_type =
          api::developer_private::ViewType::kBackgroundContents;
      break;
    case mojom::ViewType::kComponent:
      developer_private_type = api::developer_private::ViewType::kComponent;
      break;
    case mojom::ViewType::kExtensionBackgroundPage:
      developer_private_type =
          api::developer_private::ViewType::kExtensionBackgroundPage;
      break;
    case mojom::ViewType::kExtensionGuest:
      developer_private_type =
          api::developer_private::ViewType::kExtensionGuest;
      break;
    case mojom::ViewType::kExtensionPopup:
      developer_private_type =
          api::developer_private::ViewType::kExtensionPopup;
      break;
    case mojom::ViewType::kTabContents:
      developer_private_type = api::developer_private::ViewType::kTabContents;
      break;
    case mojom::ViewType::kOffscreenDocument:
      developer_private_type =
          api::developer_private::ViewType::kOffscreenDocument;
      break;
    case mojom::ViewType::kExtensionSidePanel:
      developer_private_type =
          api::developer_private::ViewType::kExtensionSidePanel;
      break;
    case mojom::ViewType::kDeveloperTools:
      developer_private_type =
          api::developer_private::ViewType::kDeveloperTools;
      break;
    case mojom::ViewType::kInvalid:
      NOTREACHED();
  }
  DCHECK(developer_private_type != api::developer_private::ViewType::kNone);
  return developer_private_type;
}

// static
InspectableViewsFinder::View InspectableViewsFinder::ConstructView(
    const GURL& url,
    int render_process_id,
    int render_view_id,
    bool incognito,
    bool is_iframe,
    api::developer_private::ViewType type) {
  api::developer_private::ExtensionView view;
  view.url = url.spec();
  view.render_process_id = render_process_id;
  // NOTE(devlin): This is called "render_view_id" in the api for legacy
  // reasons, but it's not a high priority to change.
  view.render_view_id = render_view_id;
  view.incognito = incognito;
  view.is_iframe = is_iframe;
  view.type = type;
  return view;
}

InspectableViewsFinder::ViewList InspectableViewsFinder::GetViewsForExtension(
    const Extension& extension,
    bool is_enabled) {
  ViewList result;
  if (!IsInspectionAllowed(profile_, &extension)) {
    return result;
  }
  GetViewsForExtensionForProfile(
      extension, profile_, is_enabled, false, &result);
  if (profile_->HasPrimaryOTRProfile()) {
    GetViewsForExtensionForProfile(
        extension, profile_->GetPrimaryOTRProfile(/*create_if_needed=*/true),
        is_enabled, true, &result);
  }

  return result;
}

void InspectableViewsFinder::GetViewsForExtensionForProfile(
    const Extension& extension,
    Profile* profile,
    bool is_enabled,
    bool is_incognito,
    ViewList* result) {
  ProcessManager* process_manager = ProcessManager::Get(profile);
  // Get the extension process's active views.
  GetViewsForExtensionProcess(extension,
                              process_manager,
                              is_incognito,
                              result);
  // Get app window views, if not incognito.
  if (!is_incognito) {
    GetAppWindowViewsForExtension(extension, result);
  }
  // Include a link to start the lazy background page, if applicable.
  bool include_lazy_background = true;
  // Don't include the lazy background page for incognito if the extension isn't
  // enabled incognito or doesn't have a separate background page in incognito.
  if (is_incognito &&
      (!util::IsIncognitoEnabled(extension.id(), profile) ||
       !IncognitoInfo::IsSplitMode(&extension))) {
    include_lazy_background = false;
  }

  // Get inactive backgrounds.
  if (!include_lazy_background || !is_enabled) {
    return;
  }
  if (BackgroundInfo::HasLazyBackgroundPage(&extension) &&
      !process_manager->GetBackgroundHostForExtension(extension.id())) {
    result->push_back(ConstructView(
        BackgroundInfo::GetBackgroundURL(&extension), -1, -1, is_incognito,
        false, api::developer_private::ViewType::kExtensionBackgroundPage));
  }
  if (BackgroundInfo::IsServiceWorkerBased(&extension) &&
      process_manager->GetServiceWorkersForExtension(extension.id()).empty()) {
    result->push_back(ConstructView(
        BackgroundInfo::GetBackgroundServiceWorkerScriptURL(&extension), -1, -1,
        is_incognito, false,
        api::developer_private::ViewType::kExtensionServiceWorkerBackground));
  }
}

void InspectableViewsFinder::GetViewsForExtensionProcess(
    const Extension& extension,
    ProcessManager* process_manager,
    bool is_incognito,
    ViewList* result) {
  const std::set<content::RenderFrameHost*>& hosts =
      process_manager->GetRenderFrameHostsForExtension(extension.id());
  for (content::RenderFrameHost* host : hosts) {
    content::WebContents* web_contents =
        content::WebContents::FromRenderFrameHost(host);
    mojom::ViewType host_type = GetViewType(web_contents);
    if (host_type == mojom::ViewType::kInvalid ||
        host_type == mojom::ViewType::kExtensionPopup ||
        host_type == mojom::ViewType::kAppWindow) {
      continue;
    }

    GURL url = web_contents->GetURL();
    // If this is a background page that just opened, there might not be a
    // committed (or visible) url yet. In this case, use the initial url.
    if (url.is_empty()) {
      ExtensionHost* extension_host =
          process_manager->GetBackgroundHostForRenderFrameHost(host);
      if (extension_host) {
        url = extension_host->initial_url();
      }
    }

    content::RenderProcessHost* process = host->GetProcess();
    result->push_back(ConstructView(
        url, process->GetDeprecatedID(), host->GetRoutingID(), is_incognito,
        !host->IsInPrimaryMainFrame(), ConvertViewType(host_type)));
  }

  std::vector<WorkerId> service_worker_ids =
      process_manager->GetServiceWorkersForExtension(extension.id());
  for (const WorkerId& service_worker_id : service_worker_ids) {
    result->push_back(ConstructView(
        BackgroundInfo::GetBackgroundServiceWorkerScriptURL(&extension),
        service_worker_id.render_process_id.GetUnsafeValue(),
        /*render_view_id=*/-1, is_incognito,
        /*is_iframe=*/false,
        api::developer_private::ViewType::kExtensionServiceWorkerBackground));
  }
}

void InspectableViewsFinder::GetAppWindowViewsForExtension(
    const Extension& extension,
    ViewList* result) {
#if BUILDFLAG(ENABLE_EXTENSIONS)
  AppWindowRegistry* registry = AppWindowRegistry::Get(profile_);
  if (!registry) {
    return;
  }

  AppWindowRegistry::AppWindowList windows =
      registry->GetAppWindowsForApp(extension.id());

  for (const AppWindow* window : windows) {
    content::WebContents* web_contents = window->web_contents();

    // If the window just opened, there might not be a committed (or visible)
    // url yet. In this case, use the initial url.
    GURL url = web_contents->GetLastCommittedURL();
    if (url.is_empty()) {
      url = window->initial_url();
    }

    content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
    result->push_back(
        ConstructView(url, main_frame->GetProcess()->GetDeprecatedID(),
                      main_frame->GetRoutingID(), false, false,
                      ConvertViewType(GetViewType(web_contents))));
  }
#endif  // BUILDFLAG(ENABLE_EXTENSIONS)
}

}  // namespace extensions
