// 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 "extensions/browser/api/device_permissions_prompt.h"

#include <memory>
#include <utility>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/i18n/message_formatter.h"
#include "base/memory/scoped_refptr.h"
#include "base/no_destructor.h"
#include "base/scoped_observation.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/device_service.h"
#include "extensions/browser/api/device_permissions_manager.h"
#include "extensions/browser/api/usb/usb_device_manager.h"
#include "extensions/common/extension.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/device/public/cpp/hid/hid_device_filter.h"
#include "services/device/public/cpp/hid/hid_usage_and_page.h"
#include "services/device/public/cpp/usb/usb_utils.h"
#include "services/device/public/mojom/usb_enumeration_options.mojom.h"
#include "ui/base/l10n/l10n_util.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chromeos/dbus/permission_broker/permission_broker_client.h"  // nogncheck
#endif  // BUILDFLAG(IS_CHROMEOS)

using device::HidDeviceFilter;
using device::mojom::UsbDeviceFilterPtr;

namespace extensions {

namespace {

class UsbDeviceInfo : public DevicePermissionsPrompt::Prompt::DeviceInfo {
 public:
  explicit UsbDeviceInfo(device::mojom::UsbDeviceInfoPtr device)
      : device_(std::move(device)) {
    name_ = DevicePermissionsManager::GetPermissionMessage(
        device_->vendor_id, device_->product_id,
        device_->manufacturer_name.value_or(std::u16string()),
        device_->product_name.value_or(std::u16string()),
        std::u16string(),  // Serial number is displayed separately.
        true);
    serial_number_ =
        device_->serial_number ? *(device_->serial_number) : std::u16string();
  }

  ~UsbDeviceInfo() override {}

  device::mojom::UsbDeviceInfoPtr& device() { return device_; }

