// 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/extensions/extension_keybinding_registry.h"

#include <memory>
#include <utility>

#include "base/values.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/tab_list/tab_list_interface.h"
#include "components/keyed_service/content/browser_context_keyed_service_shutdown_notifier_factory.h"
#include "components/keyed_service/core/keyed_service_shutdown_notifier.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/media_keys_listener_manager.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_registry_factory.h"
#include "extensions/browser/permissions/active_tab_permission_granter.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/command.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/extension_set.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/mojom/context_type.mojom.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ui/ash/media_client/media_client_impl.h"
#endif

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace {

const char kOnCommandEventName[] = "commands.onCommand";

// Monitors for shutdown for both regular and incognito profiles.
class ExtensionKeybindingRegistryShutdownNotifierFactory
    : public BrowserContextKeyedServiceShutdownNotifierFactory {
 public:
  static ExtensionKeybindingRegistryShutdownNotifierFactory* GetInstance() {
    static base::NoDestructor<
        ExtensionKeybindingRegistryShutdownNotifierFactory>
        instance;
    return instance.get();
  }

 private:
  friend class base::NoDestructor<
      ExtensionKeybindingRegistryShutdownNotifierFactory>;

  ExtensionKeybindingRegistryShutdownNotifierFactory()
      : BrowserContextKeyedServiceShutdownNotifierFactory(
            "ExtensionKeybindingRegistryShutdownNotifierFactory") {
    DependsOn(extensions::ExtensionRegistryFactory::GetInstance());
  }
};

}  // namespace

namespace extensions {

ExtensionKeybindingRegistry::ExtensionKeybindingRegistry(
    content::BrowserContext* context,
    TabListInterface* tab_list_interface,
    ExtensionFilter extension_filter)
    : browser_context_(context),
      tab_list_interface_(tab_list_interface),
      extension_filter_(extension_filter),
      shortcut_handling_suspended_(false) {
  extension_registry_observation_.Observe(
      ExtensionRegistry::Get(browser_context_));
  command_service_observation_.Observe(CommandService::Get(browser_context_));
  media_keys_listener_ = ui::MediaKeysListener::Create(
      this, ui::MediaKeysListener::Scope::kFocused);

  shutdown_subscription_ =
      ExtensionKeybindingRegistryShutdownNotifierFactory::GetInstance()
          ->Get(browser_context_)
          ->Subscribe(base::BindRepeating(
              &ExtensionKeybindingRegistry::Shutdown, base::Unretained(this)));
}

ExtensionKeybindingRegistry::~ExtensionKeybindingRegistry() = default;

// static
void ExtensionKeybindingRegistry::EnsureAssociatedFactoryBuilt() {
  ExtensionKeybindingRegistryShutdownNotifierFactory::GetInstance();
}

void ExtensionKeybindingRegistry::Shutdown() {
  extension_registry_observation_.Reset();
  command_service_observation_.Reset();
  media_keys_listener_.reset();
  shutdown_subscription_ = {};
  browser_context_ = nullptr;
}

void ExtensionKeybindingRegistry::SetShortcutHandlingSuspended(bool suspended) {
  shortcut_handling_suspended_ = suspended;
  OnShortcutHandlingSuspended(suspended);
}

void ExtensionKeybindingRegistry::AddExtensionKeybindings(
    const Extension* extension,
    const std::string& command_name) {
  // If in shutdown, do nothing.
  if (!browser_context_) {
    return;
  }
  // This object only handles named commands, not toolbar action execution.
  if (ShouldIgnoreCommand(command_name)) {
    return;
  }

  // Add all the active keybindings. On Desktop, `PopulateCommands()` doesn't
  // add toolbar action executions (and hence returns `false`) because they are
  // handled by the Views for each action.
  ui::CommandMap commands;
  if (!PopulateCommands(extension, &commands)) {
    return;
  }

  for (auto& command : commands) {
    if (!command_name.empty() &&
        (command.second.command_name() != command_name)) {
      continue;
    }
    const ui::Accelerator& accelerator = command.second.accelerator();

    if (!IsAcceleratorRegistered(accelerator)) {
      if (!RegisterAccelerator(accelerator, extension->id(),
                               command.second.command_name())) {
        continue;
      }
    }

    AddEventTarget(accelerator, extension->id(), command.second.command_name());
  }
}

void ExtensionKeybindingRegistry::RemoveExtensionKeybinding(
    const Extension* extension,
    const std::string& command_name) {
  if (!browser_context_) {
    return;
  }
  bool any_media_keys_removed = false;
  auto it = event_targets_.begin();
  while (it != event_targets_.end()) {
    TargetList& target_list = it->second;
    auto target = target_list.begin();
    while (target != target_list.end()) {
      if (target->first == extension->id() &&
          (command_name.empty() || command_name == target->second))
        target = target_list.erase(target);
      else
        target++;
    }

    auto old = it++;
    if (target_list.empty()) {
      // Let each platform-specific implementation get a chance to clean up.
      UnregisterAccelerator(old->first);

      if (old->first.IsMediaKey()) {
        any_media_keys_removed = true;
        if (media_keys_listener_)
          media_keys_listener_->StopWatchingMediaKey(old->first.key_code());
      }

      event_targets_.erase(old);

      // If a specific command_name was requested, it has now been deleted so no
      // further work is required.
      if (!command_name.empty())
        break;
    }
  }

  // If we're no longer listening to any media keys, tell the browser that
  // it can start handling media keys.
  if (any_media_keys_removed && !IsListeningToAnyMediaKeys()) {
    if (content::MediaKeysListenerManager::
            IsMediaKeysListenerManagerEnabled()) {
      content::MediaKeysListenerManager* media_keys_listener_manager =
          content::MediaKeysListenerManager::GetInstance();
      DCHECK(media_keys_listener_manager);

      media_keys_listener_manager->EnableInternalMediaKeyHandling();
    } else {
#if BUILDFLAG(IS_CHROMEOS)
      MediaClientImpl::Get()->DisableCustomMediaKeyHandler(browser_context_,
                                                           this);
#endif
    }
  }
}

bool ExtensionKeybindingRegistry::ShouldIgnoreCommand(
    const std::string& command) const {
  return Command::IsActionRelatedCommand(command);
}

void ExtensionKeybindingRegistry::Init() {
  ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context_);
  if (!registry)
    return;  // ExtensionRegistry can be null during testing.

  for (const scoped_refptr<const Extension>& extension :
       registry->enabled_extensions()) {
    if (ExtensionMatchesFilter(extension.get()))
      AddExtensionKeybindings(extension.get(), std::string());
  }
}


bool ExtensionKeybindingRegistry::NotifyEventTargets(
    const ui::Accelerator& accelerator) {
  return ExecuteCommands(accelerator, std::string());
}

void ExtensionKeybindingRegistry::CommandExecuted(
    const ExtensionId& extension_id,
    const std::string& command) {
  if (!browser_context_) {
    return;
  }
  const Extension* extension = ExtensionRegistry::Get(browser_context_)
                                   ->enabled_extensions()
                                   .GetByID(extension_id);
  if (!extension)
    return;

  base::ListValue args;
  args.Append(command);

  base::Value tab_value;
  if (tab_list_interface_) {
    auto* active_tab = tab_list_interface_->GetActiveTab();
    content::WebContents* web_contents =
        active_tab ? active_tab->GetContents() : nullptr;
    // Grant before sending the event so that the permission is granted before
    // the extension acts on the command. NOTE: The Global Commands handler does
    // not set the TabListInterface as it deals only with named commands (not
    // page/browser actions that are associated with the current page directly).
    ActiveTabPermissionGranter* granter =
        web_contents ? ActiveTabPermissionGranter::FromWebContents(web_contents)
                     : nullptr;
    if (granter) {
      granter->GrantIfRequested(extension);
    }

    if (web_contents) {
      // The action APIs (browserAction, pageAction, action) are only available
      // to privileged extension contexts. As such, we deterministically know
      // that the right context type here is privileged.
      constexpr mojom::ContextType context_type =
          mojom::ContextType::kPrivilegedExtension;
      ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
          ExtensionTabUtil::GetScrubTabBehavior(extension, context_type,
                                                web_contents);
      tab_value = base::Value(ExtensionTabUtil::CreateTabObject(
                                  web_contents, scrub_tab_behavior, extension)
                                  .ToValue());
    }
  }

  args.Append(std::move(tab_value));

  auto event =
      std::make_unique<Event>(events::COMMANDS_ON_COMMAND, kOnCommandEventName,
                              std::move(args), browser_context_);
  event->user_gesture = EventRouter::UserGestureState::kEnabled;
  EventRouter::Get(browser_context_)
      ->DispatchEventToExtension(extension_id, std::move(event));
}

bool ExtensionKeybindingRegistry::IsAcceleratorRegistered(
    const ui::Accelerator& accelerator) const {
  return event_targets_.find(accelerator) != event_targets_.end();
}

void ExtensionKeybindingRegistry::AddEventTarget(
    const ui::Accelerator& accelerator,
    const ExtensionId& extension_id,
    const std::string& command_name) {
  event_targets_[accelerator].push_back(
      std::make_pair(extension_id, command_name));
  // Shortcuts except media keys have only one target in the list. See comment
  // about |event_targets_|.
  if (!accelerator.IsMediaKey()) {
    DCHECK_EQ(1u, event_targets_[accelerator].size());
  } else {
    if (media_keys_listener_)
      media_keys_listener_->StartWatchingMediaKey(accelerator.key_code());

    // Tell the browser that it should not handle media keys, since we're going
    // to handle them.
    if (content::MediaKeysListenerManager::
            IsMediaKeysListenerManagerEnabled()) {
      content::MediaKeysListenerManager* media_keys_listener_manager =
          content::MediaKeysListenerManager::GetInstance();
      DCHECK(media_keys_listener_manager);

      media_keys_listener_manager->DisableInternalMediaKeyHandling();
    } else {
#if BUILDFLAG(IS_CHROMEOS)
      MediaClientImpl::Get()->EnableCustomMediaKeyHandler(browser_context_,
                                                          this);
#endif
    }
  }
}

