// 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.

#include "chrome/browser/plugins/plugin_info_host_impl.h"

#include <stddef.h>

#include <algorithm>
#include <memory>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/no_destructor.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/plugins/chrome_plugin_service_filter.h"
#include "chrome/browser/plugins/plugin_metadata.h"
#include "chrome/browser/plugins/plugin_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/buildflags.h"
#include "chrome/common/chrome_content_client.h"
#include "chrome/common/plugin.mojom.h"
#include "components/component_updater/component_updater_service.h"
#include "components/content_settings/core/browser/content_settings_utils.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/keyed_service/content/browser_context_keyed_service_shutdown_notifier_factory.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/plugin_service.h"
#include "content/public/browser/plugin_service_filter.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/common/content_constants.h"
#include "extensions/buildflags/buildflags.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "url/gurl.h"
#include "url/origin.h"

#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "components/guest_view/browser/guest_view_base.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/guest_view/web_view/web_view_renderer_state.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_handlers/webview_info.h"
#endif

using content::PluginService;
using content::WebPluginInfo;

namespace {

class PluginInfoHostImplShutdownNotifierFactory
    : public BrowserContextKeyedServiceShutdownNotifierFactory {
 public:
  static PluginInfoHostImplShutdownNotifierFactory* GetInstance() {
    static base::NoDestructor<PluginInfoHostImplShutdownNotifierFactory>
        instance;
    return instance.get();
  }

  PluginInfoHostImplShutdownNotifierFactory(
      const PluginInfoHostImplShutdownNotifierFactory&) = delete;
  PluginInfoHostImplShutdownNotifierFactory& operator=(
      const PluginInfoHostImplShutdownNotifierFactory&) = delete;

 private:
  friend base::NoDestructor<PluginInfoHostImplShutdownNotifierFactory>;

  PluginInfoHostImplShutdownNotifierFactory()
      : BrowserContextKeyedServiceShutdownNotifierFactory(
            "PluginInfoHostImpl") {}

  ~PluginInfoHostImplShutdownNotifierFactory() override = default;
};

std::unique_ptr<PluginMetadata> GetPluginMetadata(const WebPluginInfo& plugin) {
  // Gets the base name of the file path as the identifier.
  std::string identifier = plugin.path.BaseName().AsUTF8Unsafe();

  // Gets the plugin group name as the plugin name if it is not empty, or the
  // filename without extension if the name is empty.
  std::u16string group_name = plugin.name;
  if (group_name.empty()) {
    group_name = plugin.path.BaseName().RemoveExtension().AsUTF16Unsafe();
  } else {
    // Remove any unwanted locale direction characters from the group name.
    // For extension-based plugins, the plugin name is derived from the
    // extension name, and `extensions::Extension::LoadName()` may add locale
    // direction characters to the extension name.
    base::i18n::UnadjustStringForLocaleDirection(&group_name);
  }

  // Treat plugins as requiring authorization by default.
  PluginMetadata::SecurityStatus security_status =
      PluginMetadata::SECURITY_STATUS_REQUIRES_AUTHORIZATION;

  // Handle the PDF plugins specially.
  if (plugin.path.value() == ChromeContentClient::kPDFExtensionPluginPath) {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
    identifier = "google-chrome-pdf";
#else
    identifier = "chromium-pdf";
#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
    security_status = PluginMetadata::SECURITY_STATUS_FULLY_TRUSTED;
  } else if (plugin.path.value() ==
             ChromeContentClient::kPDFInternalPluginPath) {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
    identifier = "google-chrome-pdf-plugin";
#else
    identifier = "chromium-pdf-plugin";
#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
    security_status = PluginMetadata::SECURITY_STATUS_FULLY_TRUSTED;
  }

  return std::make_unique<PluginMetadata>(identifier, group_name,
                                          security_status);
}

#if BUILDFLAG(ENABLE_EXTENSIONS)
// Returns whether a request from a plugin to load `resource` from a renderer
// with `rfh_token` is a request for an internal resource by an app listed in
// `accessible_resources` in its manifest.
bool IsPluginLoadingAccessibleResourceInWebView(
    extensions::ExtensionRegistry* extension_registry,
    const content::GlobalRenderFrameHostToken& rfh_token,
    const GURL& resource) {
  extensions::WebViewRendererState* renderer_state =
      extensions::WebViewRendererState::GetInstance();
  std::string partition_id;
  if (!renderer_state->IsGuest(rfh_token.child_id) ||
      !renderer_state->GetPartitionID(rfh_token.child_id, &partition_id)) {
    return false;
  }

  const std::string extension_id = resource.GetHost();
  const extensions::Extension* extension =
      extension_registry->enabled_extensions().GetByID(extension_id);
  if (!extension || !extensions::WebviewInfo::IsResourceWebviewAccessible(
                        extension, partition_id, resource.GetPath())) {
    return false;
  }

  // Make sure the renderer making the request actually belongs to the
  // same extension.
  std::string owner_extension;
  return renderer_state->GetOwnerInfo(rfh_token.child_id, nullptr,
                                      &owner_extension) &&
         owner_extension == extension_id;
}
#endif  // BUILDFLAG(ENABLE_EXTENSIONS)

}  // namespace