 private:
  device::mojom::UsbDeviceInfoPtr device_;
};

class UsbDevicePermissionsPrompt : public DevicePermissionsPrompt::Prompt,
                                   public UsbDeviceManager::Observer {
 public:
  UsbDevicePermissionsPrompt(
      const Extension* extension,
      content::BrowserContext* context,
      bool multiple,
      std::vector<UsbDeviceFilterPtr> filters,
      DevicePermissionsPrompt::UsbDevicesCallback callback)
      : Prompt(extension, context, multiple),
        filters_(std::move(filters)),
        callback_(std::move(callback)) {}

 private:
  ~UsbDevicePermissionsPrompt() override { manager_observation_.Reset(); }

  // DevicePermissionsPrompt::Prompt implementation:
  void SetObserver(
      DevicePermissionsPrompt::Prompt::Observer* observer) override {
    DevicePermissionsPrompt::Prompt::SetObserver(observer);

    if (observer) {
      auto* device_manager = UsbDeviceManager::Get(browser_context());
      if (device_manager &&
          !manager_observation_.IsObservingSource(device_manager)) {
        device_manager->GetDevices(base::BindOnce(
            &UsbDevicePermissionsPrompt::OnDevicesEnumerated, this));
        manager_observation_.Observe(device_manager);
      }
    }
  }

  void Dismissed() override {
    DevicePermissionsManager* permissions_manager =
        DevicePermissionsManager::Get(browser_context());
    std::vector<device::mojom::UsbDeviceInfoPtr> devices;
    for (const auto& device : devices_) {
      if (device->granted()) {
        UsbDeviceInfo* usb_device = static_cast<UsbDeviceInfo*>(device.get());
        if (permissions_manager) {
          DCHECK(usb_device->device());
          permissions_manager->AllowUsbDevice(extension()->id(),
                                              *usb_device->device());
        }
        devices.push_back(std::move(usb_device->device()));
      }
    }
    DCHECK(multiple() || devices.size() <= 1);
    std::move(callback_).Run(std::move(devices));
  }

  // extensions::UsbDeviceManager::Observer implementation
  void OnDeviceAdded(const device::mojom::UsbDeviceInfo& device) override {
    MaybeAddDevice(device, /*initial_enumeration=*/false);
  }

  // extensions::UsbDeviceManager::Observer implementation
  void OnDeviceRemoved(const device::mojom::UsbDeviceInfo& device) override {
    for (auto it = devices_.begin(); it != devices_.end(); ++it) {
      UsbDeviceInfo* entry = static_cast<UsbDeviceInfo*>((*it).get());
      if (entry->device()->guid == device.guid) {
        size_t index = it - devices_.begin();
        std::u16string device_name = (*it)->name();
        devices_.erase(it);
        if (observer())
          observer()->OnDeviceRemoved(index, device_name);
        return;
      }
    }
  }

  void OnDevicesEnumerated(
      std::vector<device::mojom::UsbDeviceInfoPtr> devices) {
    for (const auto& device : devices) {
      MaybeAddDevice(*device, /*initial_enumeration=*/true);
    }
  }

  void MaybeAddDevice(const device::mojom::UsbDeviceInfo& device,
                      bool initial_enumeration) {
    if (!device::UsbDeviceFilterMatchesAny(filters_, device))
      return;

    if (initial_enumeration)
      remaining_initial_devices_++;

    auto device_info = std::make_unique<UsbDeviceInfo>(device.Clone());
#if BUILDFLAG(IS_CHROMEOS)
    auto* device_manager = UsbDeviceManager::Get(browser_context());
    DCHECK(device_manager);
    device_manager->CheckAccess(
        device.guid,
        base::BindOnce(&UsbDevicePermissionsPrompt::AddCheckedDevice, this,
                       std::move(device_info), initial_enumeration));
#else
    AddCheckedDevice(std::move(device_info), initial_enumeration,
                     /*allowed=*/true);
#endif  // BUILDFLAG(IS_CHROMEOS)
  }

  void AddCheckedDevice(std::unique_ptr<UsbDeviceInfo> device_info,
                        bool initial_enumeration,
                        bool allowed) {
    if (allowed)
      AddDevice(std::move(device_info));

    if (initial_enumeration && --remaining_initial_devices_ == 0 &&
        observer()) {
      observer()->OnDevicesInitialized();
    }
  }

  std::vector<UsbDeviceFilterPtr> filters_;
  size_t remaining_initial_devices_ = 0;
  DevicePermissionsPrompt::UsbDevicesCallback callback_;
  base::ScopedObservation<UsbDeviceManager, UsbDeviceManager::Observer>
      manager_observation_{this};
};

class HidDeviceInfo : public DevicePermissionsPrompt::Prompt::DeviceInfo {
 public:
  explicit HidDeviceInfo(device::mojom::HidDeviceInfoPtr device)
      : device_(std::move(device)) {
    name_ = DevicePermissionsManager::GetPermissionMessage(
        device_->vendor_id, device_->product_id,
        std::u16string(),  // HID devices include manufacturer in product name.
        base::UTF8ToUTF16(device_->product_name),
        std::u16string(),  // Serial number is displayed separately.
        false);
    serial_number_ = base::UTF8ToUTF16(device_->serial_number);
  }

  ~HidDeviceInfo() override {}

  device::mojom::HidDeviceInfoPtr& device() { return device_; }

