Commit f6ea63a2 authored by rdevlin.cronin's avatar rdevlin.cronin Committed by Commit bot

[Extensions] Make chrome://extensions use developerPrivate for unpacked loading

Make the chrome://extensions page use chrome.developerPrivate API to
select a file path and load an unpacked extension.
Also add tests for the api function, and convert it to a
UIThreadExtensionFunction.

BUG=461039

Review URL: https://codereview.chromium.org/979453002

Cr-Commit-Position: refs/heads/master@{#319081}
parent 31f4de69
......@@ -756,7 +756,9 @@ ExtensionFunction::ResponseAction DeveloperPrivateReloadFunction::Run() {
if (!extension)
return RespondNow(Error(kNoSuchExtensionError));
bool fail_quietly = params->options && params->options->fail_quietly;
bool fail_quietly = params->options &&
params->options->fail_quietly &&
*params->options->fail_quietly;
ExtensionService* service = GetExtensionService(browser_context());
if (fail_quietly)
......@@ -871,7 +873,15 @@ bool DeveloperPrivateInspectFunction::RunSync() {
DeveloperPrivateInspectFunction::~DeveloperPrivateInspectFunction() {}
DeveloperPrivateLoadUnpackedFunction::DeveloperPrivateLoadUnpackedFunction()
: fail_quietly_(false) {
}
ExtensionFunction::ResponseAction DeveloperPrivateLoadUnpackedFunction::Run() {
scoped_ptr<developer_private::LoadUnpacked::Params> params(
developer_private::LoadUnpacked::Params::Create(*args_));
EXTENSION_FUNCTION_VALIDATE(params);
if (!ShowPicker(
ui::SelectFileDialog::SELECT_FOLDER,
l10n_util::GetStringUTF16(IDS_EXTENSION_LOAD_FROM_DIRECTORY),
......@@ -880,16 +890,25 @@ ExtensionFunction::ResponseAction DeveloperPrivateLoadUnpackedFunction::Run() {
return RespondNow(Error(kCouldNotShowSelectFileDialogError));
}
fail_quietly_ = params->options &&
params->options->fail_quietly &&
*params->options->fail_quietly;
AddRef(); // Balanced in FileSelected / FileSelectionCanceled.
return RespondLater();
}
void DeveloperPrivateLoadUnpackedFunction::FileSelected(
const base::FilePath& path) {
UnpackedInstaller::Create(GetExtensionService(browser_context()))->Load(path);
scoped_refptr<UnpackedInstaller> installer(
UnpackedInstaller::Create(GetExtensionService(browser_context())));
installer->set_be_noisy_on_failure(!fail_quietly_);
installer->set_completion_callback(
base::Bind(&DeveloperPrivateLoadUnpackedFunction::OnLoadComplete, this));
installer->Load(path);
DeveloperPrivateAPI::Get(browser_context())->SetLastUnpackedDirectory(path);
// TODO(devlin): Shouldn't we wait until the extension is loaded?
Respond(NoArguments());
Release(); // Balanced in Run().
}
......@@ -900,6 +919,13 @@ void DeveloperPrivateLoadUnpackedFunction::FileSelectionCanceled() {
Release(); // Balanced in Run().
}
void DeveloperPrivateLoadUnpackedFunction::OnLoadComplete(
const Extension* extension,
const base::FilePath& file_path,
const std::string& error) {
Respond(extension ? NoArguments() : Error(error));
}
bool DeveloperPrivateChooseEntryFunction::ShowPicker(
ui::SelectFileDialog::Type picker_type,
const base::string16& select_title,
......
......@@ -312,6 +312,7 @@ class DeveloperPrivateLoadUnpackedFunction
public:
DECLARE_EXTENSION_FUNCTION("developerPrivate.loadUnpacked",
DEVELOPERPRIVATE_LOADUNPACKED);
DeveloperPrivateLoadUnpackedFunction();
protected:
~DeveloperPrivateLoadUnpackedFunction() override;
......@@ -320,6 +321,15 @@ class DeveloperPrivateLoadUnpackedFunction
// EntryPickerClient:
void FileSelected(const base::FilePath& path) override;
void FileSelectionCanceled() override;
// Callback for the UnpackedLoader.
void OnLoadComplete(const Extension* extension,
const base::FilePath& file_path,
const std::string& error);
private:
// Whether or not we should fail quietly in the event of a load error.
bool fail_quietly_;
};
class DeveloperPrivateChoosePathFunction
......
......@@ -9,21 +9,32 @@
#include "chrome/browser/extensions/extension_service_test_base.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/test_extension_dir.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/browser/extensions/unpacked_installer.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/host_desktop.h"
#include "chrome/common/extensions/api/developer_private.h"
#include "chrome/test/base/test_browser_window.h"
#include "content/public/test/test_web_contents_factory.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_set.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/test_util.h"
namespace extensions {
namespace {
KeyedService* BuildAPI(content::BrowserContext* context) {
return new DeveloperPrivateAPI(context);
}
} // namespace
class DeveloperPrivateApiUnitTest : public ExtensionServiceTestBase {
protected:
DeveloperPrivateApiUnitTest() {}
......@@ -180,6 +191,13 @@ void DeveloperPrivateApiUnitTest::SetUp() {
params.type = Browser::TYPE_TABBED;
params.window = browser_window_.get();
browser_.reset(new Browser(params));
// Allow the API to be created.
static_cast<TestExtensionSystem*>(ExtensionSystem::Get(profile()))->
SetEventRouter(make_scoped_ptr(
new EventRouter(profile(), ExtensionPrefs::Get(profile()))));
DeveloperPrivateAPI::GetFactoryInstance()->SetTestingFactory(
profile(), &BuildAPI);
}
void DeveloperPrivateApiUnitTest::TearDown() {
......@@ -264,4 +282,94 @@ TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivatePackFunction) {
base::DeleteFile(pem_path, false);
}
// Test developerPrivate.choosePath.
TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivateChoosePath) {
ResetThreadBundle(content::TestBrowserThreadBundle::DEFAULT);
content::TestWebContentsFactory web_contents_factory;
content::WebContents* web_contents =
web_contents_factory.CreateWebContents(profile());
base::FilePath expected_dir_path = data_dir().AppendASCII("good_unpacked");
api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&expected_dir_path);
// Try selecting a directory.
base::ListValue choose_args;
choose_args.AppendString("FOLDER");
choose_args.AppendString("LOAD");
scoped_refptr<UIThreadExtensionFunction> function(
new api::DeveloperPrivateChoosePathFunction());
function->SetRenderViewHost(web_contents->GetRenderViewHost());
EXPECT_TRUE(RunFunction(function, choose_args)) << function->GetError();
std::string path;
EXPECT_TRUE(function->GetResultList() &&
function->GetResultList()->GetString(0, &path));
EXPECT_EQ(path, expected_dir_path.AsUTF8Unsafe());
// Try selecting a pem file.
base::FilePath expected_file_path =
data_dir().AppendASCII("good_unpacked.pem");
api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&expected_file_path);
choose_args.Clear();
choose_args.AppendString("FILE");
choose_args.AppendString("PEM");
function = new api::DeveloperPrivateChoosePathFunction();
function->SetRenderViewHost(web_contents->GetRenderViewHost());
EXPECT_TRUE(RunFunction(function, choose_args)) << function->GetError();
EXPECT_TRUE(function->GetResultList() &&
function->GetResultList()->GetString(0, &path));
EXPECT_EQ(path, expected_file_path.AsUTF8Unsafe());
// Try canceling the file dialog.
api::EntryPicker::SkipPickerAndAlwaysCancelForTest();
function = new api::DeveloperPrivateChoosePathFunction();
function->SetRenderViewHost(web_contents->GetRenderViewHost());
EXPECT_FALSE(RunFunction(function, choose_args));
EXPECT_EQ(std::string("File selection was canceled."), function->GetError());
}
// Test developerPrivate.loadUnpacked.
TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivateLoadUnpacked) {
ResetThreadBundle(content::TestBrowserThreadBundle::DEFAULT);
content::TestWebContentsFactory web_contents_factory;
content::WebContents* web_contents =
web_contents_factory.CreateWebContents(profile());
base::FilePath path = data_dir().AppendASCII("good_unpacked");
api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&path);
// Try loading a good extension (it should succeed, and the extension should
// be added).
scoped_refptr<UIThreadExtensionFunction> function(
new api::DeveloperPrivateLoadUnpackedFunction());
function->SetRenderViewHost(web_contents->GetRenderViewHost());
ExtensionIdSet current_ids = registry()->enabled_extensions().GetIDs();
EXPECT_TRUE(RunFunction(function, base::ListValue())) << function->GetError();
// We should have added one new extension.
ExtensionIdSet id_difference = base::STLSetDifference<ExtensionIdSet>(
registry()->enabled_extensions().GetIDs(), current_ids);
ASSERT_EQ(1u, id_difference.size());
// The new extension should have the same path.
EXPECT_EQ(
path,
registry()->enabled_extensions().GetByID(*id_difference.begin())->path());
path = data_dir().AppendASCII("empty_manifest");
api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&path);
// Try loading a bad extension (it should fail, and we should get an error).
function = new api::DeveloperPrivateLoadUnpackedFunction();
function->SetRenderViewHost(web_contents->GetRenderViewHost());
base::ListValue unpacked_args;
scoped_ptr<base::DictionaryValue> options(new base::DictionaryValue());
options->SetBoolean("failQuietly", true);
unpacked_args.Append(options.release());
current_ids = registry()->enabled_extensions().GetIDs();
EXPECT_FALSE(RunFunction(function, unpacked_args));
EXPECT_EQ(manifest_errors::kManifestUnreadable, function->GetError());
// We should have no new extensions installed.
EXPECT_EQ(0u, base::STLSetDifference<ExtensionIdSet>(
registry()->enabled_extensions().GetIDs(),
current_ids).size());
}
} // namespace extensions
......@@ -32,29 +32,29 @@ EntryPicker::EntryPicker(EntryPickerClient* client,
const ui::SelectFileDialog::FileTypeInfo& info,
int file_type_index)
: client_(client) {
select_file_dialog_ = ui::SelectFileDialog::Create(
this, new ChromeSelectFilePolicy(web_contents));
gfx::NativeWindow owning_window = web_contents ?
platform_util::GetTopLevel(web_contents->GetNativeView()) :
NULL;
if (g_skip_picker_for_test) {
if (g_path_to_be_picked_for_test) {
content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE,
base::Bind(
&EntryPicker::FileSelected,
base::Unretained(this), *g_path_to_be_picked_for_test, 1,
static_cast<void*>(NULL)));
static_cast<void*>(nullptr)));
} else {
content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE,
base::Bind(
&EntryPicker::FileSelectionCanceled,
base::Unretained(this), static_cast<void*>(NULL)));
base::Unretained(this), static_cast<void*>(nullptr)));
}
return;
}
select_file_dialog_ = ui::SelectFileDialog::Create(
this, new ChromeSelectFilePolicy(web_contents));
gfx::NativeWindow owning_window = web_contents ?
platform_util::GetTopLevel(web_contents->GetNativeView()) :
nullptr;
select_file_dialog_->SelectFile(picker_type,
select_title,
last_directory,
......@@ -62,7 +62,7 @@ EntryPicker::EntryPicker(EntryPickerClient* client,
file_type_index,
base::FilePath::StringType(),
owning_window,
NULL);
nullptr);
}
EntryPicker::~EntryPicker() {}
......
......@@ -350,6 +350,11 @@ void UnpackedInstaller::ReportExtensionLoadError(const std::string &error) {
service_weak_->profile(),
be_noisy_on_failure_);
}
if (!callback_.is_null()) {
callback_.Run(nullptr, extension_path_, error);
callback_.Reset();
}
}
void UnpackedInstaller::InstallExtension() {
......@@ -361,6 +366,11 @@ void UnpackedInstaller::InstallExtension() {
service_weak_->OnExtensionInstalled(
extension(), syncer::StringOrdinal(), kInstallFlagInstallImmediately);
if (!callback_.is_null()) {
callback_.Run(extension(), extension_path_, std::string());
callback_.Reset();
}
}
} // namespace extensions
......@@ -29,8 +29,9 @@ class Extension;
class UnpackedInstaller
: public base::RefCountedThreadSafe<UnpackedInstaller> {
public:
typedef base::Callback<void(const base::FilePath&, const std::string&)>
OnFailureCallback;
using CompletionCallback = base::Callback<void(const Extension* extension,
const base::FilePath&,
const std::string&)>;
static scoped_refptr<UnpackedInstaller> Create(
ExtensionService* extension_service);
......@@ -68,6 +69,10 @@ class UnpackedInstaller
be_noisy_on_failure_ = be_noisy_on_failure;
}
void set_completion_callback(const CompletionCallback& callback) {
callback_ = callback;
}
private:
friend class base::RefCountedThreadSafe<UnpackedInstaller>;
......@@ -133,6 +138,8 @@ class UnpackedInstaller
// installed.
ExtensionInstallChecker install_checker_;
CompletionCallback callback_;
DISALLOW_COPY_AND_ASSIGN(UnpackedInstaller);
};
......
......@@ -185,12 +185,26 @@ cr.define('extensions', function() {
cr.addSingletonGetter(ExtensionLoader);
ExtensionLoader.prototype = {
/**
* Whether or not we are currently loading an unpacked extension.
* @private {boolean}
*/
isLoading_: false,
/**
* Begin the sequence of loading an unpacked extension. If an error is
* encountered, this object will get notified via notifyFailed().
*/
loadUnpacked: function() {
chrome.send('extensionLoaderLoadUnpacked');
if (this.isLoading_) // Only one running load at a time.
return;
this.isLoading_ = true;
chrome.developerPrivate.loadUnpacked({failQuietly: true}, function() {
// Check lastError to avoid the log, but don't do anything with it -
// error-handling is done on the C++ side.
var lastError = chrome.runtime.lastError;
this.isLoading_ = false;
}.bind(this));
},
/**
......
......@@ -14,12 +14,9 @@
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/path_util.h"
#include "chrome/browser/extensions/unpacked_installer.h"
#include "chrome/browser/extensions/zipfile_installer.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/chrome_select_file_policy.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/user_metrics.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_data_source.h"
......@@ -30,7 +27,6 @@
#include "extensions/common/manifest_constants.h"
#include "third_party/re2/re2/re2.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/shell_dialogs/select_file_dialog.h"
namespace extensions {
......@@ -47,96 +43,8 @@ std::string ReadFileToString(const base::FilePath& path) {
} // namespace
class ExtensionLoaderHandler::FileHelper
: public ui::SelectFileDialog::Listener {
public:
explicit FileHelper(ExtensionLoaderHandler* loader_handler);
~FileHelper() override;
// Create a FileDialog for the user to select the unpacked extension
// directory.
void ChooseFile();
private:
// ui::SelectFileDialog::Listener implementation.
void FileSelected(const base::FilePath& path,
int index,
void* params) override;
void MultiFilesSelected(const std::vector<base::FilePath>& files,
void* params) override;
// The associated ExtensionLoaderHandler. Weak, but guaranteed to be alive,
// as it owns this object.
ExtensionLoaderHandler* loader_handler_;
// The dialog used to pick a directory when loading an unpacked extension.
scoped_refptr<ui::SelectFileDialog> load_extension_dialog_;
// The last selected directory, so we can start in the same spot.
base::FilePath last_unpacked_directory_;
// The title of the dialog.
base::string16 title_;
DISALLOW_COPY_AND_ASSIGN(FileHelper);
};
ExtensionLoaderHandler::FileHelper::FileHelper(
ExtensionLoaderHandler* loader_handler)
: loader_handler_(loader_handler),
title_(l10n_util::GetStringUTF16(IDS_EXTENSION_LOAD_FROM_DIRECTORY)) {
}
ExtensionLoaderHandler::FileHelper::~FileHelper() {
// There may be a pending file dialog; inform it the listener is destroyed so
// it doesn't try and call back.
if (load_extension_dialog_.get())
load_extension_dialog_->ListenerDestroyed();
}
void ExtensionLoaderHandler::FileHelper::ChooseFile() {
static const int kFileTypeIndex = 0; // No file type information to index.
static const ui::SelectFileDialog::Type kSelectType =
ui::SelectFileDialog::SELECT_FOLDER;
gfx::NativeWindow parent_window =
loader_handler_->web_ui()->GetWebContents()->GetTopLevelNativeWindow();
if (!load_extension_dialog_.get()) {
load_extension_dialog_ = ui::SelectFileDialog::Create(
this,
new ChromeSelectFilePolicy(
loader_handler_->web_ui()->GetWebContents()));
} else if (load_extension_dialog_->IsRunning(parent_window)) {
// File chooser dialog is already running; ignore the click.
return;
}
load_extension_dialog_->SelectFile(
kSelectType,
title_,
last_unpacked_directory_,
NULL,
kFileTypeIndex,
base::FilePath::StringType(),
parent_window,
NULL);
content::RecordComputedAction("Options_LoadUnpackedExtension");
}
void ExtensionLoaderHandler::FileHelper::FileSelected(
const base::FilePath& path, int index, void* params) {
loader_handler_->LoadUnpackedExtensionImpl(path);
}
void ExtensionLoaderHandler::FileHelper::MultiFilesSelected(
const std::vector<base::FilePath>& files, void* params) {
NOTREACHED();
}
ExtensionLoaderHandler::ExtensionLoaderHandler(Profile* profile)
: profile_(profile),
file_helper_(new FileHelper(this)),
extension_error_reporter_observer_(this),
ui_ready_(false),
weak_ptr_factory_(this) {
......@@ -176,10 +84,6 @@ void ExtensionLoaderHandler::RegisterMessages() {
// on page load and persists between refreshes.
content::WebContentsObserver::Observe(web_ui()->GetWebContents());
web_ui()->RegisterMessageCallback(
"extensionLoaderLoadUnpacked",
base::Bind(&ExtensionLoaderHandler::HandleLoadUnpacked,
weak_ptr_factory_.GetWeakPtr()));
web_ui()->RegisterMessageCallback(
"extensionLoaderRetry",
base::Bind(&ExtensionLoaderHandler::HandleRetry,
......@@ -194,16 +98,11 @@ void ExtensionLoaderHandler::RegisterMessages() {
weak_ptr_factory_.GetWeakPtr()));
}
void ExtensionLoaderHandler::HandleLoadUnpacked(const base::ListValue* args) {
DCHECK(args->empty());
file_helper_->ChooseFile();
}
void ExtensionLoaderHandler::HandleRetry(const base::ListValue* args) {
DCHECK(args->empty());
const base::FilePath file_path = failed_paths_.back();
failed_paths_.pop_back();
LoadUnpackedExtensionImpl(file_path);
LoadUnpackedExtension(file_path);
}
void ExtensionLoaderHandler::HandleIgnoreFailure(const base::ListValue* args) {
......@@ -222,8 +121,8 @@ void ExtensionLoaderHandler::HandleDisplayFailures(
NotifyFrontendOfFailure();
}
void ExtensionLoaderHandler::LoadUnpackedExtensionImpl(
const base::FilePath& file_path) {
void ExtensionLoaderHandler::LoadUnpackedExtension(
const base::FilePath& file_path) {
scoped_refptr<UnpackedInstaller> installer = UnpackedInstaller::Create(
ExtensionSystem::Get(profile_)->extension_service());
......
......@@ -45,11 +45,6 @@ class ExtensionLoaderHandler : public content::WebUIMessageHandler,
void RegisterMessages() override;
private:
class FileHelper;
// Handle the 'extensionLoaderLoadUnpacked' message.
void HandleLoadUnpacked(const base::ListValue* args);
// Handle the 'extensionLoaderRetry' message.
void HandleRetry(const base::ListValue* args);
......@@ -60,7 +55,7 @@ class ExtensionLoaderHandler : public content::WebUIMessageHandler,
void HandleDisplayFailures(const base::ListValue* args);
// Try to load an unpacked extension from the given |file_path|.
void LoadUnpackedExtensionImpl(const base::FilePath& file_path);
void LoadUnpackedExtension(const base::FilePath& file_path);
// ExtensionErrorReporter::Observer:
void OnLoadFailure(content::BrowserContext* browser_context,
......@@ -86,9 +81,6 @@ class ExtensionLoaderHandler : public content::WebUIMessageHandler,
// The profile with which this Handler is associated.
Profile* profile_;
// A helper to manage file picking.
scoped_ptr<FileHelper> file_helper_;
// Holds information about all unpacked extension install failures that
// were reported while the extensions page was loading.
base::ListValue failures_;
......
......@@ -78,7 +78,13 @@ namespace developerPrivate {
dictionary ReloadOptions {
// If false, an alert dialog will show in the event of a reload error.
// Defaults to false.
boolean failQuietly;
boolean? failQuietly;
};
dictionary LoadUnpackedOptions {
// If false, an alert dialog will show in the event of a reload error.
// Defaults to false.
boolean? failQuietly;
};
enum PackStatus {
......@@ -247,7 +253,9 @@ namespace developerPrivate {
optional VoidCallback callback);
// Loads a user-selected unpacked item.
static void loadUnpacked(optional VoidCallback callback);
// |options| : Additional configuration parameters.
static void loadUnpacked(optional LoadUnpackedOptions options,
optional VoidCallback callback);
// Loads an extension / app.
// |directory| : The directory to load the extension from.
......
......@@ -76,11 +76,18 @@ var InspectOptions;
/**
* @typedef {{
* failQuietly: boolean
* failQuietly: (boolean|undefined)
* }}
*/
var ReloadOptions;
/**
* @typedef {{
* failQuietly: (boolean|undefined)
* }}
*/
var LoadUnpackedOptions;
/**
* @enum {string}
*/
......@@ -251,9 +258,10 @@ chrome.developerPrivate.allowIncognito = function(extensionId, allow, callback)
/**
* Loads a user-selected unpacked item.
* @param {LoadUnpackedOptions=} options Additional configuration parameters.
* @param {Function=} callback
*/
chrome.developerPrivate.loadUnpacked = function(callback) {};
chrome.developerPrivate.loadUnpacked = function(options, callback) {};
/**
* Loads an extension / app.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment