// 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.

// Implements the Chrome Extensions WebNavigation API.

#include "chrome/browser/extensions/extension_webnavigation_api.h"

#include "base/json/json_writer.h"
#include "base/string_number_conversions.h"
#include "base/time.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_event_router.h"
#include "chrome/browser/extensions/extension_tabs_module.h"
#include "chrome/browser/extensions/extension_webnavigation_api_constants.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/url_constants.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "content/common/notification_service.h"
#include "content/common/view_messages.h"
#include "net/base/net_errors.h"

namespace keys = extension_webnavigation_api_constants;

namespace {

// URL schemes for which we'll send events.
const char* kValidSchemes[] = {
  chrome::kHttpScheme,
  chrome::kHttpsScheme,
  chrome::kFileScheme,
  chrome::kFtpScheme,
};

// Returns 0 if the navigation happens in the main frame, or the frame ID
// modulo 32 bits otherwise.
int GetFrameId(bool is_main_frame, int64 frame_id) {
  return is_main_frame ? 0 : static_cast<int>(frame_id);
}

// Returns |time| as milliseconds since the epoch.
double MilliSecondsFromTime(const base::Time& time) {
  return 1000 * time.ToDoubleT();
}

// Dispatches events to the extension message service.
void DispatchEvent(Profile* profile,
                   const char* event_name,
                   const std::string& json_args) {
  if (profile && profile->GetExtensionEventRouter()) {
    profile->GetExtensionEventRouter()->DispatchEventToRenderers(
        event_name, json_args, profile, GURL());
  }
}

// Constructs and dispatches an onBeforeNavigate event.
void DispatchOnBeforeNavigate(TabContents* tab_contents,
                              int64 frame_id,
                              bool is_main_frame,
                              const GURL& validated_url,
                              uint64 request_id) {
  ListValue args;
  DictionaryValue* dict = new DictionaryValue();
  dict->SetInteger(keys::kTabIdKey,
                   ExtensionTabUtil::GetTabId(tab_contents));
  dict->SetString(keys::kUrlKey, validated_url.spec());
  dict->SetInteger(keys::kFrameIdKey, GetFrameId(is_main_frame, frame_id));
  dict->SetString(keys::kRequestIdKey,
                  base::Uint64ToString(request_id));
  dict->SetDouble(keys::kTimeStampKey, MilliSecondsFromTime(base::Time::Now()));
  args.Append(dict);

  std::string json_args;
  base::JSONWriter::Write(&args, false, &json_args);
  DispatchEvent(tab_contents->profile(), keys::kOnBeforeNavigate, json_args);
}

// Constructs and dispatches an onCommitted event.
void DispatchOnCommitted(TabContents* tab_contents,
                         int64 frame_id,
                         bool is_main_frame,
                         const GURL& url,
                         PageTransition::Type transition_type) {
  ListValue args;
  DictionaryValue* dict = new DictionaryValue();
  dict->SetInteger(keys::kTabIdKey,
                   ExtensionTabUtil::GetTabId(tab_contents));
  dict->SetString(keys::kUrlKey, url.spec());
  dict->SetInteger(keys::kFrameIdKey, GetFrameId(is_main_frame, frame_id));
  dict->SetString(keys::kTransitionTypeKey,
                  PageTransition::CoreTransitionString(transition_type));
  ListValue* qualifiers = new ListValue();
  if (transition_type & PageTransition::CLIENT_REDIRECT)
    qualifiers->Append(Value::CreateStringValue("client_redirect"));
  if (transition_type & PageTransition::SERVER_REDIRECT)
    qualifiers->Append(Value::CreateStringValue("server_redirect"));
  if (transition_type & PageTransition::FORWARD_BACK)
    qualifiers->Append(Value::CreateStringValue("forward_back"));
  dict->Set(keys::kTransitionQualifiersKey, qualifiers);
  dict->SetDouble(keys::kTimeStampKey, MilliSecondsFromTime(base::Time::Now()));
  args.Append(dict);

  std::string json_args;
  base::JSONWriter::Write(&args, false, &json_args);
  DispatchEvent(tab_contents->profile(), keys::kOnCommitted, json_args);
}

// Constructs and dispatches an onDOMContentLoaded event.
void DispatchOnDOMContentLoaded(TabContents* tab_contents,
                                const GURL& url,
                                bool is_main_frame,
                                int64 frame_id) {
  ListValue args;
  DictionaryValue* dict = new DictionaryValue();
  dict->SetInteger(keys::kTabIdKey,
                   ExtensionTabUtil::GetTabId(tab_contents));
  dict->SetString(keys::kUrlKey, url.spec());
  dict->SetInteger(keys::kFrameIdKey,
      is_main_frame ? 0 : static_cast<int>(frame_id));
  dict->SetDouble(keys::kTimeStampKey, MilliSecondsFromTime(base::Time::Now()));
  args.Append(dict);

  std::string json_args;
  base::JSONWriter::Write(&args, false, &json_args);
  DispatchEvent(tab_contents->profile(), keys::kOnDOMContentLoaded, json_args);
}

// Constructs and dispatches an onCompleted event.
void DispatchOnCompleted(TabContents* tab_contents,
                         const GURL& url,
                         bool is_main_frame,
                         int64 frame_id) {
  ListValue args;
  DictionaryValue* dict = new DictionaryValue();
  dict->SetInteger(keys::kTabIdKey,
                   ExtensionTabUtil::GetTabId(tab_contents));
  dict->SetString(keys::kUrlKey, url.spec());
  dict->SetInteger(keys::kFrameIdKey,
      is_main_frame ? 0 : static_cast<int>(frame_id));
  dict->SetDouble(keys::kTimeStampKey, MilliSecondsFromTime(base::Time::Now()));
  args.Append(dict);

  std::string json_args;
  base::JSONWriter::Write(&args, false, &json_args);
  DispatchEvent(tab_contents->profile(), keys::kOnCompleted, json_args);
}

// Constructs and dispatches an onBeforeRetarget event.
void DispatchOnBeforeRetarget(TabContents* tab_contents,
                              Profile* profile,
                              const GURL& opener_url,
                              const GURL& target_url) {
  ListValue args;
  DictionaryValue* dict = new DictionaryValue();
  dict->SetInteger(keys::kSourceTabIdKey,
                   ExtensionTabUtil::GetTabId(tab_contents));
  dict->SetString(keys::kSourceUrlKey, opener_url.spec());
  dict->SetString(keys::kUrlKey, target_url.possibly_invalid_spec());
  dict->SetDouble(keys::kTimeStampKey, MilliSecondsFromTime(base::Time::Now()));
  args.Append(dict);

  std::string json_args;
  base::JSONWriter::Write(&args, false, &json_args);
  DispatchEvent(profile, keys::kOnBeforeRetarget, json_args);
}

}  // namespace


