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

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

#include "base/json/json_writer.h"
#include "base/task.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_cookies_api_constants.h"
#include "chrome/browser/extensions/extension_cookies_helpers.h"
#include "chrome/browser/extensions/extension_event_router.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/extensions/extension_error_utils.h"
#include "content/browser/browser_thread.h"
#include "content/common/notification_service.h"
#include "content/common/notification_type.h"
#include "net/base/cookie_monster.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_getter.h"

namespace keys = extension_cookies_api_constants;

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

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

void ExtensionCookiesEventRouter::Observe(NotificationType type,
                                          const NotificationSource& source,
                                          const NotificationDetails& details) {
  switch (type.value) {
    case NotificationType::COOKIE_CHANGED:
      CookieChanged(
          Source<Profile>(source).ptr(),
          Details<ChromeCookieDetails>(details).ptr());
      break;

    default:
      NOTREACHED();
  }
}

void ExtensionCookiesEventRouter::CookieChanged(
    Profile* profile,
    ChromeCookieDetails* details) {
  ListValue args;
  DictionaryValue* dict = new DictionaryValue();
  dict->SetBoolean(keys::kRemovedKey, details->removed);
  dict->Set(
      keys::kCookieKey,
      extension_cookies_helpers::CreateCookieValue(*details->cookie,
          extension_cookies_helpers::GetStoreIdFromProfile(profile)));

  // Map the interal cause to an external string.
  std::string cause;
  switch (details->cause) {
    case net::CookieMonster::Delegate::CHANGE_COOKIE_EXPLICIT:
      cause = keys::kExplicitChangeCause;
      break;

    case net::CookieMonster::Delegate::CHANGE_COOKIE_OVERWRITE:
      cause = keys::kOverwriteChangeCause;
      break;

    case net::CookieMonster::Delegate::CHANGE_COOKIE_EXPIRED:
      cause = keys::kExpiredChangeCause;
      break;

    case net::CookieMonster::Delegate::CHANGE_COOKIE_EVICTED:
      cause = keys::kEvictedChangeCause;
      break;

    case net::CookieMonster::Delegate::CHANGE_COOKIE_EXPIRED_OVERWRITE:
      cause = keys::kExpiredOverwriteChangeCause;
      break;

    default:
      NOTREACHED();
  }
  dict->SetString(keys::kCauseKey, cause);

  args.Append(dict);

  std::string json_args;
  base::JSONWriter::Write(&args, false, &json_args);
  GURL cookie_domain =
      extension_cookies_helpers::GetURLFromCanonicalCookie(*details->cookie);
  DispatchEvent(profile, keys::kOnChanged, json_args, cookie_domain);
}

void ExtensionCookiesEventRouter::DispatchEvent(Profile* profile,
                                                const char* event_name,
                                                const std::string& json_args,
                                                GURL& cookie_domain) {
  if (profile && profile->GetExtensionEventRouter()) {
    profile->GetExtensionEventRouter()->DispatchEventToRenderers(
        event_name, json_args, profile, cookie_domain);
  }
}

bool CookiesFunction::ParseUrl(const DictionaryValue* details, GURL* url,
                               bool check_host_permissions) {
  DCHECK(details && url);
  std::string url_string;
  // Get the URL string or return false.
  EXTENSION_FUNCTION_VALIDATE(details->GetString(keys::kUrlKey, &url_string));
  *url = GURL(url_string);
  if (!url->is_valid()) {
    error_ = ExtensionErrorUtils::FormatErrorMessage(
        keys::kInvalidUrlError, url_string);
    return false;
  }
  // Check against host permissions if needed.
  if (check_host_permissions &&
      !GetExtension()->HasHostPermission(*url)) {
    error_ = ExtensionErrorUtils::FormatErrorMessage(
        keys::kNoHostPermissionsError, url->spec());
    return false;
  }
  return true;
}