PluginInfoHostImpl::Context::Context(
    content::GlobalRenderFrameHostToken rfh_token,
    Profile* profile)
    : rfh_token_(rfh_token),
#if BUILDFLAG(ENABLE_EXTENSIONS)
      extension_registry_(extensions::ExtensionRegistry::Get(profile)),
#endif
      host_content_settings_map_(
          HostContentSettingsMapFactory::GetForProfile(profile)) {
}

PluginInfoHostImpl::Context::~Context() = default;

PluginInfoHostImpl::PluginInfoHostImpl(
    content::GlobalRenderFrameHostToken rfh_token,
    Profile* profile)
    : context_(rfh_token, profile) {
  shutdown_subscription_ =
      PluginInfoHostImplShutdownNotifierFactory::GetInstance()
          ->Get(profile)
          ->Subscribe(base::BindRepeating(
              &PluginInfoHostImpl::ShutdownOnUIThread, base::Unretained(this)));
}

void PluginInfoHostImpl::ShutdownOnUIThread() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  shutdown_subscription_ = {};
}

PluginInfoHostImpl::~PluginInfoHostImpl() = default;

void PluginInfoHostImpl::GetPluginInfo(const GURL& url,
                                       const std::string& mime_type,
                                       GetPluginInfoCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  // Refresh plugins.
  PluginService::GetInstance()->GetPlugins();
  chrome::mojom::PluginInfoPtr output = chrome::mojom::PluginInfo::New();
  auto* rfh = content::RenderFrameHost::FromFrameToken(context_.rfh_token());
  if (!rfh) {
    std::move(callback).Run(std::move(output));
    return;
  }

  // This also fills in |actual_mime_type|.
  std::unique_ptr<PluginMetadata> plugin_metadata;
  if (context_.FindEnabledPlugin(url, mime_type, &output->status,
                                 &output->plugin, &output->actual_mime_type,
                                 &plugin_metadata)) {
    // TODO(crbug.com/40164563): Simplify this once PDF is the only "plugin."
    context_.DecidePluginStatus(
        url, rfh->GetMainFrame()->GetLastCommittedOrigin(), output->plugin,
        plugin_metadata->security_status(), plugin_metadata->identifier(),
        &output->status);
  }

  if (plugin_metadata) {
    output->group_identifier = plugin_metadata->identifier();
    output->group_name = plugin_metadata->name();
  }

  context_.MaybeGrantAccess(output->status, output->plugin.path);

  std::move(callback).Run(std::move(output));
}

