// Copyright (c) 2011 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.
//
// Implementation of the MalwareDetails class.

#include "chrome/browser/safe_browsing/malware_details.h"

#include "base/callback.h"
#include "base/lazy_instance.h"
#include "chrome/browser/net/chrome_url_request_context.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/malware_details_cache.h"
#include "chrome/browser/safe_browsing/report.pb.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/common/safe_browsing/safebrowsing_messages.h"
#include "content/browser/browser_thread.h"
#include "content/browser/renderer_host/render_view_host.h"
#include "content/browser/tab_contents/navigation_entry.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "net/base/io_buffer.h"
#include "net/disk_cache/disk_cache.h"
#include "net/url_request/url_request_context_getter.h"

using safe_browsing::ClientMalwareReportRequest;

// Keep in sync with KMaxNodes in renderer/safe_browsing/malware_dom_details
static const uint32 kMaxDomNodes = 500;

// static
MalwareDetailsFactory* MalwareDetails::factory_ = NULL;

// The default MalwareDetailsFactory.  Global, made a singleton so we
// don't leak it.
class MalwareDetailsFactoryImpl
    : public MalwareDetailsFactory {
 public:
  MalwareDetails* CreateMalwareDetails(
      SafeBrowsingService* sb_service,
      TabContents* tab_contents,
      const SafeBrowsingService::UnsafeResource& unsafe_resource) {
    return new MalwareDetails(sb_service, tab_contents, unsafe_resource);
  }

 private:
  friend struct base::DefaultLazyInstanceTraits<
      MalwareDetailsFactoryImpl>;

  MalwareDetailsFactoryImpl() { }

  DISALLOW_COPY_AND_ASSIGN(MalwareDetailsFactoryImpl);
};

static base::LazyInstance<MalwareDetailsFactoryImpl>
    g_malware_details_factory_impl(base::LINKER_INITIALIZED);

// Create a MalwareDetails for the given tab.
/* static */
MalwareDetails* MalwareDetails::NewMalwareDetails(
    SafeBrowsingService* sb_service,
    TabContents* tab_contents,
    const SafeBrowsingService::UnsafeResource& resource) {
  // Set up the factory if this has not been done already (tests do that
  // before this method is called).
  if (!factory_)
    factory_ = g_malware_details_factory_impl.Pointer();
  return factory_->CreateMalwareDetails(sb_service, tab_contents, resource);
}

// Create a MalwareDetails for the given tab. Runs in the UI thread.
MalwareDetails::MalwareDetails(
    SafeBrowsingService* sb_service,
    TabContents* tab_contents,
    const SafeBrowsingService::UnsafeResource& resource)
    : TabContentsObserver(tab_contents),
      request_context_getter_(tab_contents->profile()->GetRequestContext()),
      sb_service_(sb_service),
      resource_(resource),
      cache_collector_(new MalwareDetailsCacheCollector) {
  StartCollection();
}

MalwareDetails::~MalwareDetails() {
}

bool MalwareDetails::OnMessageReceived(const IPC::Message& message) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(MalwareDetails, message)
    IPC_MESSAGE_HANDLER(SafeBrowsingHostMsg_MalwareDOMDetails,
                        OnReceivedMalwareDOMDetails)
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}

bool MalwareDetails::IsPublicUrl(const GURL& url) const {
  return url.SchemeIs("http");  // TODO(panayiotis): also skip internal urls.
}

// Looks for a Resource for the given url in resources_.  If found, it
// updates |resource|. Otherwise, it creates a new message, adds it to
// resources_ and updates |resource| to point to it.
ClientMalwareReportRequest::Resource* MalwareDetails::FindOrCreateResource(
    const GURL& url) {
  safe_browsing::ResourceMap::iterator it = resources_.find(url.spec());
  if (it != resources_.end()) {
    return it->second.get();
  }

  // Create the resource for |url|.
  int id = resources_.size();
  linked_ptr<ClientMalwareReportRequest::Resource> new_resource(
      new ClientMalwareReportRequest::Resource());
  new_resource->set_url(url.spec());
  new_resource->set_id(id);
  resources_[url.spec()] = new_resource;
  return new_resource.get();
}

void MalwareDetails::AddUrl(const GURL& url,
                            const GURL& parent,
                            const std::string& tagname,
                            const std::vector<GURL>* children) {
  if (!IsPublicUrl(url))
    return;

  // Find (or create) the resource for the url.
  ClientMalwareReportRequest::Resource* url_resource =
      FindOrCreateResource(url);
  if (!tagname.empty()) {
    url_resource->set_tag_name(tagname);
  }
  if (!parent.is_empty() && IsPublicUrl(parent)) {
    // Add the resource for the parent.
    ClientMalwareReportRequest::Resource* parent_resource =
        FindOrCreateResource(parent);
    // Update the parent-child relation
    url_resource->set_parent_id(parent_resource->id());
  }
  if (children) {
    for (std::vector<GURL>::const_iterator it = children->begin();
         it != children->end(); it++) {
      ClientMalwareReportRequest::Resource* child_resource =
          FindOrCreateResource(*it);
      url_resource->add_child_ids(child_resource->id());
    }
  }
}