bool CookiesFunction::ParseStoreContext(const DictionaryValue* details,
                                        net::URLRequestContextGetter** context,
                                        std::string* store_id) {
  DCHECK(details && (context || store_id));
  Profile* store_profile = NULL;
  if (details->HasKey(keys::kStoreIdKey)) {
    // The store ID was explicitly specified in the details dictionary.
    // Retrieve its corresponding cookie store.
    std::string store_id_value;
    // Get the store ID string or return false.
    EXTENSION_FUNCTION_VALIDATE(
        details->GetString(keys::kStoreIdKey, &store_id_value));
    store_profile = extension_cookies_helpers::ChooseProfileFromStoreId(
        store_id_value, profile(), include_incognito());
    if (!store_profile) {
      error_ = ExtensionErrorUtils::FormatErrorMessage(
          keys::kInvalidStoreIdError, store_id_value);
      return false;
    }
  } else {
    // The store ID was not specified; use the current execution context's
    // cookie store by default.
    // GetCurrentBrowser() already takes into account incognito settings.
    Browser* current_browser = GetCurrentBrowser();
    if (!current_browser) {
      error_ = keys::kNoCookieStoreFoundError;
      return false;
    }
    store_profile = current_browser->profile();
  }
  DCHECK(store_profile);

  if (context)
    *context = store_profile->GetRequestContext();
  if (store_id)
    *store_id = extension_cookies_helpers::GetStoreIdFromProfile(store_profile);

  return true;
}

GetCookieFunction::GetCookieFunction() {}

GetCookieFunction::~GetCookieFunction() {}

bool GetCookieFunction::RunImpl() {
  // Return false if the arguments are malformed.
  DictionaryValue* details;
  EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(0, &details));
  DCHECK(details);

  // Read/validate input parameters.
  if (!ParseUrl(details, &url_, true))
    return false;

  // Get the cookie name string or return false.
  EXTENSION_FUNCTION_VALIDATE(details->GetString(keys::kNameKey, &name_));

  net::URLRequestContextGetter* store_context = NULL;
  if (!ParseStoreContext(details, &store_context, &store_id_))
    return false;

  DCHECK(store_context && !store_id_.empty());
  store_context_ = store_context;

  bool rv = BrowserThread::PostTask(
      BrowserThread::IO, FROM_HERE,
      NewRunnableMethod(this, &GetCookieFunction::GetCookieOnIOThread));
  DCHECK(rv);

  // Will finish asynchronously.
  return true;
}

void GetCookieFunction::GetCookieOnIOThread() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
  net::CookieStore* cookie_store =
      store_context_->GetURLRequestContext()->cookie_store();
  net::CookieList cookie_list =
      extension_cookies_helpers::GetCookieListFromStore(cookie_store, url_);
  net::CookieList::iterator it;
  for (it = cookie_list.begin(); it != cookie_list.end(); ++it) {
    // Return the first matching cookie. Relies on the fact that the
    // CookieMonster returns them in canonical order (longest path, then
    // earliest creation time).
    if (it->Name() == name_) {
      result_.reset(
          extension_cookies_helpers::CreateCookieValue(*it, store_id_));
      break;
    }
  }

  // The cookie doesn't exist; return null.
  if (it == cookie_list.end())
    result_.reset(Value::CreateNullValue());

  bool rv = BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      NewRunnableMethod(this, &GetCookieFunction::RespondOnUIThread));
  DCHECK(rv);
}

void GetCookieFunction::RespondOnUIThread() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  SendResponse(true);
}

GetAllCookiesFunction::GetAllCookiesFunction() : details_(NULL) {}

GetAllCookiesFunction::~GetAllCookiesFunction() {}

bool GetAllCookiesFunction::RunImpl() {
  // Return false if the arguments are malformed.
  EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(0, &details_));
  DCHECK(details_);

  // Read/validate input parameters.
  if (details_->HasKey(keys::kUrlKey) && !ParseUrl(details_, &url_, false))
    return false;

  net::URLRequestContextGetter* store_context = NULL;
  if (!ParseStoreContext(details_, &store_context, &store_id_))
    return false;
  DCHECK(store_context);
  store_context_ = store_context;

  bool rv = BrowserThread::PostTask(
      BrowserThread::IO, FROM_HERE,
      NewRunnableMethod(this, &GetAllCookiesFunction::GetAllCookiesOnIOThread));
  DCHECK(rv);

  // Will finish asynchronously.
  return true;
}