// FrameNavigationState -------------------------------------------------------

// static
bool FrameNavigationState::allow_extension_scheme_ = false;

FrameNavigationState::FrameNavigationState() {}

FrameNavigationState::~FrameNavigationState() {}

bool FrameNavigationState::CanSendEvents(int64 frame_id) const {
  FrameIdToStateMap::const_iterator frame_state =
      frame_state_map_.find(frame_id);
  if (frame_state == frame_state_map_.end() ||
      frame_state->second.error_occurred) {
    return false;
  }
  const std::string& scheme = frame_state->second.url.scheme();
  for (unsigned i = 0; i < arraysize(kValidSchemes); ++i) {
    if (scheme == kValidSchemes[i])
      return true;
  }
  if (allow_extension_scheme_ && scheme == chrome::kExtensionScheme)
    return true;
  return false;
}

void FrameNavigationState::TrackFrame(int64 frame_id,
                                      const GURL& url,
                                      bool is_main_frame,
                                      bool is_error_page,
                                      const TabContents* tab_contents) {
  if (is_main_frame)
    RemoveTabContentsState(tab_contents);
  tab_contents_map_.insert(std::make_pair(tab_contents, frame_id));
  FrameState& frame_state = frame_state_map_[frame_id];
  frame_state.error_occurred = is_error_page;
  frame_state.url = url;
  frame_state.is_main_frame = is_main_frame;
}

