// 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 "chrome/browser/devtools/chrome_devtools_manager_delegate.h"

#include <memory>
#include <utility>

#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "cef/libcef/features/features.h"
#include "chrome/browser/browser_features.h"
#include "chrome/browser/devtools/chrome_devtools_session.h"
#include "chrome/browser/devtools/device/android_device_manager.h"
#include "chrome/browser/devtools/device/tcp_device_provider.h"
#include "chrome/browser/devtools/devtools_availability_checker.h"
#include "chrome/browser/devtools/devtools_browser_context_manager.h"
#include "chrome/browser/devtools/devtools_connection_dialog.h"
#include "chrome/browser/devtools/devtools_remote_server_infobar_delegate.h"
#include "chrome/browser/devtools/devtools_window.h"
#include "chrome/browser/devtools/protocol/target_handler.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/lifetime/application_lifetime.h"
#include "chrome/browser/policy/developer_tools_policy_handler.h"
#include "chrome/browser/policy/profile_policy_connector.h"
#include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#include "chrome/browser/ui/webui_browser/webui_browser.h"
#include "chrome/browser/web_applications/proto/web_app_install_state.pb.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_filter.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/browser/web_applications/web_app_tab_helper.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/grit/browser_resources.h"
#include "components/guest_view/browser/guest_view_base.h"
#include "components/keep_alive_registry/keep_alive_types.h"
#include "components/keep_alive_registry/scoped_keep_alive.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/devtools_agent_host_client_channel.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 "content/public/common/content_switches.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/view_type_utils.h"
#include "extensions/common/manifest.h"
#include "extensions/common/mojom/view_type.mojom.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/switches.h"
#include "ui/views/controls/webview/webview.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/constants/ash_switches.h"
#include "chromeos/constants/chromeos_features.h"
#endif

#if BUILDFLAG(ENABLE_CEF)
#include "cef/libcef/browser/chrome/extensions/chrome_extension_util.h"
#endif

using content::DevToolsAgentHost;

const char ChromeDevToolsManagerDelegate::kTypeApp[] = "app";
const char ChromeDevToolsManagerDelegate::kTypeBackgroundPage[] =
    "background_page";
const char ChromeDevToolsManagerDelegate::kTypePage[] = "page";

namespace {

std::optional<std::string> GetIsolatedWebAppNameAndVersion(
    content::WebContents* web_contents) {
  const webapps::AppId* app_id =
      web_app::WebAppTabHelper::GetAppId(web_contents);
  if (!app_id) {
    return std::nullopt;
  }
  const web_app::WebAppProvider* provider =
      web_app::WebAppProvider::GetForWebContents(web_contents);
  if (!provider) {
    return std::nullopt;
  }
  // In this case we will not modify any data and reading stale data is
  // fine, since the app will already be installed and open in the case
  // it needs to be checked in DevTools.
  const web_app::WebAppRegistrar& registrar = provider->registrar_unsafe();
  const web_app::WebApp* web_app = registrar.GetAppById(*app_id);

  if (web_app &&
      registrar.AppMatches(*app_id, web_app::WebAppFilter::IsIsolatedApp())) {
    // Version is a key part of IWA so should be displayed in inspect tool
    return base::StrCat({registrar.GetAppShortName(*app_id), " (",
                         web_app->isolation_data()->version().GetString(),
                         ")"});
  }

  return std::nullopt;
}

bool IsIsolatedWebApp(content::WebContents* web_contents) {
  return GetIsolatedWebAppNameAndVersion(web_contents).has_value();
}

bool GetExtensionInfo(content::WebContents* wc,
                      std::string* name,
                      std::string* type) {
  auto* process_manager =
      extensions::ProcessManager::Get(wc->GetBrowserContext());
  if (!process_manager) {
    return false;
  }
  const extensions::Extension* extension =
      process_manager->GetExtensionForWebContents(wc);
  if (!extension)
    return false;
  extensions::ExtensionHost* extension_host =
      process_manager->GetBackgroundHostForExtension(extension->id());
  if (extension_host && extension_host->host_contents() == wc) {
    *name = extension->name();
    *type = ChromeDevToolsManagerDelegate::kTypeBackgroundPage;
    return true;
  }
  if (extension->is_hosted_app() || extension->is_legacy_packaged_app() ||
      extension->is_platform_app()) {
    *name = extension->name();
    *type = ChromeDevToolsManagerDelegate::kTypeApp;
    return true;
  }

  auto view_type = extensions::GetViewType(wc);
  if (view_type == extensions::mojom::ViewType::kExtensionPopup ||
      view_type == extensions::mojom::ViewType::kExtensionSidePanel) {
    // Note that we are intentionally not setting name here, so that we can
    // construct a name based on the URL or page title in
    // RenderFrameDevToolsAgentHost::GetTitle()
    *type = ChromeDevToolsManagerDelegate::kTypePage;
    return true;
  }

  if (view_type == extensions::mojom::ViewType::kOffscreenDocument) {
    // Note that we are intentionally not setting name here, so that we can
    // construct a name based on the URL or page title in
    // RenderFrameDevToolsAgentHost::GetTitle()
    //
    // Use `kTypeBackgroundPage` for offscreen doc until devtools frontend is
    // updated to support a new `offscreen_document` target type. Otherwise,
    // DOM is not inspectable.
    *type = ChromeDevToolsManagerDelegate::kTypeBackgroundPage;
    return true;
  }

  // Set type to other for extensions if not matched previously.
  *type = DevToolsAgentHost::kTypeOther;
  return true;
}

ChromeDevToolsManagerDelegate* g_instance;

}  // namespace

// static
ChromeDevToolsManagerDelegate* ChromeDevToolsManagerDelegate::GetInstance() {
  return g_instance;
}

ChromeDevToolsManagerDelegate::ChromeDevToolsManagerDelegate() {
  DCHECK(!g_instance);
  g_instance = this;

#if !BUILDFLAG(IS_CHROMEOS)
  // Only create and hold keep alive for automation test for non ChromeOS.
  // ChromeOS automation test (aka tast) manages chrome instance via session
  // manager daemon. The extra keep alive is not needed and makes ChromeOS
  // not able to shutdown chrome properly. See https://crbug.com/1174627.
  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
  if ((command_line->HasSwitch(switches::kNoStartupWindow) ||
       command_line->HasSwitch(switches::kHeadless)) &&
      (command_line->HasSwitch(switches::kRemoteDebuggingPipe) ||
       command_line->HasSwitch(switches::kRemoteDebuggingPort))) {
    // If running without a startup window with remote debugging,
    // we are controlled entirely by the automation process.
    // Keep the application running until explicit close through DevTools
    // protocol.
    keep_alive_ = std::make_unique<ScopedKeepAlive>(
        KeepAliveOrigin::REMOTE_DEBUGGING, KeepAliveRestartOption::DISABLED);

    // Also keep the initial profile alive so that TargetHandler::CreateTarget()
    // can retrieve it without risking disk access even when all pages are
    // closed. Keep-a-living the very first loaded profile looks like a
    // reasonable option.
    if (Profile* profile = ProfileManager::GetLastUsedProfile()) {
      if (profile->IsOffTheRecord()) {
        profile = profile->GetOriginalProfile();
      }
      profile_keep_alive_ = std::make_unique<ScopedProfileKeepAlive>(
          profile, ProfileKeepAliveOrigin::kRemoteDebugging);
    }
  }
#endif  // !BUILDFLAG(IS_CHROMEOS)
}

ChromeDevToolsManagerDelegate::~ChromeDevToolsManagerDelegate() {
  DCHECK(g_instance == this);
  g_instance = nullptr;
  if (infobar_) {
    infobar_->Close();
  }
}

void ChromeDevToolsManagerDelegate::Inspect(
    content::DevToolsAgentHost* agent_host) {
  DevToolsWindow::OpenDevToolsWindow(agent_host, nullptr,
                                     DevToolsOpenedByAction::kInspectLink);
}

scoped_refptr<content::DevToolsAgentHost>
ChromeDevToolsManagerDelegate::GetDevToolsAgentHost(
    content::DevToolsAgentHost* agent_host) {
  scoped_refptr<content::DevToolsAgentHost> tab_agent_host(
      content::DevToolsAgentHost::GetForTab(agent_host->GetWebContents()));
  DevToolsWindow* window =
      DevToolsWindow::FindDevToolsWindow(tab_agent_host.get());
  if (!window) {
    return nullptr;
  }
  return DevToolsAgentHost::GetOrCreateFor(window->GetDevToolsWebContents());
}

scoped_refptr<content::DevToolsAgentHost>
ChromeDevToolsManagerDelegate::OpenDevTools(
    content::DevToolsAgentHost* agent_host,
    const content::DevToolsManagerDelegate::DevToolsOptions& devtools_options) {
  scoped_refptr<content::DevToolsAgentHost> tab_agent_host(
      content::DevToolsAgentHost::GetOrCreateForTab(
          agent_host->GetWebContents()));
  DevToolsWindow::OpenDevToolsWindow(tab_agent_host, nullptr,
                                     DevToolsOpenedByAction::kUnknown,
                                     devtools_options);
  DevToolsWindow* window =
      DevToolsWindow::FindDevToolsWindow(tab_agent_host.get());
  if (!window) {
    return nullptr;
  }

  return DevToolsAgentHost::GetOrCreateFor(window->GetDevToolsWebContents());
}

void ChromeDevToolsManagerDelegate::Activate(
    content::DevToolsAgentHost* agent_host) {
  auto* web_contents = agent_host->GetWebContents();
  if (!web_contents) {
    return;
  }

  // Brings the tab to foreground. We need to do this in case the devtools
  // window is undocked and this is being called from another tab that is in
  // the foreground.
  web_contents->GetDelegate()->ActivateContents(web_contents);

  // Brings a undocked devtools window to the foreground.
  DevToolsWindow* devtools_window =
      DevToolsWindow::GetInstanceForInspectedWebContents(
          agent_host->GetWebContents());
  if (!devtools_window) {
    return;
  }
  devtools_window->ActivateWindow();
}

void ChromeDevToolsManagerDelegate::HandleCommand(
    content::DevToolsAgentHostClientChannel* channel,
    base::span<const uint8_t> message,
    NotHandledCallback callback) {
  auto it = sessions_.find(channel);
  if (it == sessions_.end()) {
    std::move(callback).Run(message);
    // This should not happen, but happens. NOTREACHED tries to get
    // a repro in some test.
    NOTREACHED();
  }
  it->second->HandleCommand(message, std::move(callback));
}

std::string ChromeDevToolsManagerDelegate::GetTargetType(
    content::WebContents* web_contents) {
  if (webui_browser::IsBrowserUIWebContents(web_contents)) {
    return DevToolsAgentHost::kTypeBrowserUI;
  }

  if (IsIsolatedWebApp(web_contents)) {
    return ChromeDevToolsManagerDelegate::kTypeApp;
  }

  if (tabs::TabInterface::MaybeGetFromContents(web_contents)) {
    return DevToolsAgentHost::kTypePage;
  }

  std::string extension_name;
  std::string extension_type;
  if (GetExtensionInfo(web_contents, &extension_name, &extension_type)) {
    return extension_type;
  }

  if (views::WebView::IsWebViewContents(web_contents)) {
    return DevToolsAgentHost::kTypePage;
  }

#if BUILDFLAG(ENABLE_CEF)
  if (cef::IsAlloyContents(web_contents, /*primary_only=*/true)) {
    return DevToolsAgentHost::kTypePage;
  }
#endif

  return DevToolsAgentHost::kTypeOther;
}

std::optional<bool> ChromeDevToolsManagerDelegate::ShouldReportAsTabTarget(
    content::WebContents* web_contents) {
  if (webui_browser::IsBrowserUIWebContents(web_contents)) {
    // Return false for browser UI so its WebContents is not reported as Tab.
    // Browser UI is not a Tab and can not be interacted as a Tab. Reporting
    // browser UI WebContents as Tab target will confuse tools such as test
    // drivers and cause them to fail.
    return false;
  }

  if (tabs::TabInterface::MaybeGetFromContents(web_contents)) {
    return true;
  }

  return std::nullopt;
}

std::string ChromeDevToolsManagerDelegate::GetTargetTitle(
    content::WebContents* web_contents) {
  if (auto iwa_name_version = GetIsolatedWebAppNameAndVersion(web_contents)) {
    return *iwa_name_version;
  }

  std::string extension_name;
  std::string extension_type;
  if (!GetExtensionInfo(web_contents, &extension_name, &extension_type)) {
    return std::string();
  }

  return extension_name;
}

bool ChromeDevToolsManagerDelegate::AllowInspectingRenderFrameHost(
    content::RenderFrameHost* rfh) {
  Profile* profile =
      Profile::FromBrowserContext(rfh->GetProcess()->GetBrowserContext());
  return IsInspectionAllowed(profile,
                             content::WebContents::FromRenderFrameHost(rfh));
}

void ChromeDevToolsManagerDelegate::ClientAttached(
    content::DevToolsAgentHostClientChannel* channel) {
  DCHECK(sessions_.find(channel) == sessions_.end());
  sessions_.emplace(channel, std::make_unique<ChromeDevToolsSession>(channel));
}

void ChromeDevToolsManagerDelegate::ClientDetached(
    content::DevToolsAgentHostClientChannel* channel) {
  sessions_.erase(channel);
}

scoped_refptr<DevToolsAgentHost> ChromeDevToolsManagerDelegate::CreateNewTarget(
    const GURL& url,
    DevToolsManagerDelegate::TargetType target_type,
    bool new_window) {
  NavigateParams params(ProfileManager::GetLastUsedProfile(), url,
                        ui::PAGE_TRANSITION_AUTO_TOPLEVEL);
  params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
  Navigate(&params);
  if (!params.navigated_or_inserted_contents)
    return nullptr;
  return target_type == DevToolsManagerDelegate::kTab
             ? DevToolsAgentHost::GetOrCreateForTab(
                   params.navigated_or_inserted_contents)
             : DevToolsAgentHost::GetOrCreateFor(
                   params.navigated_or_inserted_contents);
}