void PluginInfoHostImpl::Context::DecidePluginStatus(
    const GURL& url,
    const url::Origin& main_frame_origin,
    const WebPluginInfo& plugin,
    PluginMetadata::SecurityStatus security_status,
    const std::string& plugin_identifier,
    chrome::mojom::PluginStatus* status) const {
  if (security_status == PluginMetadata::SECURITY_STATUS_FULLY_TRUSTED) {
    *status = chrome::mojom::PluginStatus::kAllowed;
    return;
  }

  ContentSetting plugin_setting = CONTENT_SETTING_DEFAULT;
  bool is_managed = false;
  // Check plugin content settings. The primary URL is the top origin URL and
  // the secondary URL is the plugin URL.
  PluginUtils::GetPluginContentSetting(
      host_content_settings_map_, plugin, main_frame_origin, url,
      plugin_identifier, &plugin_setting, &is_managed);
  // GetPluginContentSetting() reads the JS setting, which only allows these 2
  // values.
  CHECK(plugin_setting == CONTENT_SETTING_ALLOW ||
        plugin_setting == CONTENT_SETTING_BLOCK);

#if BUILDFLAG(ENABLE_EXTENSIONS)
  // If an app has explicitly made internal resources available by listing them
  // in |accessible_resources| in the manifest, then allow them to be loaded by
  // plugins inside a guest-view.
  if (url.SchemeIs(extensions::kExtensionScheme) && !is_managed &&
      plugin_setting == CONTENT_SETTING_BLOCK &&
      IsPluginLoadingAccessibleResourceInWebView(extension_registry_,
                                                 rfh_token_, url)) {
    plugin_setting = CONTENT_SETTING_ALLOW;
  }
#endif  // BUILDFLAG(ENABLE_EXTENSIONS)

  if (plugin_setting == CONTENT_SETTING_ALLOW) {
    *status = chrome::mojom::PluginStatus::kAllowed;
  } else {
    *status = is_managed ? chrome::mojom::PluginStatus::kBlockedByPolicy
                         : chrome::mojom::PluginStatus::kBlocked;
  }

#if BUILDFLAG(ENABLE_EXTENSIONS)
  // Allow an embedder of <webview> to block a plugin from being loaded inside
  // the guest. In order to do this, set the status to 'Unauthorized' here,
  // and update the status as appropriate depending on the response from the
  // embedder.
  if (*status == chrome::mojom::PluginStatus::kAllowed ||
      *status == chrome::mojom::PluginStatus::kBlocked) {
    if (extensions::WebViewRendererState::GetInstance()->IsGuest(
            rfh_token_.child_id)) {
      *status = chrome::mojom::PluginStatus::kUnauthorized;
    }
  }
#endif
}

bool PluginInfoHostImpl::Context::FindEnabledPlugin(
    const GURL& url,
    const std::string& mime_type,
    chrome::mojom::PluginStatus* status,
    WebPluginInfo* plugin,
    std::string* actual_mime_type,
    std::unique_ptr<PluginMetadata>* plugin_metadata) const {
  *status = chrome::mojom::PluginStatus::kAllowed;

  std::vector<WebPluginInfo> matching_plugins;
  std::vector<std::string> mime_types;
  PluginService::GetInstance()->GetPluginInfoArray(
      url, mime_type, &matching_plugins, &mime_types);
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
  std::erase_if(matching_plugins, [&](const WebPluginInfo& info) {
    return info.path.value() == ChromeContentClient::kNotPresent;
  });
#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
  if (matching_plugins.empty()) {
    *status = chrome::mojom::PluginStatus::kNotFound;
    return false;
  }

  content::PluginServiceFilter* filter =
      PluginService::GetInstance()->GetFilter();
  auto* rfh = content::RenderFrameHost::FromFrameToken(rfh_token_);
  content::BrowserContext* browser_context =
      rfh ? rfh->GetBrowserContext() : nullptr;
  size_t i = 0;
  for (; i < matching_plugins.size(); ++i) {
    if (!filter ||
        filter->IsPluginAvailable(browser_context, matching_plugins[i])) {
      break;
    }
  }

  // If we broke out of the loop, we have found an enabled plugin.
  bool enabled = i < matching_plugins.size();
  if (!enabled) {
    // Otherwise, we only found disabled plugins, so we take the first one.
    i = 0;
    *status = chrome::mojom::PluginStatus::kDisabled;
  }

  *plugin = matching_plugins[i];
  *actual_mime_type = mime_types[i];
  if (plugin_metadata)
    *plugin_metadata = GetPluginMetadata(*plugin);

  return enabled;
}

// static
void PluginInfoHostImpl::EnsureFactoryBuilt() {
  PluginInfoHostImplShutdownNotifierFactory::GetInstance();
}

void PluginInfoHostImpl::Context::MaybeGrantAccess(
    chrome::mojom::PluginStatus status,
    const base::FilePath& path) const {
  if (status == chrome::mojom::PluginStatus::kAllowed) {
    ChromePluginServiceFilter::GetInstance()->AuthorizePlugin(
        rfh_token_.child_id, path);
  }
}