void GetAllCookiesFunction::GetAllCookiesOnIOThread() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
  net::CookieStore* cookie_store =
      store_context_->GetURLRequestContext()->cookie_store();
  net::CookieList cookie_list =
      extension_cookies_helpers::GetCookieListFromStore(cookie_store, url_);

  const Extension* extension = GetExtension();
  if (extension) {
    ListValue* matching_list = new ListValue();
    extension_cookies_helpers::AppendMatchingCookiesToList(
        cookie_list, store_id_, url_, details_,
        GetExtension(), matching_list);
    result_.reset(matching_list);
  }
  bool rv = BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      NewRunnableMethod(this, &GetAllCookiesFunction::RespondOnUIThread));
  DCHECK(rv);
}

void GetAllCookiesFunction::RespondOnUIThread() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  SendResponse(true);
}

SetCookieFunction::SetCookieFunction()
    : secure_(false),
      http_only_(false),
      success_(false) {
}

SetCookieFunction::~SetCookieFunction() {
}

bool SetCookieFunction::RunImpl() {
  // Return false if the arguments are malformed.
  DictionaryValue* details;
  EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(0, &details));
  DCHECK(details);

  // Read/validate input parameters.
  if (!ParseUrl(details, &url_, true))
      return false;
  // The macros below return false if argument types are not as expected.
  if (details->HasKey(keys::kNameKey))
    EXTENSION_FUNCTION_VALIDATE(details->GetString(keys::kNameKey, &name_));
  if (details->HasKey(keys::kValueKey))
    EXTENSION_FUNCTION_VALIDATE(details->GetString(keys::kValueKey, &value_));
  if (details->HasKey(keys::kDomainKey))
    EXTENSION_FUNCTION_VALIDATE(details->GetString(keys::kDomainKey, &domain_));
  if (details->HasKey(keys::kPathKey))
    EXTENSION_FUNCTION_VALIDATE(details->GetString(keys::kPathKey, &path_));

  if (details->HasKey(keys::kSecureKey)) {
    EXTENSION_FUNCTION_VALIDATE(
        details->GetBoolean(keys::kSecureKey, &secure_));
  }
  if (details->HasKey(keys::kHttpOnlyKey)) {
    EXTENSION_FUNCTION_VALIDATE(
        details->GetBoolean(keys::kHttpOnlyKey, &http_only_));
  }
  if (details->HasKey(keys::kExpirationDateKey)) {
    Value* expiration_date_value;
    EXTENSION_FUNCTION_VALIDATE(details->Get(keys::kExpirationDateKey,
                                             &expiration_date_value));
    double expiration_date;
    if (expiration_date_value->IsType(Value::TYPE_INTEGER)) {
      int expiration_date_int;
      EXTENSION_FUNCTION_VALIDATE(
          expiration_date_value->GetAsInteger(&expiration_date_int));
      expiration_date = static_cast<double>(expiration_date_int);
    } else {
      EXTENSION_FUNCTION_VALIDATE(
          expiration_date_value->GetAsDouble(&expiration_date));
    }
    // Time::FromDoubleT converts double time 0 to empty Time object. So we need
    // to do special handling here.
    expiration_time_ = (expiration_date == 0) ?
        base::Time::UnixEpoch() : base::Time::FromDoubleT(expiration_date);
  }

  net::URLRequestContextGetter* store_context = NULL;
  if (!ParseStoreContext(details, &store_context, NULL))
    return false;
  DCHECK(store_context);
  store_context_ = store_context;

  bool rv = BrowserThread::PostTask(
      BrowserThread::IO, FROM_HERE,
      NewRunnableMethod(this, &SetCookieFunction::SetCookieOnIOThread));
  DCHECK(rv);

  // Will finish asynchronously.
  return true;
}

void SetCookieFunction::SetCookieOnIOThread() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
  net::CookieMonster* cookie_monster =
      store_context_->GetURLRequestContext()->cookie_store()->
      GetCookieMonster();
  success_ = cookie_monster->SetCookieWithDetails(
      url_, name_, value_, domain_, path_, expiration_time_,
      secure_, http_only_);

  // Pull the newly set cookie.
  net::CookieList cookie_list =
      extension_cookies_helpers::GetCookieListFromStore(cookie_monster, url_);
  net::CookieList::iterator it;
  for (it = cookie_list.begin(); it != cookie_list.end(); ++it) {
    // Return the first matching cookie. Relies on the fact that the
    // CookieMonster returns them in canonical order (longest path, then
    // earliest creation time).
    if (it->Name() == name_) {
      result_.reset(
          extension_cookies_helpers::CreateCookieValue(*it, store_id_));
      break;
    }
  }

  bool rv = BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      NewRunnableMethod(this, &SetCookieFunction::RespondOnUIThread));
  DCHECK(rv);
}

