// 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();
}