// Copyright 2014 The Chromium Authors. All rights reserved.
// 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 <utility>

#include "base/bind.h"
#include "base/i18n/message_formatter.h"
#include "base/scoped_observer.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "device/base/device_client.h"
#include "device/hid/hid_device_filter.h"
#include "device/hid/hid_device_info.h"
#include "device/hid/hid_service.h"
#include "device/usb/usb_device.h"
#include "device/usb/usb_device_filter.h"
#include "device/usb/usb_ids.h"
#include "device/usb/usb_service.h"
#include "extensions/browser/api/device_permissions_manager.h"
#include "extensions/common/extension.h"
#include "extensions/strings/grit/extensions_strings.h"
#include "ui/base/l10n/l10n_util.h"

#if defined(OS_CHROMEOS)
#include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/dbus/permission_broker_client.h"
#include "device/hid/hid_device_info_linux.h"
#endif  // defined(OS_CHROMEOS)

using device::HidDeviceFilter;
using device::HidService;
using device::UsbDevice;
using device::UsbDeviceFilter;
using device::UsbService;

namespace extensions {

namespace {

void NoopHidCallback(const std::vector<scoped_refptr<device::HidDeviceInfo>>&) {
}

void NoopUsbCallback(const std::vector<scoped_refptr<device::UsbDevice>>&) {}

class UsbDeviceInfo : public DevicePermissionsPrompt::Prompt::DeviceInfo {
 public:
  UsbDeviceInfo(scoped_refptr<UsbDevice> device) : device_(device) {
    name_ = DevicePermissionsManager::GetPermissionMessage(
        device->vendor_id(), device->product_id(),
        device->manufacturer_string(), device->product_string(),
        base::string16(),  // Serial number is displayed separately.
        true);
    serial_number_ = device->serial_number();
  }

  ~UsbDeviceInfo() override {}

  const scoped_refptr<UsbDevice>& device() const { return device_; }

 private:
  // TODO(reillyg): Convert this to a weak reference when UsbDevice has a
  // connected flag.
  scoped_refptr<UsbDevice> device_;
};

class UsbDevicePermissionsPrompt : public DevicePermissionsPrompt::Prompt,
                                   public device::UsbService::Observer {
 public:
  UsbDevicePermissionsPrompt(
      const Extension* extension,
      content::BrowserContext* context,
      bool multiple,
      const std::vector<UsbDeviceFilter>& filters,
      const DevicePermissionsPrompt::UsbDevicesCallback& callback)
      : Prompt(extension, context, multiple),
        filters_(filters),
        callback_(callback),
        service_observer_(this) {}

 private:
  ~UsbDevicePermissionsPrompt() override {}

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

    if (observer) {
      UsbService* service = device::DeviceClient::Get()->GetUsbService();
      if (service && !service_observer_.IsObserving(service)) {
        service->GetDevices(
            base::Bind(&UsbDevicePermissionsPrompt::OnDevicesEnumerated, this));
        service_observer_.Add(service);
      }
    }
  }

  base::string16 GetHeading() const override {
    return l10n_util::GetSingleOrMultipleStringUTF16(
        IDS_USB_DEVICE_PERMISSIONS_PROMPT_TITLE, multiple());
  }

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

  // device::UsbService::Observer implementation:
  void OnDeviceAdded(scoped_refptr<UsbDevice> device) override {
    if (!UsbDeviceFilter::MatchesAny(device, filters_))
      return;

    std::unique_ptr<DeviceInfo> device_info(new UsbDeviceInfo(device));
    device->CheckUsbAccess(
        base::Bind(&UsbDevicePermissionsPrompt::AddCheckedDevice, this,
                   base::Passed(&device_info)));
  }

  void OnDeviceRemoved(scoped_refptr<UsbDevice> device) override {
    for (auto it = devices_.begin(); it != devices_.end(); ++it) {
      const UsbDeviceInfo* entry =
          static_cast<const UsbDeviceInfo*>((*it).get());
      if (entry->device() == device) {
        devices_.erase(it);
        if (observer()) {
          observer()->OnDevicesChanged();
        }
        return;
      }
    }
  }

  void OnDevicesEnumerated(
      const std::vector<scoped_refptr<UsbDevice>>& devices) {
    for (const auto& device : devices) {
      OnDeviceAdded(device);
    }
  }

  std::vector<UsbDeviceFilter> filters_;
  DevicePermissionsPrompt::UsbDevicesCallback callback_;
  ScopedObserver<UsbService, UsbService::Observer> service_observer_;
};

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