GURL FrameNavigationState::GetUrl(int64 frame_id) const {
  FrameIdToStateMap::const_iterator frame_state =
      frame_state_map_.find(frame_id);
  if (frame_state == frame_state_map_.end()) {
    NOTREACHED();
    return GURL();
  }
  return frame_state->second.url;
}

bool FrameNavigationState::IsMainFrame(int64 frame_id) const {
  FrameIdToStateMap::const_iterator frame_state =
      frame_state_map_.find(frame_id);
  if (frame_state == frame_state_map_.end()) {
    NOTREACHED();
    return false;
  }
  return frame_state->second.is_main_frame;
}

void FrameNavigationState::ErrorOccurredInFrame(int64 frame_id) {
  DCHECK(frame_state_map_.find(frame_id) != frame_state_map_.end());
  frame_state_map_[frame_id].error_occurred = true;
}

void FrameNavigationState::RemoveTabContentsState(
    const TabContents* tab_contents) {
  typedef TabContentsToFrameIdMap::iterator FrameIdIterator;
  std::pair<FrameIdIterator, FrameIdIterator> frame_ids =
      tab_contents_map_.equal_range(tab_contents);
  for (FrameIdIterator frame_id = frame_ids.first; frame_id != frame_ids.second;
       ++frame_id) {
    frame_state_map_.erase(frame_id->second);
  }
  tab_contents_map_.erase(tab_contents);
}


// ExtensionWebNavigtionEventRouter -------------------------------------------

ExtensionWebNavigationEventRouter::ExtensionWebNavigationEventRouter() {}

ExtensionWebNavigationEventRouter::~ExtensionWebNavigationEventRouter() {}

// static
ExtensionWebNavigationEventRouter*
ExtensionWebNavigationEventRouter::GetInstance() {
  return Singleton<ExtensionWebNavigationEventRouter>::get();
}

void ExtensionWebNavigationEventRouter::Init() {
  if (registrar_.IsEmpty()) {
    registrar_.Add(this,
                   NotificationType::CREATING_NEW_WINDOW,
                   NotificationService::AllSources());
  }
}

void ExtensionWebNavigationEventRouter::Observe(
    NotificationType type,
    const NotificationSource& source,
    const NotificationDetails& details) {
  switch (type.value) {
    case NotificationType::CREATING_NEW_WINDOW:
      CreatingNewWindow(
          Source<TabContents>(source).ptr(),
          Details<const ViewHostMsg_CreateWindow_Params>(details).ptr());
      break;

    default:
      NOTREACHED();
  }
}

void ExtensionWebNavigationEventRouter::CreatingNewWindow(
    TabContents* tab_contents,
    const ViewHostMsg_CreateWindow_Params* details) {
  DispatchOnBeforeRetarget(tab_contents,
                           tab_contents->profile(),
                           details->opener_url,
                           details->target_url);
}


// ExtensionWebNavigationTabObserver ------------------------------------------

ExtensionWebNavigationTabObserver::ExtensionWebNavigationTabObserver(
    TabContents* tab_contents)
    : TabContentsObserver(tab_contents) {}

ExtensionWebNavigationTabObserver::~ExtensionWebNavigationTabObserver() {}

void ExtensionWebNavigationTabObserver::DidStartProvisionalLoadForFrame(
    int64 frame_id,
    bool is_main_frame,
    const GURL& validated_url,
    bool is_error_page) {
  navigation_state_.TrackFrame(frame_id,
                               validated_url,
                               is_main_frame,
                               is_error_page,
                               tab_contents());
  if (!navigation_state_.CanSendEvents(frame_id))
    return;
  DispatchOnBeforeNavigate(
      tab_contents(), frame_id, is_main_frame, validated_url, 0);
}

void ExtensionWebNavigationTabObserver::DidCommitProvisionalLoadForFrame(
    int64 frame_id,
    bool is_main_frame,
    const GURL& url,
    PageTransition::Type transition_type) {
  if (!navigation_state_.CanSendEvents(frame_id))
    return;
  // On reference fragment navigations, only a new navigation state is
  // committed. We need to catch this case and generate a full sequence
  // of events.
  if (IsReferenceFragmentNavigation(frame_id, url)) {
    NavigatedReferenceFragment(frame_id, is_main_frame, url, transition_type);
    return;
  }
  DispatchOnCommitted(
      tab_contents(), frame_id, is_main_frame, url, transition_type);
}