std::vector<content::BrowserContext*>
ChromeDevToolsManagerDelegate::GetBrowserContexts() {
  return DevToolsBrowserContextManager::GetInstance().GetBrowserContexts();
}

content::BrowserContext*
ChromeDevToolsManagerDelegate::GetDefaultBrowserContext() {
  return DevToolsBrowserContextManager::GetInstance()
      .GetDefaultBrowserContext();
}

content::BrowserContext* ChromeDevToolsManagerDelegate::CreateBrowserContext() {
  return DevToolsBrowserContextManager::GetInstance().CreateBrowserContext();
}

void ChromeDevToolsManagerDelegate::DisposeBrowserContext(
    content::BrowserContext* context,
    DisposeCallback callback) {
  DevToolsBrowserContextManager::GetInstance().DisposeBrowserContext(
      context, std::move(callback));
}

bool ChromeDevToolsManagerDelegate::HasBundledFrontendResources() {
  return true;
}

void ChromeDevToolsManagerDelegate::DevicesAvailable(
    const DevToolsDeviceDiscovery::CompleteDevices& devices) {
  DevToolsAgentHost::List remote_targets;
  for (const auto& complete : devices) {
    for (const auto& browser : complete.second->browsers()) {
      for (const auto& page : browser->pages())
        remote_targets.push_back(page->CreateTarget());
    }
  }
  remote_agent_hosts_.swap(remote_targets);
}

void ChromeDevToolsManagerDelegate::UpdateDeviceDiscovery() {
  RemoteLocations remote_locations;
  for (const auto& it : sessions_) {
    TargetHandler* target_handler = it.second->target_handler();
    if (!target_handler)
      continue;
    RemoteLocations& locations = target_handler->remote_locations();
    remote_locations.insert(locations.begin(), locations.end());
  }

  if (remote_locations == remote_locations_) {
    return;
  }

  if (remote_locations.empty()) {
    device_discovery_.reset();
    remote_agent_hosts_.clear();
  } else {
    if (!device_manager_)
      device_manager_ = AndroidDeviceManager::Create();

    AndroidDeviceManager::DeviceProviders providers;
    providers.push_back(new TCPDeviceProvider(remote_locations));
    device_manager_->SetDeviceProviders(providers);

    device_discovery_ = std::make_unique<DevToolsDeviceDiscovery>(
        device_manager_.get(),
        base::BindRepeating(&ChromeDevToolsManagerDelegate::DevicesAvailable,
                            base::Unretained(this)));
  }
  remote_locations_.swap(remote_locations);
}

void ChromeDevToolsManagerDelegate::AcceptDebugging(AcceptCallback callback) {
  DevToolsConnectionDialog::Show(chrome::FindLastActive(), std::move(callback));
}

void ChromeDevToolsManagerDelegate::SetActiveWebSocketConnections(
    size_t count) {
  if (count == 0 && infobar_) {
    // We need to reset the pointer to the infobar before closing it because
    // closing the infobar deletes it.
    auto* infobar = infobar_.get();
    infobar_ = nullptr;
    infobar->Close();
  } else if (count > 0 && !infobar_) {
    auto delegate = std::make_unique<DevToolsRemoteServerInfobarDelegate>(
        chrome::FindLastActive());
    delegate->AddObserver(this);
    infobar_ = GlobalConfirmInfoBar::Show(std::move(delegate));
  }
}

void ChromeDevToolsManagerDelegate::OnAccept() {
  infobar_ = nullptr;
}

void ChromeDevToolsManagerDelegate::OnDismiss() {
  infobar_ = nullptr;
}

void ChromeDevToolsManagerDelegate::ResetAndroidDeviceManagerForTesting() {
  device_manager_.reset();

  // We also need |device_discovery_| to go away because there may be a pending
  // task using a raw pointer to the DeviceManager we just deleted.
  device_discovery_.reset();
}

// static
void ChromeDevToolsManagerDelegate::CloseBrowserSoon() {
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce([]() {
        // Do not keep the application running anymore, we got an explicit
        // request to close.
        AllowBrowserToClose();
        chrome::ExitIgnoreUnloadHandlers();
      }));
}

// static
void ChromeDevToolsManagerDelegate::AllowBrowserToClose() {
  if (auto* instance = GetInstance()) {
    instance->keep_alive_.reset();
  }
}