  ~HidDeviceInfo() override {}

  const scoped_refptr<device::HidDeviceInfo>& device() const { return device_; }

 private:
  scoped_refptr<device::HidDeviceInfo> device_;
};

class HidDevicePermissionsPrompt : public DevicePermissionsPrompt::Prompt,
                                   public device::HidService::Observer {
 public:
  HidDevicePermissionsPrompt(
      const Extension* extension,
      content::BrowserContext* context,
      bool multiple,
      const std::vector<HidDeviceFilter>& filters,
      const DevicePermissionsPrompt::HidDevicesCallback& callback)
      : Prompt(extension, context, multiple),
        filters_(filters),
        callback_(callback),
        service_observer_(this) {}

 private:
  ~HidDevicePermissionsPrompt() override {}

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

    if (observer) {
      HidService* service = device::DeviceClient::Get()->GetHidService();
      if (service && !service_observer_.IsObserving(service)) {
        service->GetDevices(
            base::Bind(&HidDevicePermissionsPrompt::OnDevicesEnumerated, this));
        service_observer_.Add(service);
      }
    }
  }

  base::string16 GetHeading() const override {
    return l10n_util::GetSingleOrMultipleStringUTF16(
        IDS_HID_DEVICE_PERMISSIONS_PROMPT_TITLE, multiple());
  }

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

  // device::HidService::Observer implementation:
  void OnDeviceAdded(scoped_refptr<device::HidDeviceInfo> device) override {
    if (HasUnprotectedCollections(device) &&
        (filters_.empty() || HidDeviceFilter::MatchesAny(device, filters_))) {
      std::unique_ptr<DeviceInfo> device_info(new HidDeviceInfo(device));
#if defined(OS_CHROMEOS)
      chromeos::PermissionBrokerClient* client =
          chromeos::DBusThreadManager::Get()->GetPermissionBrokerClient();
      DCHECK(client) << "Could not get permission broker client.";
      device::HidDeviceInfoLinux* linux_device_info =
          static_cast<device::HidDeviceInfoLinux*>(device.get());
      client->CheckPathAccess(
          linux_device_info->device_node(),
          base::Bind(&HidDevicePermissionsPrompt::AddCheckedDevice, this,
                     base::Passed(&device_info)));
#else
      AddCheckedDevice(std::move(device_info), true);
#endif  // defined(OS_CHROMEOS)
    }
  }

  void OnDeviceRemoved(scoped_refptr<device::HidDeviceInfo> device) override {
    for (auto it = devices_.begin(); it != devices_.end(); ++it) {
      const HidDeviceInfo* entry =
          static_cast<const HidDeviceInfo*>((*it).get());
      if (entry->device() == device) {
        devices_.erase(it);
        if (observer()) {
          observer()->OnDevicesChanged();
        }
        return;
      }
    }
  }

  void OnDevicesEnumerated(
      const std::vector<scoped_refptr<device::HidDeviceInfo>>& devices) {
    for (const auto& device : devices) {
      OnDeviceAdded(device);
    }
  }

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

  std::vector<HidDeviceFilter> filters_;
  DevicePermissionsPrompt::HidDevicesCallback callback_;
  ScopedObserver<HidService, HidService::Observer> service_observer_;
};

}  // 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;
}

base::string16 DevicePermissionsPrompt::Prompt::GetPromptMessage() const {
  return base::i18n::MessageFormatter::FormatWithNumberedArgs(
      l10n_util::GetStringUTF16(IDS_DEVICE_PERMISSIONS_PROMPT),
      multiple_ ? "multiple" : "single", extension_->name());
}

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

base::string16 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::AddCheckedDevice(
    std::unique_ptr<DeviceInfo> device,
    bool allowed) {
  if (allowed) {
    devices_.push_back(std::move(device));
    if (observer_) {
      observer_->OnDevicesChanged();
    }
  }
}

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

DevicePermissionsPrompt::~DevicePermissionsPrompt() {
}

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

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

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

// static
scoped_refptr<DevicePermissionsPrompt::Prompt>
DevicePermissionsPrompt::CreateUsbPromptForTest(const Extension* extension,
                                                bool multiple) {
  return make_scoped_refptr(new UsbDevicePermissionsPrompt(
      extension, nullptr, multiple, std::vector<UsbDeviceFilter>(),
      base::Bind(&NoopUsbCallback)));
}

}  // namespace extensions