void SetCookieFunction::RespondOnUIThread() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  if (!success_) {
    error_ = ExtensionErrorUtils::FormatErrorMessage(
        keys::kCookieSetFailedError, name_);
  }
  SendResponse(success_);
}

RemoveCookieFunction::RemoveCookieFunction() : success_(false) {
}

RemoveCookieFunction::~RemoveCookieFunction() {
}

bool RemoveCookieFunction::RunImpl() {
  // Return false if the arguments are malformed.
  DictionaryValue* details;
  EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(0, &details));
  DCHECK(details);

  // Read/validate input parameters.
  if (!ParseUrl(details, &url_, true))
    return false;

  // Get the cookie name string or return false.
  EXTENSION_FUNCTION_VALIDATE(details->GetString(keys::kNameKey, &name_));

  net::URLRequestContextGetter* store_context = NULL;
  if (!ParseStoreContext(details, &store_context, &store_id_))
    return false;
  DCHECK(store_context);
  store_context_ = store_context;

  // Pass the work off to the IO thread.
  bool rv = BrowserThread::PostTask(
      BrowserThread::IO, FROM_HERE,
      NewRunnableMethod(this, &RemoveCookieFunction::RemoveCookieOnIOThread));
  DCHECK(rv);

  // Will return asynchronously.
  return true;
}

void RemoveCookieFunction::RemoveCookieOnIOThread() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));

  // Remove the cookie
  net::CookieStore* cookie_store =
      store_context_->GetURLRequestContext()->cookie_store();
  cookie_store->DeleteCookie(url_, name_);

  // Build the callback result
  DictionaryValue* resultDictionary = new DictionaryValue();
  resultDictionary->SetString(keys::kNameKey, name_);
  resultDictionary->SetString(keys::kUrlKey, url_.spec());
  resultDictionary->SetString(keys::kStoreIdKey, store_id_);
  result_.reset(resultDictionary);

  // Return to UI thread
  bool rv = BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      NewRunnableMethod(this, &RemoveCookieFunction::RespondOnUIThread));
  DCHECK(rv);
}

void RemoveCookieFunction::RespondOnUIThread() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  SendResponse(true);
}

bool GetAllCookieStoresFunction::RunImpl() {
  Profile* original_profile = profile();
  DCHECK(original_profile);
  scoped_ptr<ListValue> original_tab_ids(new ListValue());
  Profile* incognito_profile = NULL;
  scoped_ptr<ListValue> incognito_tab_ids;
  if (include_incognito() && profile()->HasOffTheRecordProfile()) {
    incognito_profile = profile()->GetOffTheRecordProfile();
    if (incognito_profile)
      incognito_tab_ids.reset(new ListValue());
  }
  DCHECK(original_profile != incognito_profile);

  // Iterate through all browser instances, and for each browser,
  // add its tab IDs to either the regular or incognito tab ID list depending
  // whether the browser is regular or incognito.
  for (BrowserList::const_iterator iter = BrowserList::begin();
       iter != BrowserList::end(); ++iter) {
    Browser* browser = *iter;
    if (browser->profile() == original_profile) {
      extension_cookies_helpers::AppendToTabIdList(browser,
                                                   original_tab_ids.get());
    } else if (incognito_tab_ids.get() &&
               browser->profile() == incognito_profile) {
      extension_cookies_helpers::AppendToTabIdList(browser,
                                                   incognito_tab_ids.get());
    }
  }
  // Return a list of all cookie stores with at least one open tab.
  ListValue* cookie_store_list = new ListValue();
  if (original_tab_ids->GetSize() > 0) {
    cookie_store_list->Append(
        extension_cookies_helpers::CreateCookieStoreValue(
            original_profile, original_tab_ids.release()));
  }
  if (incognito_tab_ids.get() && incognito_tab_ids->GetSize() > 0) {
    cookie_store_list->Append(
        extension_cookies_helpers::CreateCookieStoreValue(
            incognito_profile, incognito_tab_ids.release()));
  }
  result_.reset(cookie_store_list);
  return true;
}

void GetAllCookieStoresFunction::Run() {
  SendResponse(RunImpl());
}