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

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

#include <string>
#include <vector>

#include "base/memory/scoped_temp_dir.h"
#include "base/string_util.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/crx_installer.h"
#include "chrome/browser/extensions/extension_install_dialog.h"
#include "chrome/browser/extensions/extension_prefs.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/net/gaia/token_service.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/sync/profile_sync_service.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/extensions/extension_error_utils.h"
#include "chrome/common/net/gaia/gaia_constants.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "content/common/notification_details.h"
#include "content/common/notification_source.h"
#include "content/common/notification_type.h"
#include "grit/chromium_strings.h"
#include "grit/generated_resources.h"
#include "net/base/escape.h"
#include "ui/base/l10n/l10n_util.h"

namespace {

const char kLoginKey[] = "login";
const char kTokenKey[] = "token";
const char kImageDecodeError[] = "Image decode failed";
const char kInvalidIdError[] = "Invalid id";
const char kInvalidManifestError[] = "Invalid manifest";
const char kNoPreviousBeginInstallError[] =
    "* does not match a previous call to beginInstall";
const char kUserCancelledError[] = "User cancelled install";
const char kUserGestureRequiredError[] =
    "This function must be called during a user gesture";

ProfileSyncService* test_sync_service = NULL;
BrowserSignin* test_signin = NULL;
bool ignore_user_gesture_for_tests = false;

// Returns either the test sync service, or the real one from |profile|.
ProfileSyncService* GetSyncService(Profile* profile) {
  if (test_sync_service)
    return test_sync_service;
  else
    return profile->GetProfileSyncService();
}

BrowserSignin* GetBrowserSignin(Profile* profile) {
  if (test_signin)
    return test_signin;
  else
    return profile->GetBrowserSignin();
}

bool IsWebStoreURL(Profile* profile, const GURL& url) {
  ExtensionService* service = profile->GetExtensionService();
  const Extension* store = service->GetWebStoreApp();
  if (!store) {
    NOTREACHED();
    return false;
  }
  return (service->GetExtensionByWebExtent(url) == store);
}

// Helper to create a dictionary with login and token properties set from
// the appropriate values in the passed-in |profile|.
DictionaryValue* CreateLoginResult(Profile* profile) {
  DictionaryValue* dictionary = new DictionaryValue();
  std::string username = GetBrowserSignin(profile)->GetSignedInUsername();
  dictionary->SetString(kLoginKey, username);
  if (!username.empty()) {
    CommandLine* cmdline = CommandLine::ForCurrentProcess();
    TokenService* token_service = profile->GetTokenService();
    if (cmdline->HasSwitch(switches::kAppsGalleryReturnTokens) &&
        token_service->HasTokenForService(GaiaConstants::kGaiaService)) {
      dictionary->SetString(kTokenKey,
                            token_service->GetTokenForService(
                                GaiaConstants::kGaiaService));
    }
  }
  return dictionary;
}

// If |profile| is not incognito, returns it. Otherwise returns the real
// (not incognito) default profile.
Profile* GetDefaultProfile(Profile* profile) {
  if (!profile->IsOffTheRecord())
    return profile;
  else
    return g_browser_process->profile_manager()->GetDefaultProfile();
}

}  // namespace

// static
void WebstorePrivateApi::SetTestingProfileSyncService(
    ProfileSyncService* service) {
  test_sync_service = service;
}

// static
void WebstorePrivateApi::SetTestingBrowserSignin(BrowserSignin* signin) {
  test_signin = signin;
}

// static
void BeginInstallFunction::SetIgnoreUserGestureForTests(bool ignore) {
  ignore_user_gesture_for_tests = ignore;
}

bool BeginInstallFunction::RunImpl() {
  if (!IsWebStoreURL(profile_, source_url()))
    return false;

  std::string id;
  EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &id));
  if (!Extension::IdIsValid(id)) {
    error_ = kInvalidIdError;
    return false;
  }

  if (!user_gesture() && !ignore_user_gesture_for_tests) {
    error_ = kUserGestureRequiredError;
    return false;
  }

  // This gets cleared in CrxInstaller::ConfirmInstall(). TODO(asargent) - in
  // the future we may also want to add time-based expiration, where a whitelist
  // entry is only valid for some number of minutes.
  CrxInstaller::SetWhitelistedInstallId(id);
  return true;
}

// This is a class to help BeginInstallWithManifestFunction manage sending
// JSON manifests and base64-encoded icon data to the utility process for
// parsing.
class SafeBeginInstallHelper : public UtilityProcessHost::Client {
 public:
  SafeBeginInstallHelper(BeginInstallWithManifestFunction* client,
                         const std::string& icon_data,
                         const std::string& manifest)
      : client_(client),
        icon_data_(icon_data),
        manifest_(manifest),
        utility_host_(NULL),
        icon_decode_complete_(false),
        manifest_parse_complete_(false),
        parse_error_(BeginInstallWithManifestFunction::UNKNOWN_ERROR) {}

