// Copyright (c) 2012 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 "content/browser/plugin_data_remover_impl.h"

#include <limits>

#include "base/bind.h"
#include "base/metrics/histogram.h"
#include "base/sequenced_task_runner_helpers.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/waitable_event.h"
#include "base/version.h"
#include "content/browser/plugin_process_host.h"
#include "content/browser/plugin_service_impl.h"
#include "content/browser/renderer_host/pepper/pepper_flash_file_message_filter.h"
#include "content/common/child_process_host_impl.h"
#include "content/common/plugin_process_messages.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/content_constants.h"
#include "content/public/common/pepper_plugin_info.h"
#include "ppapi/proxy/ppapi_messages.h"

namespace content {

namespace {

// The minimum Flash Player version that implements NPP_ClearSiteData.
const char kMinFlashVersion[] = "10.3";
const int64 kRemovalTimeoutMs = 10000;
const uint64 kClearAllData = 0;

}  // namespace

// static
PluginDataRemover* PluginDataRemover::Create(BrowserContext* browser_context) {
  return new PluginDataRemoverImpl(browser_context);
}

// static
void PluginDataRemover::GetSupportedPlugins(
    std::vector<WebPluginInfo>* supported_plugins) {
  bool allow_wildcard = false;
  std::vector<WebPluginInfo> plugins;
  PluginService::GetInstance()->GetPluginInfoArray(
      GURL(), kFlashPluginSwfMimeType, allow_wildcard, &plugins, NULL);
  Version min_version(kMinFlashVersion);
  for (std::vector<WebPluginInfo>::iterator it = plugins.begin();
       it != plugins.end(); ++it) {
    Version version;
    WebPluginInfo::CreateVersionFromString(it->version, &version);
    if (version.IsValid() && min_version.CompareTo(version) == -1)
      supported_plugins->push_back(*it);
  }
}

class PluginDataRemoverImpl::Context
    : public PluginProcessHost::Client,
      public PpapiPluginProcessHost::BrokerClient,
      public IPC::Listener,
      public base::RefCountedThreadSafe<Context,
                                        BrowserThread::DeleteOnIOThread> {
 public:
  Context(base::Time begin_time, BrowserContext* browser_context)
      : event_(new base::WaitableEvent(true, false)),
        begin_time_(begin_time),
        is_removing_(false),
        browser_context_path_(browser_context->GetPath()),
        resource_context_(browser_context->GetResourceContext()) {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  }

  void Init(const std::string& mime_type) {
    BrowserThread::PostTask(
        BrowserThread::IO,
        FROM_HERE,
        base::Bind(&Context::InitOnIOThread, this, mime_type));
    BrowserThread::PostDelayedTask(
        BrowserThread::IO,
        FROM_HERE,
        base::Bind(&Context::OnTimeout, this),
        base::TimeDelta::FromMilliseconds(kRemovalTimeoutMs));
  }

  void InitOnIOThread(const std::string& mime_type) {
    PluginServiceImpl* plugin_service = PluginServiceImpl::GetInstance();

    // Get the plugin file path.
    std::vector<WebPluginInfo> plugins;
    plugin_service->GetPluginInfoArray(
        GURL(), mime_type, false, &plugins, NULL);
    base::FilePath plugin_path;
    if (!plugins.empty())  // May be empty for some tests.
      plugin_path = plugins[0].path;

    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
    remove_start_time_ = base::Time::Now();
    is_removing_ = true;
    // Balanced in On[Ppapi]ChannelOpened or OnError. Exactly one them will
    // eventually be called, so we need to keep this object around until then.
    AddRef();

    PepperPluginInfo* pepper_info =
        plugin_service->GetRegisteredPpapiPluginInfo(plugin_path);
    if (pepper_info) {
      plugin_name_ = pepper_info->name;
      // Use the broker since we run this function outside the sandbox.
      plugin_service->OpenChannelToPpapiBroker(0, plugin_path, this);
    } else {
      plugin_service->OpenChannelToNpapiPlugin(
          0, 0, GURL(), GURL(), mime_type, this);
    }
  }

  // Called when a timeout happens in order not to block the client
  // indefinitely.
  void OnTimeout() {
    LOG_IF(ERROR, is_removing_) << "Timed out";
    SignalDone();
  }

  // PluginProcessHost::Client methods.
  virtual int ID() OVERRIDE {
    // Generate a unique identifier for this PluginProcessHostClient.
    return ChildProcessHostImpl::GenerateChildProcessUniqueId();
  }

  virtual bool OffTheRecord() OVERRIDE {
    return false;
  }

  virtual ResourceContext* GetResourceContext() OVERRIDE {
    return resource_context_;
  }

  virtual void SetPluginInfo(const WebPluginInfo& info) OVERRIDE {}

  virtual void OnFoundPluginProcessHost(PluginProcessHost* host) OVERRIDE {}

  virtual void OnSentPluginChannelRequest() OVERRIDE {}

  virtual void OnChannelOpened(const IPC::ChannelHandle& handle) OVERRIDE {
    ConnectToChannel(handle, false);
    // Balancing the AddRef call.
    Release();
  }

  virtual void OnError() OVERRIDE {
    LOG(ERROR) << "Couldn't open plugin channel";
    SignalDone();
    // Balancing the AddRef call.
    Release();
  }

  // PpapiPluginProcessHost::BrokerClient implementation.
  virtual void GetPpapiChannelInfo(base::ProcessHandle* renderer_handle,
                                   int* renderer_id) OVERRIDE {
    *renderer_handle = base::kNullProcessHandle;
    *renderer_id = 0;
  }

  virtual void OnPpapiChannelOpened(
      const IPC::ChannelHandle& channel_handle,
      base::ProcessId  /* peer_pid */,
      int /* child_id */) OVERRIDE {
    if (!channel_handle.name.empty())
      ConnectToChannel(channel_handle, true);

    // Balancing the AddRef call.
    Release();
  }

  // IPC::Listener methods.
  virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE {
    IPC_BEGIN_MESSAGE_MAP(Context, message)
      IPC_MESSAGE_HANDLER(PluginProcessHostMsg_ClearSiteDataResult,
                          OnClearSiteDataResult)
      IPC_MESSAGE_HANDLER(PpapiHostMsg_ClearSiteDataResult,
                          OnPpapiClearSiteDataResult)
      IPC_MESSAGE_UNHANDLED_ERROR()
    IPC_END_MESSAGE_MAP()

    return true;
  }

  virtual void OnChannelError() OVERRIDE {
    if (is_removing_) {
      NOTREACHED() << "Channel error";
      SignalDone();
    }
  }

  base::WaitableEvent* event() { return event_.get(); }

 private:
  friend struct BrowserThread::DeleteOnThread<BrowserThread::IO>;
  friend class base::DeleteHelper<Context>;
  virtual ~Context() {}

  IPC::Message* CreatePpapiClearSiteDataMsg(uint64 max_age) {
    base::FilePath profile_path =
        PepperFlashFileMessageFilter::GetDataDirName(browser_context_path_);
    // TODO(vtl): This "duplicates" logic in webkit/plugins/ppapi/file_path.cc
    // (which prepends the plugin name to the relative part of the path
    // instead, with the absolute, profile-dependent part being enforced by
    // the browser).
#if defined(OS_WIN)
    base::FilePath plugin_data_path =
        profile_path.Append(base::FilePath(UTF8ToUTF16(plugin_name_)));
#else
    base::FilePath plugin_data_path =
        profile_path.Append(base::FilePath(plugin_name_));
#endif  // defined(OS_WIN)
    return new PpapiMsg_ClearSiteData(0u, plugin_data_path, std::string(),
                                      kClearAllData, max_age);
  }

  // Connects the client side of a newly opened plug-in channel.
  void ConnectToChannel(const IPC::ChannelHandle& handle, bool is_ppapi) {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));