void ExtensionWebNavigationTabObserver::DidFailProvisionalLoad(
    int64 frame_id,
    bool is_main_frame,
    const GURL& validated_url,
    int error_code) {
  if (!navigation_state_.CanSendEvents(frame_id))
    return;
  ListValue args;
  DictionaryValue* dict = new DictionaryValue();
  dict->SetInteger(keys::kTabIdKey,
                   ExtensionTabUtil::GetTabId(tab_contents()));
  dict->SetString(keys::kUrlKey, validated_url.spec());
  dict->SetInteger(keys::kFrameIdKey, GetFrameId(is_main_frame, frame_id));
  dict->SetString(keys::kErrorKey,
                  std::string(net::ErrorToString(error_code)));
  dict->SetDouble(keys::kTimeStampKey, MilliSecondsFromTime(base::Time::Now()));
  args.Append(dict);

  std::string json_args;
  base::JSONWriter::Write(&args, false, &json_args);
  navigation_state_.ErrorOccurredInFrame(frame_id);
  DispatchEvent(tab_contents()->profile(), keys::kOnErrorOccurred, json_args);
}

void ExtensionWebNavigationTabObserver::DocumentLoadedInFrame(
    int64 frame_id) {
  if (!navigation_state_.CanSendEvents(frame_id))
    return;
  DispatchOnDOMContentLoaded(tab_contents(),
                             navigation_state_.GetUrl(frame_id),
                             navigation_state_.IsMainFrame(frame_id),
                             frame_id);
}

void ExtensionWebNavigationTabObserver::DidFinishLoad(
    int64 frame_id) {
  if (!navigation_state_.CanSendEvents(frame_id))
    return;
  DispatchOnCompleted(tab_contents(),
                      navigation_state_.GetUrl(frame_id),
                      navigation_state_.IsMainFrame(frame_id),
                      frame_id);
}

void ExtensionWebNavigationTabObserver::TabContentsDestroyed(
    TabContents* tab) {
  navigation_state_.RemoveTabContentsState(tab);
}

void ExtensionWebNavigationTabObserver::DidOpenURL(
    const GURL& url,
    const GURL& referrer,
    WindowOpenDisposition disposition,
    PageTransition::Type transition) {
  if (disposition != NEW_FOREGROUND_TAB &&
      disposition != NEW_BACKGROUND_TAB &&
      disposition != NEW_WINDOW &&
      disposition != OFF_THE_RECORD) {
    return;
  }
  Profile* profile = tab_contents()->profile();
  if (disposition == OFF_THE_RECORD) {
    if (!profile->HasOffTheRecordProfile()) {
      NOTREACHED();
      return;
    }
    profile = profile->GetOffTheRecordProfile();
  }
  DispatchOnBeforeRetarget(tab_contents(),
                           profile,
                           tab_contents()->GetURL(),
                           url);
}

// See also NavigationController::IsURLInPageNavigation.
bool ExtensionWebNavigationTabObserver::IsReferenceFragmentNavigation(
    int64 frame_id,
    const GURL& url) {
  GURL existing_url = navigation_state_.GetUrl(frame_id);
  if (existing_url == url)
    return false;

  url_canon::Replacements<char> replacements;
  replacements.ClearRef();
  return existing_url.ReplaceComponents(replacements) ==
      url.ReplaceComponents(replacements);
}

void ExtensionWebNavigationTabObserver::NavigatedReferenceFragment(
    int64 frame_id,
    bool is_main_frame,
    const GURL& url,
    PageTransition::Type transition_type) {
  navigation_state_.TrackFrame(frame_id,
                               url,
                               is_main_frame,
                               false,
                               tab_contents());

  DispatchOnBeforeNavigate(tab_contents(),
                           frame_id,
                           is_main_frame,
                           url,
                           0);
  DispatchOnCommitted(tab_contents(),
                      frame_id,
                      is_main_frame,
                      url,
                      transition_type);
  DispatchOnDOMContentLoaded(tab_contents(),
                             url,
                             is_main_frame,
                             frame_id);
  DispatchOnCompleted(tab_contents(),
                      url,
                      is_main_frame,
                      frame_id);
}