  void Start() {
    CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    BrowserThread::PostTask(
        BrowserThread::IO,
        FROM_HERE,
        NewRunnableMethod(this,
                          &SafeBeginInstallHelper::StartWorkOnIOThread));
  }

  void StartWorkOnIOThread() {
    CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
    utility_host_ = new UtilityProcessHost(this, BrowserThread::IO);
    utility_host_->StartBatchMode();
    if (icon_data_.empty())
      icon_decode_complete_ = true;
    else
      utility_host_->StartImageDecodingBase64(icon_data_);
    utility_host_->StartJSONParsing(manifest_);
  }

  // Implementing pieces of the UtilityProcessHost::Client interface.
  virtual void OnDecodeImageSucceeded(const SkBitmap& decoded_image) {
    CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
    icon_ = decoded_image;
    icon_decode_complete_ = true;
    ReportResultsIfComplete();
  }
  virtual void OnDecodeImageFailed() {
    CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
    icon_decode_complete_ = true;
    error_ = std::string(kImageDecodeError);
    parse_error_ = BeginInstallWithManifestFunction::ICON_ERROR;
    ReportResultsIfComplete();
  }
  virtual void OnJSONParseSucceeded(const ListValue& wrapper) {
    CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
    manifest_parse_complete_ = true;
    Value* value = NULL;
    CHECK(wrapper.Get(0, &value));
    if (value->IsType(Value::TYPE_DICTIONARY)) {
      parsed_manifest_.reset(
          static_cast<DictionaryValue*>(value)->DeepCopy());
    } else {
      parse_error_ = BeginInstallWithManifestFunction::MANIFEST_ERROR;
    }
    ReportResultsIfComplete();
  }

  virtual void OnJSONParseFailed(const std::string& error_message) {
    CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
    manifest_parse_complete_ = true;
    error_ = error_message;
    parse_error_ = BeginInstallWithManifestFunction::MANIFEST_ERROR;
    ReportResultsIfComplete();
  }

  void ReportResultsIfComplete() {
    CHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));

    if (!icon_decode_complete_ || !manifest_parse_complete_)
      return;

    // The utility_host_ will take care of deleting itself after this call.
    utility_host_->EndBatchMode();
    utility_host_ = NULL;

    BrowserThread::PostTask(
        BrowserThread::UI,
        FROM_HERE,
        NewRunnableMethod(this,
                          &SafeBeginInstallHelper::ReportResultFromUIThread));
  }

  void ReportResultFromUIThread() {
    CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    if (error_.empty() && parsed_manifest_.get())
      client_->OnParseSuccess(icon_, parsed_manifest_.release());
    else
      client_->OnParseFailure(parse_error_, error_);
  }

 private:
  ~SafeBeginInstallHelper() {}

  // The client who we'll report results back to.
  BeginInstallWithManifestFunction* client_;

  // The data to parse.
  std::string icon_data_;
  std::string manifest_;

  UtilityProcessHost* utility_host_;

  // Flags for whether we're done doing icon decoding and manifest parsing.
  bool icon_decode_complete_;
  bool manifest_parse_complete_;

  // The results of succesful decoding/parsing.
  SkBitmap icon_;
  scoped_ptr<DictionaryValue> parsed_manifest_;

  // A details string for keeping track of any errors.
  std::string error_;

  // A code to distinguish between an error with the icon, and an error with the
  // manifest.
  BeginInstallWithManifestFunction::ResultCode parse_error_;
};

BeginInstallWithManifestFunction::BeginInstallWithManifestFunction() {}

BeginInstallWithManifestFunction::~BeginInstallWithManifestFunction() {}

bool BeginInstallWithManifestFunction::RunImpl() {
  if (!IsWebStoreURL(profile_, source_url())) {
    SetResult(PERMISSION_DENIED);
    return false;
  }

  if (!user_gesture() && !ignore_user_gesture_for_tests) {
    SetResult(NO_GESTURE);
    error_ = kUserGestureRequiredError;
    return false;
  }

  EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &id_));
  if (!Extension::IdIsValid(id_)) {
    SetResult(INVALID_ID);
    error_ = kInvalidIdError;
    return false;
  }

  EXTENSION_FUNCTION_VALIDATE(args_->GetString(1, &icon_data_));
  EXTENSION_FUNCTION_VALIDATE(args_->GetString(2, &manifest_));

  scoped_refptr<SafeBeginInstallHelper> helper =
      new SafeBeginInstallHelper(this, icon_data_, manifest_);
  // The helper will call us back via OnParseSucces or OnParseFailure.
  helper->Start();

  // Matched with a Release in OnSuccess/OnFailure.
  AddRef();

  // The response is sent asynchronously in OnSuccess/OnFailure.
  return true;
}