bool ExtensionKeybindingRegistry::GetFirstTarget(
    const ui::Accelerator& accelerator,
    ExtensionId* extension_id,
    std::string* command_name) const {
  auto targets = event_targets_.find(accelerator);
  if (targets == event_targets_.end())
    return false;

  DCHECK(!targets->second.empty());
  auto first_target = targets->second.begin();
  *extension_id = first_target->first;
  *command_name = first_target->second;
  return true;
}

bool ExtensionKeybindingRegistry::IsEventTargetsEmpty() const {
  return event_targets_.empty();
}

void ExtensionKeybindingRegistry::OnExtensionLoaded(
    content::BrowserContext* browser_context,
    const Extension* extension) {
  if (!browser_context_) {
    return;
  }
  if (ExtensionMatchesFilter(extension))
    AddExtensionKeybindings(extension, std::string());
}

void ExtensionKeybindingRegistry::OnExtensionUnloaded(
    content::BrowserContext* browser_context,
    const Extension* extension,
    UnloadedExtensionReason reason) {
  if (!browser_context_) {
    return;
  }
  if (ExtensionMatchesFilter(extension))
    RemoveExtensionKeybinding(extension, std::string());
}

void ExtensionKeybindingRegistry::OnExtensionCommandAdded(
    const ExtensionId& extension_id,
    const std::string& command_name) {
  if (!browser_context_) {
    return;
  }
  const Extension* extension = ExtensionRegistry::Get(browser_context_)
                                   ->enabled_extensions()
                                   .GetByID(extension_id);
  // During install and uninstall the extension won't be found. We'll catch
  // those events above, with the OnExtension[Unloaded|Loaded], so we ignore
  // this event.
  if (!extension || !ExtensionMatchesFilter(extension))
    return;

  // Component extensions trigger OnExtensionLoaded() for extension
  // installs as well as loads. This can cause adding of multiple key
  // targets.
  if (extension->location() == mojom::ManifestLocation::kComponent)
    return;

  AddExtensionKeybindings(extension, command_name);
}

void ExtensionKeybindingRegistry::OnExtensionCommandRemoved(
    const ExtensionId& extension_id,
    const std::string& command_name) {
  if (!browser_context_) {
    return;
  }
  const Extension* extension = ExtensionRegistry::Get(browser_context_)
                                   ->enabled_extensions()
                                   .GetByID(extension_id);
  // During install and uninstall the extension won't be found. We'll catch
  // those events above, with the OnExtension[Unloaded|Loaded], so we ignore
  // this event.
  if (!extension || !ExtensionMatchesFilter(extension))
    return;

  RemoveExtensionKeybinding(extension, command_name);
}

void ExtensionKeybindingRegistry::OnCommandServiceDestroying() {
  command_service_observation_.Reset();
}

void ExtensionKeybindingRegistry::OnMediaKeysAccelerator(
    const ui::Accelerator& accelerator) {
  NotifyEventTargets(accelerator);
}

bool ExtensionKeybindingRegistry::ExtensionMatchesFilter(
    const Extension* extension) {
  switch (extension_filter_) {
    case ALL_EXTENSIONS:
      return true;
    case PLATFORM_APPS_ONLY:
      return extension->is_platform_app();
    default:
      NOTREACHED();
  }
}

bool ExtensionKeybindingRegistry::ExecuteCommands(
    const ui::Accelerator& accelerator,
    const ExtensionId& extension_id) {
  if (!browser_context_) {
    return false;
  }
  auto targets = event_targets_.find(accelerator);
  if (targets == event_targets_.end() || targets->second.empty())
    return false;

  bool executed = false;
  for (TargetList::const_iterator it = targets->second.begin();
       it != targets->second.end(); it++) {
    if (!EventRouter::Get(browser_context_)
             ->ExtensionHasEventListener(it->first, kOnCommandEventName)) {
      continue;
    }

    if (extension_id.empty() || it->first == extension_id) {
      CommandExecuted(it->first, it->second);
      executed = true;
    }
  }

  return executed;
}

bool ExtensionKeybindingRegistry::IsListeningToAnyMediaKeys() const {
  for (const auto& accelerator_target : event_targets_) {
    if (accelerator_target.first.IsMediaKey()) {
      return true;
    }
  }
  return false;
}

}  // namespace extensions