 private:
  device::mojom::HidDeviceInfoPtr device_;
};

DevicePermissionsPrompt::HidManagerBinder& GetHidManagerBinderOverride() {
  static base::NoDestructor<DevicePermissionsPrompt::HidManagerBinder> binder;
  return *binder;
}

class HidDevicePermissionsPrompt : public DevicePermissionsPrompt::Prompt,
                                   public device::mojom::HidManagerClient {
 public:
  HidDevicePermissionsPrompt(
      const Extension* extension,
      content::BrowserContext* context,
      bool multiple,
      const std::vector<HidDeviceFilter>& filters,
      DevicePermissionsPrompt::HidDevicesCallback callback)
      : Prompt(extension, context, multiple),
        initialized_(false),
        filters_(filters),
        callback_(std::move(callback)) {}

 private:
  ~HidDevicePermissionsPrompt() override {}

  // DevicePermissionsPrompt::Prompt implementation:
  void SetObserver(
      DevicePermissionsPrompt::Prompt::Observer* observer) override {
    DevicePermissionsPrompt::Prompt::SetObserver(observer);

    if (observer)
      LazyInitialize();
  }

  void LazyInitialize() {
    if (initialized_) {
      return;
    }

    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    auto receiver = hid_manager_.BindNewPipeAndPassReceiver();
    const auto& binder = GetHidManagerBinderOverride();
    if (binder)
      binder.Run(std::move(receiver));
    else
      content::GetDeviceService().BindHidManager(std::move(receiver));

    hid_manager_->GetDevicesAndSetClient(
        receiver_.BindNewEndpointAndPassRemote(),
        base::BindOnce(&HidDevicePermissionsPrompt::OnDevicesEnumerated, this));

    initialized_ = true;
  }

  void Dismissed() override {
    DevicePermissionsManager* permissions_manager =
        DevicePermissionsManager::Get(browser_context());
    std::vector<device::mojom::HidDeviceInfoPtr> devices;
    for (const auto& device : devices_) {
      if (device->granted()) {
        HidDeviceInfo* hid_device = static_cast<HidDeviceInfo*>(device.get());
        if (permissions_manager) {
          DCHECK(hid_device->device());
          permissions_manager->AllowHidDevice(extension()->id(),
                                              *(hid_device->device()));
        }
        devices.push_back(std::move(hid_device->device()));
      }
    }
    DCHECK(multiple() || devices.size() <= 1);
    std::move(callback_).Run(std::move(devices));
  }

  // device::mojom::HidManagerClient implementation:
  void DeviceAdded(device::mojom::HidDeviceInfoPtr device) override {
    MaybeAddDevice(std::move(device), /*initial_enumeration=*/false);
  }

  void DeviceRemoved(device::mojom::HidDeviceInfoPtr device) override {
    for (auto it = devices_.begin(); it != devices_.end(); ++it) {
      HidDeviceInfo* entry = static_cast<HidDeviceInfo*>((*it).get());
      if (entry->device()->guid == device->guid) {
        size_t index = it - devices_.begin();
        std::u16string device_name = (*it)->name();
        devices_.erase(it);
        if (observer())
          observer()->OnDeviceRemoved(index, device_name);
        return;
      }
    }
  }

  void DeviceChanged(device::mojom::HidDeviceInfoPtr device) override {
    for (const auto& device_info : devices_) {
      auto* hid_device_info =
          reinterpret_cast<HidDeviceInfo*>(device_info.get());
      if (hid_device_info->device()->guid == device->guid) {
        // The device is already present in |devices_|. Update its device
        // information.
        hid_device_info->device() = std::move(device);
        return;
      }
    }

    // The device was not previously added to |devices_|, possibly due to
    // filters or protected collections. Try adding it again.
    MaybeAddDevice(std::move(device), /*initial_enumeration=*/false);
  }

  void OnDevicesEnumerated(
      std::vector<device::mojom::HidDeviceInfoPtr> devices) {
    for (auto& device : devices)
      MaybeAddDevice(std::move(device), /*initial_enumeration=*/true);
  }

  bool HasUnprotectedCollections(const device::mojom::HidDeviceInfo& device) {
    for (const auto& collection : device.collections) {
      if (!device::IsAlwaysProtected(*collection->usage)) {
        return true;
      }
    }
    return false;
  }

  void MaybeAddDevice(device::mojom::HidDeviceInfoPtr device,
                      bool initial_enumeration) {
    if (!HasUnprotectedCollections(*device) ||
        (!filters_.empty() &&
         !HidDeviceFilter::MatchesAny(*device, filters_))) {
      return;
    }

    if (initial_enumeration)
      remaining_initial_devices_++;

    auto device_info = std::make_unique<HidDeviceInfo>(std::move(device));
    // TODO(huangs): Enable this for Lacros (crbug.com/1217124).
#if BUILDFLAG(IS_CHROMEOS_ASH)
    chromeos::PermissionBrokerClient::Get()->CheckPathAccess(
        device_info.get()->device()->device_node,
        base::BindOnce(&HidDevicePermissionsPrompt::AddCheckedDevice, this,
                       std::move(device_info), initial_enumeration));
#else
    AddCheckedDevice(std::move(device_info), initial_enumeration,
                     /*allowed=*/true);
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
  }

  void AddCheckedDevice(std::unique_ptr<HidDeviceInfo> device_info,
                        bool initial_enumeration,
                        bool allowed) {
    if (allowed)
      AddDevice(std::move(device_info));

    if (initial_enumeration && --remaining_initial_devices_ == 0 &&
        observer()) {
      observer()->OnDevicesInitialized();
    }
  }

  bool initialized_;
  std::vector<HidDeviceFilter> filters_;
  size_t remaining_initial_devices_ = 0;
  mojo::Remote<device::mojom::HidManager> hid_manager_;
  DevicePermissionsPrompt::HidDevicesCallback callback_;
  mojo::AssociatedReceiver<device::mojom::HidManagerClient> receiver_{this};
};

}  // namespace

DevicePermissionsPrompt::Prompt::DeviceInfo::DeviceInfo() {
}

DevicePermissionsPrompt::Prompt::DeviceInfo::~DeviceInfo() {
}

DevicePermissionsPrompt::Prompt::Observer::~Observer() {
}

DevicePermissionsPrompt::Prompt::Prompt(const Extension* extension,
                                        content::BrowserContext* context,
                                        bool multiple)
    : extension_(extension), browser_context_(context), multiple_(multiple) {
}

void DevicePermissionsPrompt::Prompt::SetObserver(Observer* observer) {
  observer_ = observer;
}

std::u16string DevicePermissionsPrompt::Prompt::GetDeviceName(
    size_t index) const {
  DCHECK_LT(index, devices_.size());
  return devices_[index]->name();
}

std::u16string DevicePermissionsPrompt::Prompt::GetDeviceSerialNumber(
    size_t index) const {
  DCHECK_LT(index, devices_.size());
  return devices_[index]->serial_number();
}

void DevicePermissionsPrompt::Prompt::GrantDevicePermission(size_t index) {
  DCHECK_LT(index, devices_.size());
  devices_[index]->set_granted();
}

DevicePermissionsPrompt::Prompt::~Prompt() {
}

void DevicePermissionsPrompt::Prompt::AddDevice(
    std::unique_ptr<DeviceInfo> device) {
  std::u16string device_name = device->name();
  devices_.push_back(std::move(device));
  if (observer_)
    observer_->OnDeviceAdded(devices_.size() - 1, device_name);
}

DevicePermissionsPrompt::DevicePermissionsPrompt(
    content::WebContents* web_contents)
    : web_contents_(web_contents) {
}

DevicePermissionsPrompt::~DevicePermissionsPrompt() {
}

void DevicePermissionsPrompt::AskForUsbDevices(
    const Extension* extension,
    content::BrowserContext* context,
    bool multiple,
    std::vector<UsbDeviceFilterPtr> filters,
    UsbDevicesCallback callback) {
  prompt_ = base::MakeRefCounted<UsbDevicePermissionsPrompt>(
      extension, context, multiple, std::move(filters), std::move(callback));
  ShowDialog();
}

void DevicePermissionsPrompt::AskForHidDevices(
    const Extension* extension,
    content::BrowserContext* context,
    bool multiple,
    const std::vector<HidDeviceFilter>& filters,
    HidDevicesCallback callback) {
  prompt_ = base::MakeRefCounted<HidDevicePermissionsPrompt>(
      extension, context, multiple, filters, std::move(callback));
  ShowDialog();
}

// static
scoped_refptr<DevicePermissionsPrompt::Prompt>
DevicePermissionsPrompt::CreateHidPromptForTest(const Extension* extension,
                                                bool multiple) {
  return base::MakeRefCounted<HidDevicePermissionsPrompt>(
      extension, nullptr, multiple, std::vector<HidDeviceFilter>(),
      base::DoNothing());
}

// static
scoped_refptr<DevicePermissionsPrompt::Prompt>
DevicePermissionsPrompt::CreateUsbPromptForTest(const Extension* extension,
                                                bool multiple) {
  return base::MakeRefCounted<UsbDevicePermissionsPrompt>(
      extension, nullptr, multiple, std::vector<UsbDeviceFilterPtr>(),
      base::DoNothing());
}

// static
void DevicePermissionsPrompt::OverrideHidManagerBinderForTesting(
    HidManagerBinder binder) {
  GetHidManagerBinderOverride() = std::move(binder);
}

}  // namespace extensions