void BeginInstallWithManifestFunction::SetResult(ResultCode code) {
  switch (code) {
    case ERROR_NONE:
      result_.reset(Value::CreateStringValue(""));
      break;
    case UNKNOWN_ERROR:
      result_.reset(Value::CreateStringValue("unknown_error"));
      break;
    case USER_CANCELLED:
      result_.reset(Value::CreateStringValue("user_cancelled"));
      break;
    case MANIFEST_ERROR:
      result_.reset(Value::CreateStringValue("manifest_error"));
      break;
    case ICON_ERROR:
      result_.reset(Value::CreateStringValue("icon_error"));
      break;
    case INVALID_ID:
      result_.reset(Value::CreateStringValue("invalid_id"));
      break;
    case PERMISSION_DENIED:
      result_.reset(Value::CreateStringValue("permission_denied"));
      break;
    case NO_GESTURE:
      result_.reset(Value::CreateStringValue("no_gesture"));
      break;
    default:
      CHECK(false);
  }
}


void BeginInstallWithManifestFunction::OnParseSuccess(
    const SkBitmap& icon, DictionaryValue* parsed_manifest) {
  CHECK(parsed_manifest);
  icon_ = icon;
  parsed_manifest_.reset(parsed_manifest);

  // Create a dummy extension and show the extension install confirmation
  // dialog.
  std::string init_errors;
  dummy_extension_ = Extension::Create(
      FilePath(),
      Extension::INTERNAL,
      *static_cast<DictionaryValue*>(parsed_manifest_.get()),
      Extension::NO_FLAGS,
      &init_errors);
  if (!dummy_extension_.get()) {
    OnParseFailure(MANIFEST_ERROR, std::string(kInvalidManifestError));
    return;
  }
  if (icon_.empty())
    icon_ = Extension::GetDefaultIcon(dummy_extension_->is_app());

  ShowExtensionInstallDialog(profile(),
                             this,
                             dummy_extension_.get(),
                             &icon_,
                             dummy_extension_->GetPermissionMessageStrings(),
                             ExtensionInstallUI::INSTALL_PROMPT);

  // Control flow finishes up in InstallUIProceed or InstallUIAbort.
}

void BeginInstallWithManifestFunction::OnParseFailure(
    ResultCode result_code, const std::string& error_message) {
  SetResult(result_code);
  error_ = error_message;
  SendResponse(false);

  // Matches the AddRef in RunImpl().
  Release();
}

void BeginInstallWithManifestFunction::InstallUIProceed() {
  CrxInstaller::SetWhitelistedManifest(id_, parsed_manifest_.release());
  SetResult(ERROR_NONE);
  SendResponse(true);

  // Matches the AddRef in RunImpl().
  Release();
}

void BeginInstallWithManifestFunction::InstallUIAbort() {
  error_ = std::string(kUserCancelledError);
  SetResult(USER_CANCELLED);
  SendResponse(false);

  // Matches the AddRef in RunImpl().
  Release();
}

bool CompleteInstallFunction::RunImpl() {
  if (!IsWebStoreURL(profile_, source_url()))
    return false;

  std::string id;
  EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &id));
  if (!Extension::IdIsValid(id)) {
    error_ = kInvalidIdError;
    return false;
  }

  if (!CrxInstaller::IsIdWhitelisted(id) &&
      !CrxInstaller::GetWhitelistedManifest(id)) {
    error_ = ExtensionErrorUtils::FormatErrorMessage(
        kNoPreviousBeginInstallError, id);
    return false;
  }

  std::vector<std::string> params;
  params.push_back("id=" + id);
  params.push_back("lang=" + g_browser_process->GetApplicationLocale());
  params.push_back("uc");
  std::string url_string = Extension::GalleryUpdateUrl(true).spec();

  GURL url(url_string + "?response=redirect&x=" +
      EscapeQueryParamValue(JoinString(params, '&'), true));
  DCHECK(url.is_valid());

  // The download url for the given |id| is now contained in |url|. We
  // navigate the current (calling) tab to this url which will result in a
  // download starting. Once completed it will go through the normal extension
  // install flow. The above call to SetWhitelistedInstallId will bypass the
  // normal permissions install dialog.
  NavigationController& controller =
      dispatcher()->delegate()->associated_tab_contents()->controller();
  controller.LoadURL(url, source_url(), PageTransition::LINK);

  return true;
}

bool GetBrowserLoginFunction::RunImpl() {
  if (!IsWebStoreURL(profile_, source_url()))
    return false;
  result_.reset(CreateLoginResult(GetDefaultProfile(profile_)));
  return true;
}