void MalwareDetails::StartCollection() {
  DVLOG(1) << "Starting to compute malware details.";
  report_.reset(new ClientMalwareReportRequest());

  if (IsPublicUrl(resource_.url)) {
    report_->set_malware_url(resource_.url.spec());
  }

  GURL page_url = tab_contents()->GetURL();
  if (IsPublicUrl(page_url)) {
    report_->set_page_url(page_url.spec());
  }

  GURL referrer_url;
  NavigationEntry* nav_entry = tab_contents()->controller().GetActiveEntry();
  if (nav_entry) {
    referrer_url = nav_entry->referrer();
    if (IsPublicUrl(referrer_url)) {
      report_->set_referrer_url(referrer_url.spec());
    }
  }

  // Add the nodes, starting from the page url.
  AddUrl(page_url, GURL(), "", NULL);

  // Add the resource_url and its original url, if non-empty and different.
  if (!resource_.original_url.is_empty() &&
      resource_.url != resource_.original_url) {
    // Add original_url, as the parent of resource_url.
    AddUrl(resource_.original_url, GURL(), "", NULL);
    AddUrl(resource_.url, resource_.original_url, "", NULL);
  } else {
    AddUrl(resource_.url, GURL(), "", NULL);
  }

  // Add the redirect urls, if non-empty. The redirect urls do not include the
  // original url, but include the unsafe url which is the last one of the
  // redirect urls chain
  GURL parent_url;
  // Set the original url as the parent of the first redirect url if it's not
  // empty.
  if (!resource_.original_url.is_empty()) {
    parent_url = resource_.original_url;
  }
  // Set the previous redirect url as the parent of the next one
  for (unsigned int i = 0; i < resource_.redirect_urls.size(); ++i) {
    AddUrl(resource_.redirect_urls[i], parent_url, "", NULL);
    parent_url = resource_.redirect_urls[i];
  }

  // Add the referrer url.
  if (nav_entry && !referrer_url.is_empty()) {
    AddUrl(referrer_url, GURL(), "", NULL);
  }

  // Get URLs of frames, scripts etc from the DOM.
  // OnReceivedMalwareDOMDetails will be called when the renderer replies.
  tab_contents()->render_view_host()->GetMalwareDOMDetails();
}

// When the renderer is done, this is called.
void MalwareDetails::OnReceivedMalwareDOMDetails(
    const std::vector<SafeBrowsingHostMsg_MalwareDOMDetails_Node>& params) {
  // Schedule this in IO thread, so it doesn't conflict with future users
  // of our data structures (eg GetSerializedReport).
  BrowserThread::PostTask(
      BrowserThread::IO, FROM_HERE,
      NewRunnableMethod(
          this, &MalwareDetails::AddDOMDetails, params));
}

void MalwareDetails::AddDOMDetails(
    const std::vector<SafeBrowsingHostMsg_MalwareDOMDetails_Node>& params) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
  DVLOG(1) << "Nodes from the DOM: " << params.size();

  // If we have already started collecting data from the HTTP cache, don't
  // modify our state.
  if (cache_collector_->HasStarted())
    return;

  // Add the urls from the DOM to |resources_|.  The renderer could be
  // sending bogus messages, so limit the number of nodes we accept.
  for (uint32 i = 0; i < params.size() && i < kMaxDomNodes; ++i) {
    SafeBrowsingHostMsg_MalwareDOMDetails_Node node = params[i];
    DVLOG(1) << node.url << ", " << node.tag_name << ", " << node.parent;
    AddUrl(node.url, node.parent, node.tag_name, &(node.children));
  }
}

// Called from the SB Service on the IO thread, after the user has
// closed the tab, or clicked proceed or goback.  Since the user needs
// to take an action, we expect this to be called after
// OnReceivedMalwareDOMDetails in most cases. If not, we don't include
// the DOM data in our report.
void MalwareDetails::FinishCollection() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));

  cache_collector_->StartCacheCollection(
      request_context_getter_,
      &resources_,
      &cache_result_,
      NewRunnableMethod(this, &MalwareDetails::OnCacheCollectionReady));
}

void MalwareDetails::OnCacheCollectionReady() {
  DVLOG(1) << "OnCacheCollectionReady.";
  // Add all the urls in our |resources_| maps to the |report_| protocol buffer.
  for (safe_browsing::ResourceMap::const_iterator it = resources_.begin();
       it != resources_.end(); it++) {
    ClientMalwareReportRequest::Resource* pb_resource =
        report_->add_resources();
    pb_resource->CopyFrom(*(it->second));
  }

  report_->set_complete(cache_result_);

  // Send the report, using the SafeBrowsingService.
  std::string serialized;
  if (!report_->SerializeToString(&serialized)) {
    DLOG(ERROR) << "Unable to serialize the malware report.";
    return;
  }

  sb_service_->SendSerializedMalwareDetails(serialized);
}