    // If we timed out, don't bother connecting.
    if (!is_removing_)
      return;

    DCHECK(!channel_.get());
    channel_.reset(new IPC::Channel(handle, IPC::Channel::MODE_CLIENT, this));
    if (!channel_->Connect()) {
      NOTREACHED() << "Couldn't connect to plugin";
      SignalDone();
      return;
    }

    uint64 max_age = begin_time_.is_null() ?
        std::numeric_limits<uint64>::max() :
        (base::Time::Now() - begin_time_).InSeconds();

    IPC::Message* msg;
    if (is_ppapi) {
      msg = CreatePpapiClearSiteDataMsg(max_age);
    } else {
      msg = new PluginProcessMsg_ClearSiteData(
          std::string(), kClearAllData, max_age);
    }
    if (!channel_->Send(msg)) {
      NOTREACHED() << "Couldn't send ClearSiteData message";
      SignalDone();
      return;
    }
  }

  // Handles the PpapiHostMsg_ClearSiteDataResult message by delegating to the
  // PluginProcessHostMsg_ClearSiteDataResult handler.
  void OnPpapiClearSiteDataResult(uint32 request_id, bool success) {
    DCHECK_EQ(0u, request_id);
    OnClearSiteDataResult(success);
  }

  // Handles the PluginProcessHostMsg_ClearSiteDataResult message.
  void OnClearSiteDataResult(bool success) {
    LOG_IF(ERROR, !success) << "ClearSiteData returned error";
    UMA_HISTOGRAM_TIMES("ClearPluginData.time",
                        base::Time::Now() - remove_start_time_);
    SignalDone();
  }

  // Signals that we are finished with removing data (successful or not). This
  // method is safe to call multiple times.
  void SignalDone() {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
    if (!is_removing_)
      return;
    is_removing_ = false;
    event_->Signal();
  }

  scoped_ptr<base::WaitableEvent> event_;
  // The point in time when we start removing data.
  base::Time remove_start_time_;
  // The point in time from which on we remove data.
  base::Time begin_time_;
  bool is_removing_;

  // Path for the current profile. Must be retrieved on the UI thread from the
  // browser context when we start so we can use it later on the I/O thread.
  base::FilePath browser_context_path_;

  // The resource context for the profile. Use only on the I/O thread.
  ResourceContext* resource_context_;

  // The name of the plugin. Use only on the I/O thread.
  std::string plugin_name_;

  // The channel is NULL until we have opened a connection to the plug-in
  // process.
  scoped_ptr<IPC::Channel> channel_;
};


PluginDataRemoverImpl::PluginDataRemoverImpl(BrowserContext* browser_context)
    : mime_type_(kFlashPluginSwfMimeType),
      browser_context_(browser_context) {
}

PluginDataRemoverImpl::~PluginDataRemoverImpl() {
}

base::WaitableEvent* PluginDataRemoverImpl::StartRemoving(
    base::Time begin_time) {
  DCHECK(!context_.get());
  context_ = new Context(begin_time, browser_context_);
  context_->Init(mime_type_);
  return context_->event();
}

}  // namespace content