bool GetStoreLoginFunction::RunImpl() {
  if (!IsWebStoreURL(profile_, source_url()))
    return false;
  ExtensionService* service = profile_->GetExtensionService();
  ExtensionPrefs* prefs = service->extension_prefs();
  std::string login;
  if (prefs->GetWebStoreLogin(&login)) {
    result_.reset(Value::CreateStringValue(login));
  } else {
    result_.reset(Value::CreateStringValue(std::string()));
  }
  return true;
}

bool SetStoreLoginFunction::RunImpl() {
  if (!IsWebStoreURL(profile_, source_url()))
    return false;
  std::string login;
  EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &login));
  ExtensionService* service = profile_->GetExtensionService();
  ExtensionPrefs* prefs = service->extension_prefs();
  prefs->SetWebStoreLogin(login);
  return true;
}

PromptBrowserLoginFunction::PromptBrowserLoginFunction()
    : waiting_for_token_(false) {}

PromptBrowserLoginFunction::~PromptBrowserLoginFunction() {
}

bool PromptBrowserLoginFunction::RunImpl() {
  if (!IsWebStoreURL(profile_, source_url()))
    return false;

  std::string preferred_email;
  if (args_->GetSize() > 0) {
    EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &preferred_email));
  }

  Profile* profile = GetDefaultProfile(profile_);

  // Login can currently only be invoked tab-modal.  Since this is
  // coming from the webstore, we should always have a tab, but check
  // just in case.
  TabContents* tab = dispatcher()->delegate()->associated_tab_contents();
  if (!tab)
    return false;

  // We return the result asynchronously, so we addref to keep ourself alive.
  // Matched with a Release in OnLoginSuccess() and OnLoginFailure().
  AddRef();

  // Start listening for notifications about the token.
  TokenService* token_service = profile->GetTokenService();
  registrar_.Add(this,
                 NotificationType::TOKEN_AVAILABLE,
                 Source<TokenService>(token_service));
  registrar_.Add(this,
                 NotificationType::TOKEN_REQUEST_FAILED,
                 Source<TokenService>(token_service));

  GetBrowserSignin(profile)->RequestSignin(tab,
                                           ASCIIToUTF16(preferred_email),
                                           GetLoginMessage(),
                                           this);

  // The response will be sent asynchronously in OnLoginSuccess/OnLoginFailure.
  return true;
}

string16 PromptBrowserLoginFunction::GetLoginMessage() {
  using l10n_util::GetStringUTF16;
  using l10n_util::GetStringFUTF16;

  // TODO(johnnyg): This would be cleaner as an HTML template.
  // http://crbug.com/60216
  string16 message;
  message = ASCIIToUTF16("<p>")
      + GetStringUTF16(IDS_WEB_STORE_LOGIN_INTRODUCTION_1)
      + ASCIIToUTF16("</p>");
  message = message + ASCIIToUTF16("<p>")
      + GetStringFUTF16(IDS_WEB_STORE_LOGIN_INTRODUCTION_2,
                        GetStringUTF16(IDS_PRODUCT_NAME))
      + ASCIIToUTF16("</p>");
  return message;
}

void PromptBrowserLoginFunction::OnLoginSuccess() {
  // Ensure that apps are synced.
  // - If the user has already setup sync, we add Apps to the current types.
  // - If not, we create a new set which is just Apps.
  ProfileSyncService* service = GetSyncService(GetDefaultProfile(profile_));
  syncable::ModelTypeSet types;
  if (service->HasSyncSetupCompleted())
    service->GetPreferredDataTypes(&types);
  types.insert(syncable::APPS);
  service->ChangePreferredDataTypes(types);
  service->SetSyncSetupCompleted();

  // We'll finish up in Observe() when the token is ready.
  waiting_for_token_ = true;
}

void PromptBrowserLoginFunction::OnLoginFailure(
    const GoogleServiceAuthError& error) {
  SendResponse(false);
  // Matches the AddRef in RunImpl().
  Release();
}

void PromptBrowserLoginFunction::Observe(NotificationType type,
                                         const NotificationSource& source,
                                         const NotificationDetails& details) {
  // Make sure this notification is for the service we are interested in.
  std::string service;
  if (type == NotificationType::TOKEN_AVAILABLE) {
    TokenService::TokenAvailableDetails* available =
        Details<TokenService::TokenAvailableDetails>(details).ptr();
    service = available->service();
  } else if (type == NotificationType::TOKEN_REQUEST_FAILED) {
    TokenService::TokenRequestFailedDetails* failed =
        Details<TokenService::TokenRequestFailedDetails>(details).ptr();
    service = failed->service();
  } else {
    NOTREACHED();
  }

  if (service != GaiaConstants::kGaiaService) {
    return;
  }

  DCHECK(waiting_for_token_);

  result_.reset(CreateLoginResult(GetDefaultProfile(profile_)));
  SendResponse(true);

  // Matches the AddRef in RunImpl().
  Release();
}