// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/permissions/contexts/bluetooth_chooser_context.h"

#include <memory>
#include <optional>
#include <utility>
#include <vector>

#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/permissions/permissions_client.h"
#include "content/public/browser/browser_context.h"
#include "device/bluetooth/bluetooth_device.h"
#include "device/bluetooth/public/cpp/bluetooth_uuid.h"
#include "third_party/blink/public/mojom/bluetooth/web_bluetooth.mojom.h"
#include "url/origin.h"

using blink::WebBluetoothDeviceId;
using device::BluetoothUUID;

namespace permissions {

namespace {

// The Bluetooth device permission objects are dictionary type base::Values. The
// object contains keys for the device address, device name, services that can
// be accessed, and the generated web bluetooth device ID. Since base::Value
// does not have a set type, the services key contains another dictionary type
// base::Value object where each key is a UUID for a service and the value is a
// boolean that is never used. This allows for service permissions to be queried
// quickly and for new service permissions to added without duplicating existing
// service permissions. The following is an example of how a device permission
// is formatted using JSON notation:
// {
//   "device-address": "00:00:00:00:00:00",
//   "name": "Wireless Device",
//   "services": {
//     "0xabcd": "true",
//     "0x1234": "true",
//   },
//   "web-bluetooth-device-id": "4ik7W0WVaGFY6zXxJqdAKw==",
// }
constexpr char kDeviceAddressKey[] = "device-address";
constexpr char kDeviceNameKey[] = "name";
constexpr char kManufacturerDataKey[] = "manufacturer-data";
constexpr char kServicesKey[] = "services";
constexpr char kWebBluetoothDeviceIdKey[] = "web-bluetooth-device-id";

// The Web Bluetooth API spec states that when the user selects a device to
// pair with the origin, the origin is allowed to access any service listed in
// |options->filters| and |options->optional_services|.
// https://webbluetoothcg.github.io/web-bluetooth/#bluetooth
void AddUnionOfServicesTo(
    const blink::mojom::WebBluetoothRequestDeviceOptions* options,
    base::DictValue& permission_object) {
  if (!options)
    return;

  DCHECK(!!permission_object.FindDict(kServicesKey));
  auto& services_dict = *permission_object.FindDict(kServicesKey);
  if (options->filters) {
    for (const blink::mojom::WebBluetoothLeScanFilterPtr& filter :
         options->filters.value()) {
      if (!filter->services)
        continue;

      for (const BluetoothUUID& uuid : filter->services.value())
        services_dict.Set(uuid.canonical_value(), /*val=*/true);
    }
  }

  for (const BluetoothUUID& uuid : options->optional_services)
    services_dict.Set(uuid.canonical_value(), /*val=*/true);
}

void AddManufacturerDataTo(
    const blink::mojom::WebBluetoothRequestDeviceOptions* options,
    base::DictValue& permission_object) {
  if (!options || options->optional_manufacturer_data.empty())
    return;

  CHECK(permission_object.FindDict(kManufacturerDataKey));
  auto& manufacturer_data_dict =
      *permission_object.FindDict(kManufacturerDataKey);
  for (uint16_t manufacturer_code : options->optional_manufacturer_data) {
    manufacturer_data_dict.Set(base::NumberToString(manufacturer_code),
                               /*value=*/true);
  }
}

}  // namespace

BluetoothChooserContext::BluetoothChooserContext(
    content::BrowserContext* browser_context)
    : ObjectPermissionContextBase(
          ContentSettingsType::BLUETOOTH_GUARD,
          ContentSettingsType::BLUETOOTH_CHOOSER_DATA,
          PermissionsClient::Get()->GetSettingsMap(browser_context)) {}

BluetoothChooserContext::~BluetoothChooserContext() = default;

// static
base::DictValue BluetoothChooserContext::DeviceInfoToValue(
    const device::BluetoothDevice* device,
    const blink::mojom::WebBluetoothRequestDeviceOptions* options,
    const WebBluetoothDeviceId& device_id) {
  base::DictValue device_value;
  device_value.Set(kDeviceAddressKey, device->GetAddress());
  device_value.Set(kWebBluetoothDeviceIdKey, device_id.str());
  device_value.Set(kDeviceNameKey, device->GetNameForDisplay());

  device_value.Set(kServicesKey, base::DictValue());
  AddUnionOfServicesTo(options, device_value);

  device_value.Set(kManufacturerDataKey, base::DictValue());
  AddManufacturerDataTo(options, device_value);

  return device_value;
}

WebBluetoothDeviceId BluetoothChooserContext::GetWebBluetoothDeviceId(
    const url::Origin& origin,
    const std::string& device_address) {
  const std::vector<std::unique_ptr<Object>> object_list =
      GetGrantedObjects(origin);
  for (const auto& object : object_list) {
    const base::DictValue& device = object->value;
    DCHECK(IsValidObject(device));

    if (device_address == *device.FindString(kDeviceAddressKey)) {
      return WebBluetoothDeviceId(*device.FindString(kWebBluetoothDeviceIdKey));
    }
  }

  // Check if the device has been assigned an ID through an LE scan.
  auto scanned_devices_it = scanned_devices_.find(origin);
  if (scanned_devices_it == scanned_devices_.end())
    return {};

  auto address_to_id_it = scanned_devices_it->second.find(device_address);
  if (address_to_id_it != scanned_devices_it->second.end())
    return address_to_id_it->second;
  return {};
}

std::string BluetoothChooserContext::GetDeviceAddress(
    const url::Origin& origin,
    const WebBluetoothDeviceId& device_id) {
  std::optional<base::DictValue> device = FindDeviceObject(origin, device_id);
  if (device)
    return *device->FindString(kDeviceAddressKey);

  // Check if the device ID corresponds to a device detected via an LE scan.
  auto scanned_devices_it = scanned_devices_.find(origin);
  if (scanned_devices_it == scanned_devices_.end())
    return std::string();

  for (const auto& entry : scanned_devices_it->second) {
    if (entry.second == device_id)
      return entry.first;
  }
  return std::string();
}

WebBluetoothDeviceId BluetoothChooserContext::AddScannedDevice(
    const url::Origin& origin,
    const std::string& device_address) {
  // Check if a WebBluetoothDeviceId already exists for the device with
  // |device_address| for the current origin.
  const auto granted_id = GetWebBluetoothDeviceId(origin, device_address);
  if (granted_id.IsValid())
    return granted_id;

  DeviceAddressToIdMap& address_to_id_map = scanned_devices_[origin];
  auto scanned_id = WebBluetoothDeviceId::Create();
  address_to_id_map.emplace(device_address, scanned_id);
  return scanned_id;
}

WebBluetoothDeviceId BluetoothChooserContext::GrantServiceAccessPermission(
    const url::Origin& origin,
    const device::BluetoothDevice* device,
    const blink::mojom::WebBluetoothRequestDeviceOptions* options) {
  // If |origin| already has permission to access the device with
  // |device_address|, update the allowed GATT services by performing a union of
  // |services|.
  const std::vector<std::unique_ptr<Object>> object_list =
      GetGrantedObjects(origin);
  const std::string& device_address = device->GetAddress();
  for (const auto& object : object_list) {
    const base::DictValue& device_dict = object->value;
    DCHECK(IsValidObject(device_dict));
    if (device_address == *device_dict.FindString(kDeviceAddressKey)) {
      auto new_device_dict = device_dict.Clone();
      WebBluetoothDeviceId device_id(
          *new_device_dict.FindString(kWebBluetoothDeviceIdKey));

      AddUnionOfServicesTo(options, new_device_dict);
      AddManufacturerDataTo(options, new_device_dict);
      UpdateObjectPermission(origin, device_dict, std::move(new_device_dict));
      return device_id;
    }
  }

  // If the device has been detected through the Web Bluetooth Scanning API,
  // grant permission using the WebBluetoothDeviceId generated through that API.
  // Remove the ID from the temporary |scanned_devices_| map to avoid
  // duplication, since the ID will now be stored in HostContentSettingsMap.
  WebBluetoothDeviceId device_id;
  auto scanned_devices_it = scanned_devices_.find(origin);
  if (scanned_devices_it != scanned_devices_.end()) {
    auto& address_to_id_map = scanned_devices_it->second;
    auto address_to_id_it = address_to_id_map.find(device_address);

    if (address_to_id_it != address_to_id_map.end()) {
      device_id = address_to_id_it->second;
      address_to_id_map.erase(address_to_id_it);

      if (scanned_devices_it->second.empty())
        scanned_devices_.erase(scanned_devices_it);
    }
  }

  if (!device_id.IsValid())
    device_id = WebBluetoothDeviceId::Create();

  base::DictValue permission_object =
      DeviceInfoToValue(device, options, device_id);
  GrantObjectPermission(origin, std::move(permission_object));
  return device_id;
}

bool BluetoothChooserContext::HasDevicePermission(
    const url::Origin& origin,
    const WebBluetoothDeviceId& device_id) {
  std::optional<base::DictValue> device = FindDeviceObject(origin, device_id);
  return device.has_value();
}

void BluetoothChooserContext::RevokeDevicePermissionWebInitiated(
    const url::Origin& origin,
    const WebBluetoothDeviceId& device_id) {
  std::optional<base::DictValue> device = FindDeviceObject(origin, device_id);
  if (device.has_value())
    RevokeObjectPermission(origin, std::move(*device));
}

bool BluetoothChooserContext::IsAllowedToAccessAtLeastOneService(
    const url::Origin& origin,
    const WebBluetoothDeviceId& device_id) {
  std::optional<base::DictValue> device = FindDeviceObject(origin, device_id);
  if (!device.has_value())
    return false;
  return !device->FindDict(kServicesKey)->empty();
}

bool BluetoothChooserContext::IsAllowedToAccessService(
    const url::Origin& origin,
    const WebBluetoothDeviceId& device_id,
    const BluetoothUUID& service) {
  std::optional<base::DictValue> device = FindDeviceObject(origin, device_id);
  if (!device.has_value())
    return false;

  const auto& services_dict = *device->FindDict(kServicesKey);
  return !!services_dict.contains(service.canonical_value());
}

bool BluetoothChooserContext::IsAllowedToAccessManufacturerData(
    const url::Origin& origin,
    const WebBluetoothDeviceId& device_id,
    uint16_t manufacturer_code) {
  std::optional<base::DictValue> device = FindDeviceObject(origin, device_id);
  if (!device.has_value())
    return false;

  const auto& manufacturer_data_list = *device->FindDict(kManufacturerDataKey);
  return manufacturer_data_list.contains(
      base::NumberToString(manufacturer_code));
}

// static
WebBluetoothDeviceId BluetoothChooserContext::GetObjectDeviceId(
    const base::DictValue& object) {
  std::string device_id_str = *object.FindString(kWebBluetoothDeviceIdKey);
  return WebBluetoothDeviceId(device_id_str);
}

std::string BluetoothChooserContext::GetKeyForObject(
    const base::DictValue& object) {
  if (!IsValidObject(object))
    return std::string();
  return *(object.FindString(kWebBluetoothDeviceIdKey));
}

bool BluetoothChooserContext::IsValidObject(const base::DictValue& object) {
  return IsValidDict(object);
}

std::u16string BluetoothChooserContext::GetObjectDisplayName(
    const base::DictValue& object) {
  return base::UTF8ToUTF16(*object.FindString(kDeviceNameKey));
}

void BluetoothChooserContext::Shutdown() {
  FlushScheduledSaveSettingsCalls();
  ObjectPermissionContextBase::Shutdown();
}

bool BluetoothChooserContext::IsValidDict(const base::DictValue& dict) {
  return dict.FindString(kDeviceAddressKey) &&
         dict.FindString(kDeviceNameKey) &&
         dict.FindString(kWebBluetoothDeviceIdKey) &&
         WebBluetoothDeviceId::IsValid(
             *dict.FindString(kWebBluetoothDeviceIdKey)) &&
         dict.FindDict(kServicesKey) && dict.FindDict(kManufacturerDataKey);
}

std::optional<base::DictValue> BluetoothChooserContext::FindDeviceObject(
    const url::Origin& origin,
    const blink::WebBluetoothDeviceId& device_id) {
  const std::vector<std::unique_ptr<Object>> object_list =
      GetGrantedObjects(origin);
  for (const auto& object : object_list) {
    base::DictValue device = std::move(object->value);
    DCHECK(IsValidDict(device));

    const WebBluetoothDeviceId web_bluetooth_device_id(
        *device.FindString(kWebBluetoothDeviceIdKey));
    if (device_id == web_bluetooth_device_id)
      return device;
  }
  return std::nullopt;
}

}  // namespace permissions
