Commit f6a37d04 authored by Devlin Cronin's avatar Devlin Cronin Committed by Commit Bot

[MD Extensions] Support drag-and-drop errors for dropped directories

Support displaying the errors dialog from drag-and-dropped directories.
To do this, notify the developerPrivate API when a drag begins to
cache the directory that is being dragged. Also add an additional
parameter to loadUnpacked, useDraggedPath, to use the cached dragged
path rather than prompting the user to choose a path or using a retryId.
When the user drops the directory, use this to load the extension.
Going through the loadUnpacked method like this has the advantage of
displaying load errors to the user and allowing them to retry.

Note that we cannot simply provide the filepath directly from the JS to
the C++ for security reasons (in the case of a compromised renderer, we
don't want to be able to add an arbitrary extension - this ensures that
the user dragged the extension over the extensions page).

The fact that the drag_and_drop_handler is also used by the non-MD
version complicates this slightly, because we have to work for both
versions of the code. This means there's a bit more mess than I'd
otherwise like. This can be cleaned up when MD extensions launches.

Bug: 788926

Cq-Include-Trybots: master.tryserver.chromium.linux:closure_compilation
Change-Id: I888b2094dcda1e964bca0f2680573387e714ea77
Reviewed-on: https://chromium-review.googlesource.com/809953
Commit-Queue: Devlin <rdevlin.cronin@chromium.org>
Reviewed-by: default avatarKaran Bhatia <karandeepb@chromium.org>
Reviewed-by: default avatarIlya Sherman <isherman@chromium.org>
Reviewed-by: default avatarDemetrios Papadopoulos <dpapad@chromium.org>
Cr-Commit-Position: refs/heads/master@{#524125}
parent 1c99c3db
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
#include "content/public/browser/site_instance.h" #include "content/public/browser/site_instance.h"
#include "content/public/browser/storage_partition.h" #include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents.h"
#include "content/public/common/drop_data.h"
#include "extensions/browser/api/file_handlers/app_file_handler_util.h" #include "extensions/browser/api/file_handlers/app_file_handler_util.h"
#include "extensions/browser/app_window/app_window.h" #include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/app_window_registry.h" #include "extensions/browser/app_window/app_window_registry.h"
...@@ -118,6 +119,8 @@ const char kCannotRepairPolicyExtension[] = ...@@ -118,6 +119,8 @@ const char kCannotRepairPolicyExtension[] =
const char kUnpackedAppsFolder[] = "apps_target"; const char kUnpackedAppsFolder[] = "apps_target";
const char kManifestFile[] = "manifest.json"; const char kManifestFile[] = "manifest.json";
base::FilePath* g_drop_path_for_testing = nullptr;
ExtensionService* GetExtensionService(content::BrowserContext* context) { ExtensionService* GetExtensionService(content::BrowserContext* context) {
return ExtensionSystem::Get(context)->extension_service(); return ExtensionSystem::Get(context)->extension_service();
} }
...@@ -235,7 +238,7 @@ class DeveloperPrivateAPI::WebContentsTracker ...@@ -235,7 +238,7 @@ class DeveloperPrivateAPI::WebContentsTracker
void WebContentsDestroyed() override { void WebContentsDestroyed() override {
if (api_) if (api_)
api_->allowed_unpacked_paths_.erase(web_contents()); api_->web_contents_data_.erase(web_contents());
delete this; delete this;
} }
...@@ -244,6 +247,11 @@ class DeveloperPrivateAPI::WebContentsTracker ...@@ -244,6 +247,11 @@ class DeveloperPrivateAPI::WebContentsTracker
DISALLOW_COPY_AND_ASSIGN(WebContentsTracker); DISALLOW_COPY_AND_ASSIGN(WebContentsTracker);
}; };
DeveloperPrivateAPI::WebContentsData::WebContentsData() = default;
DeveloperPrivateAPI::WebContentsData::~WebContentsData() = default;
DeveloperPrivateAPI::WebContentsData::WebContentsData(WebContentsData&& other) =
default;
// static // static
BrowserContextKeyedAPIFactory<DeveloperPrivateAPI>* BrowserContextKeyedAPIFactory<DeveloperPrivateAPI>*
DeveloperPrivateAPI::GetFactoryInstance() { DeveloperPrivateAPI::GetFactoryInstance() {
...@@ -462,21 +470,16 @@ DeveloperPrivateAPI::UnpackedRetryId DeveloperPrivateAPI::AddUnpackedPath( ...@@ -462,21 +470,16 @@ DeveloperPrivateAPI::UnpackedRetryId DeveloperPrivateAPI::AddUnpackedPath(
const base::FilePath& path) { const base::FilePath& path) {
DCHECK(web_contents); DCHECK(web_contents);
last_unpacked_directory_ = path; last_unpacked_directory_ = path;
IdToPathMap& paths = allowed_unpacked_paths_[web_contents]; WebContentsData* data = GetOrCreateWebContentsData(web_contents);
if (paths.empty()) { IdToPathMap& paths = data->allowed_unpacked_paths;
// This is the first we've added this WebContents. Track its lifetime so we auto existing =
// can clean up the paths when it is destroyed. std::find_if(paths.begin(), paths.end(),
// WebContentsTracker manages its own lifetime. [path](const std::pair<std::string, base::FilePath>& entry) {
new WebContentsTracker(weak_factory_.GetWeakPtr(), web_contents); return entry.second == path;
} else { });
auto existing = std::find_if( if (existing != paths.end())
paths.begin(), paths.end(), return existing->first;
[path](const std::pair<std::string, base::FilePath>& entry) {
return entry.second == path;
});
if (existing != paths.end())
return existing->first;
}
UnpackedRetryId id = base::GenerateGUID(); UnpackedRetryId id = base::GenerateGUID();
paths[id] = path; paths[id] = path;
return id; return id;
...@@ -485,21 +488,54 @@ DeveloperPrivateAPI::UnpackedRetryId DeveloperPrivateAPI::AddUnpackedPath( ...@@ -485,21 +488,54 @@ DeveloperPrivateAPI::UnpackedRetryId DeveloperPrivateAPI::AddUnpackedPath(
base::FilePath DeveloperPrivateAPI::GetUnpackedPath( base::FilePath DeveloperPrivateAPI::GetUnpackedPath(
content::WebContents* web_contents, content::WebContents* web_contents,
const UnpackedRetryId& id) const { const UnpackedRetryId& id) const {
auto iter = allowed_unpacked_paths_.find(web_contents); const WebContentsData* data = GetWebContentsData(web_contents);
if (iter == allowed_unpacked_paths_.end()) if (!data)
return base::FilePath(); return base::FilePath();
const IdToPathMap& paths = iter->second; const IdToPathMap& paths = data->allowed_unpacked_paths;
auto path_iter = paths.find(id); auto path_iter = paths.find(id);
if (path_iter == paths.end()) if (path_iter == paths.end())
return base::FilePath(); return base::FilePath();
return path_iter->second; return path_iter->second;
} }
void DeveloperPrivateAPI::SetDraggedPath(content::WebContents* web_contents,
const base::FilePath& dragged_path) {
WebContentsData* data = GetOrCreateWebContentsData(web_contents);
data->dragged_path = dragged_path;
}
base::FilePath DeveloperPrivateAPI::GetDraggedPath(
content::WebContents* web_contents) const {
const WebContentsData* data = GetWebContentsData(web_contents);
return data ? data->dragged_path : base::FilePath();
}
void DeveloperPrivateAPI::RegisterNotifications() { void DeveloperPrivateAPI::RegisterNotifications() {
EventRouter::Get(profile_)->RegisterObserver( EventRouter::Get(profile_)->RegisterObserver(
this, developer::OnItemStateChanged::kEventName); this, developer::OnItemStateChanged::kEventName);
} }
const DeveloperPrivateAPI::WebContentsData*
DeveloperPrivateAPI::GetWebContentsData(
content::WebContents* web_contents) const {
auto iter = web_contents_data_.find(web_contents);
return iter == web_contents_data_.end() ? nullptr : &iter->second;
}
DeveloperPrivateAPI::WebContentsData*
DeveloperPrivateAPI::GetOrCreateWebContentsData(
content::WebContents* web_contents) {
auto iter = web_contents_data_.find(web_contents);
if (iter != web_contents_data_.end())
return &iter->second;
// This is the first we've added this WebContents. Track its lifetime so we
// can clean up the paths when it is destroyed.
// WebContentsTracker manages its own lifetime.
new WebContentsTracker(weak_factory_.GetWeakPtr(), web_contents);
return &web_contents_data_[web_contents];
}
DeveloperPrivateAPI::~DeveloperPrivateAPI() {} DeveloperPrivateAPI::~DeveloperPrivateAPI() {}
void DeveloperPrivateAPI::Shutdown() {} void DeveloperPrivateAPI::Shutdown() {}
...@@ -910,6 +946,17 @@ ExtensionFunction::ResponseAction DeveloperPrivateLoadUnpackedFunction::Run() { ...@@ -910,6 +946,17 @@ ExtensionFunction::ResponseAction DeveloperPrivateLoadUnpackedFunction::Run() {
return RespondLater(); return RespondLater();
} }
if (params->options && params->options->use_dragged_path &&
*params->options->use_dragged_path) {
DeveloperPrivateAPI* api = DeveloperPrivateAPI::Get(browser_context());
base::FilePath path = api->GetDraggedPath(web_contents);
if (path.empty())
return RespondNow(Error("No dragged path"));
AddRef(); // Balanced in FileSelected.
FileSelected(path);
return RespondLater();
}
if (!ShowPicker(ui::SelectFileDialog::SELECT_FOLDER, if (!ShowPicker(ui::SelectFileDialog::SELECT_FOLDER,
l10n_util::GetStringUTF16(IDS_EXTENSION_LOAD_FROM_DIRECTORY), l10n_util::GetStringUTF16(IDS_EXTENSION_LOAD_FROM_DIRECTORY),
ui::SelectFileDialog::FileTypeInfo(), ui::SelectFileDialog::FileTypeInfo(),
...@@ -969,6 +1016,47 @@ void DeveloperPrivateLoadUnpackedFunction::OnGotManifestError( ...@@ -969,6 +1016,47 @@ void DeveloperPrivateLoadUnpackedFunction::OnGotManifestError(
.ToValue())); .ToValue()));
} }
DeveloperPrivateNotifyDragInstallInProgressFunction::
DeveloperPrivateNotifyDragInstallInProgressFunction() = default;
DeveloperPrivateNotifyDragInstallInProgressFunction::
~DeveloperPrivateNotifyDragInstallInProgressFunction() = default;
ExtensionFunction::ResponseAction
DeveloperPrivateNotifyDragInstallInProgressFunction::Run() {
content::WebContents* web_contents = GetSenderWebContents();
if (!web_contents)
return RespondNow(Error(kCouldNotFindWebContentsError));
const base::FilePath* file_path = nullptr;
if (g_drop_path_for_testing) {
file_path = g_drop_path_for_testing;
} else {
content::DropData* drop_data = web_contents->GetDropData();
if (!drop_data)
return RespondNow(Error("No current drop data."));
if (drop_data->filenames.empty())
return RespondNow(Error("No files being dragged."));
const ui::FileInfo& file_info = drop_data->filenames.front();
file_path = &file_info.path;
}
DCHECK(file_path);
// Note(devlin): we don't do further validation that the file is a directory
// here. This is validated in the JS, but if that fails, then trying to load
// the file as an unpacked extension will also fail (reasonably gracefully).
DeveloperPrivateAPI::Get(browser_context())
->SetDraggedPath(web_contents, *file_path);
return RespondNow(NoArguments());
}
// static
void DeveloperPrivateNotifyDragInstallInProgressFunction::SetDropPathForTesting(
base::FilePath* file_path) {
g_drop_path_for_testing = file_path;
}
bool DeveloperPrivateChooseEntryFunction::ShowPicker( bool DeveloperPrivateChooseEntryFunction::ShowPicker(
ui::SelectFileDialog::Type picker_type, ui::SelectFileDialog::Type picker_type,
const base::string16& select_title, const base::string16& select_title,
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
#ifndef CHROME_BROWSER_EXTENSIONS_API_DEVELOPER_PRIVATE_DEVELOPER_PRIVATE_API_H_ #ifndef CHROME_BROWSER_EXTENSIONS_API_DEVELOPER_PRIVATE_DEVELOPER_PRIVATE_API_H_
#define CHROME_BROWSER_EXTENSIONS_API_DEVELOPER_PRIVATE_DEVELOPER_PRIVATE_API_H_ #define CHROME_BROWSER_EXTENSIONS_API_DEVELOPER_PRIVATE_DEVELOPER_PRIVATE_API_H_
#include <map>
#include <set> #include <set>
#include "base/files/file.h" #include "base/files/file.h"
...@@ -187,6 +188,13 @@ class DeveloperPrivateAPI : public BrowserContextKeyedAPI, ...@@ -187,6 +188,13 @@ class DeveloperPrivateAPI : public BrowserContextKeyedAPI,
base::FilePath GetUnpackedPath(content::WebContents* web_contents, base::FilePath GetUnpackedPath(content::WebContents* web_contents,
const UnpackedRetryId& id) const; const UnpackedRetryId& id) const;
// Sets the dragged path for the given |web_contents|.
void SetDraggedPath(content::WebContents* web_contents,
const base::FilePath& path);
// Returns the dragged path for the given |web_contents|, if one exists.
base::FilePath GetDraggedPath(content::WebContents* web_contents) const;
// KeyedService implementation // KeyedService implementation
void Shutdown() override; void Shutdown() override;
...@@ -204,6 +212,30 @@ class DeveloperPrivateAPI : public BrowserContextKeyedAPI, ...@@ -204,6 +212,30 @@ class DeveloperPrivateAPI : public BrowserContextKeyedAPI,
private: private:
class WebContentsTracker; class WebContentsTracker;
using IdToPathMap = std::map<UnpackedRetryId, base::FilePath>;
// Data specific to a given WebContents.
struct WebContentsData {
WebContentsData();
~WebContentsData();
WebContentsData(WebContentsData&& other);
// A set of unpacked paths that we are allowed to load for different
// WebContents. For security reasons, we don't let JavaScript arbitrarily
// pass us a path and load the extension at that location; instead, the user
// has to explicitly select the path through a native dialog first, and then
// we will allow JavaScript to request we reload that same selected path.
// Additionally, these are segmented by WebContents; this is primarily to
// allow collection (removing old paths when the WebContents closes) but has
// the effect that WebContents A cannot retry a path selected in
// WebContents B.
IdToPathMap allowed_unpacked_paths;
// The last dragged path for the WebContents.
base::FilePath dragged_path;
DISALLOW_COPY_AND_ASSIGN(WebContentsData);
};
friend class BrowserContextKeyedAPIFactory<DeveloperPrivateAPI>; friend class BrowserContextKeyedAPIFactory<DeveloperPrivateAPI>;
// BrowserContextKeyedAPI implementation. // BrowserContextKeyedAPI implementation.
...@@ -213,23 +245,18 @@ class DeveloperPrivateAPI : public BrowserContextKeyedAPI, ...@@ -213,23 +245,18 @@ class DeveloperPrivateAPI : public BrowserContextKeyedAPI,
void RegisterNotifications(); void RegisterNotifications();
const WebContentsData* GetWebContentsData(
content::WebContents* web_contents) const;
WebContentsData* GetOrCreateWebContentsData(
content::WebContents* web_contents);
Profile* profile_; Profile* profile_;
// Used to start the load |load_extension_dialog_| in the last directory that // Used to start the load |load_extension_dialog_| in the last directory that
// was loaded. // was loaded.
base::FilePath last_unpacked_directory_; base::FilePath last_unpacked_directory_;
// A set of unpacked paths that we are allowed to load for different std::map<content::WebContents*, WebContentsData> web_contents_data_;
// WebContents. For security reasons, we don't let JavaScript arbitrarily
// pass us a path and load the extension at that location; instead, the user
// has to explicitly select the path through a native dialog first, and then
// we will allow JavaScript to request we reload that same selected path.
// Additionally, these are segmented by WebContents; this is primarily to
// allow collection (removing old paths when the WebContents closes) but has
// the effect that WebContents A cannot retry a path selected in
// WebContents B.
using IdToPathMap = std::map<UnpackedRetryId, base::FilePath>;
std::map<content::WebContents*, IdToPathMap> allowed_unpacked_paths_;
// Created lazily upon OnListenerAdded. // Created lazily upon OnListenerAdded.
std::unique_ptr<DeveloperPrivateEventRouter> developer_private_event_router_; std::unique_ptr<DeveloperPrivateEventRouter> developer_private_event_router_;
...@@ -465,6 +492,24 @@ class DeveloperPrivateLoadUnpackedFunction ...@@ -465,6 +492,24 @@ class DeveloperPrivateLoadUnpackedFunction
DeveloperPrivateAPI::UnpackedRetryId retry_guid_; DeveloperPrivateAPI::UnpackedRetryId retry_guid_;
}; };
class DeveloperPrivateNotifyDragInstallInProgressFunction
: public DeveloperPrivateAPIFunction {
public:
DECLARE_EXTENSION_FUNCTION("developerPrivate.notifyDragInstallInProgress",
DEVELOPERPRIVATE_NOTIFYDRAGINSTALLINPROGRESS);
DeveloperPrivateNotifyDragInstallInProgressFunction();
ResponseAction Run() override;
static void SetDropPathForTesting(base::FilePath* file_path);
private:
~DeveloperPrivateNotifyDragInstallInProgressFunction() override;
DISALLOW_COPY_AND_ASSIGN(DeveloperPrivateNotifyDragInstallInProgressFunction);
};
class DeveloperPrivateChoosePathFunction class DeveloperPrivateChoosePathFunction
: public DeveloperPrivateChooseEntryFunction { : public DeveloperPrivateChooseEntryFunction {
public: public:
......
...@@ -861,6 +861,87 @@ TEST_F(DeveloperPrivateApiUnitTest, ReloadBadExtensionToLoadUnpackedRetry) { ...@@ -861,6 +861,87 @@ TEST_F(DeveloperPrivateApiUnitTest, ReloadBadExtensionToLoadUnpackedRetry) {
} }
} }
TEST_F(DeveloperPrivateApiUnitTest,
DeveloperPrivateNotifyDragInstallInProgress) {
std::unique_ptr<content::WebContents> web_contents(
content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
TestExtensionDir dir;
dir.WriteManifest(
R"({
"name": "foo",
"description": "bar",
"version": "1",
"manifest_version": 2
})");
base::FilePath path = dir.UnpackedPath();
api::DeveloperPrivateNotifyDragInstallInProgressFunction::
SetDropPathForTesting(&path);
{
auto function = base::MakeRefCounted<
api::DeveloperPrivateNotifyDragInstallInProgressFunction>();
function->SetRenderFrameHost(web_contents->GetMainFrame());
api_test_utils::RunFunction(function.get(), "[]", profile());
}
// Set the picker to choose an invalid path (the picker should be skipped if
// we supply a retry id).
base::FilePath empty_path;
api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&empty_path);
constexpr char kLoadUnpackedArgs[] =
R"([{"failQuietly": true,
"populateError": true,
"useDraggedPath": true}])";
{
// Try reloading the extension by supplying the retry id. It should succeed.
auto function =
base::MakeRefCounted<api::DeveloperPrivateLoadUnpackedFunction>();
function->SetRenderFrameHost(web_contents->GetMainFrame());
TestExtensionRegistryObserver observer(registry());
api_test_utils::RunFunction(function.get(), kLoadUnpackedArgs, profile());
const Extension* extension = observer.WaitForExtensionLoaded();
ASSERT_TRUE(extension);
EXPECT_EQ(extension->path(), path);
}
// Next, ensure that nothing catastrophic happens if the file that was dropped
// was not a directory. In theory, this shouldn't happen (the JS validates the
// file), but it could in the case of a compromised renderer, JS bug, etc.
base::FilePath invalid_path = path.AppendASCII("manifest.json");
api::DeveloperPrivateNotifyDragInstallInProgressFunction::
SetDropPathForTesting(&invalid_path);
{
auto function = base::MakeRefCounted<
api::DeveloperPrivateNotifyDragInstallInProgressFunction>();
function->SetRenderFrameHost(web_contents->GetMainFrame());
std::unique_ptr<base::Value> result =
api_test_utils::RunFunctionAndReturnSingleResult(function.get(), "[]",
profile());
}
{
// Trying to load the bad extension (the path points to the manifest, not
// the directory) should result in a load error.
auto function =
base::MakeRefCounted<api::DeveloperPrivateLoadUnpackedFunction>();
function->SetRenderFrameHost(web_contents->GetMainFrame());
TestExtensionRegistryObserver observer(registry());
std::unique_ptr<base::Value> result =
api_test_utils::RunFunctionAndReturnSingleResult(
function.get(), kLoadUnpackedArgs, profile());
ASSERT_TRUE(result);
EXPECT_TRUE(api::developer_private::LoadError::FromValue(*result));
}
// Cleanup.
api::DeveloperPrivateNotifyDragInstallInProgressFunction::
SetDropPathForTesting(nullptr);
api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(nullptr);
}
// Test developerPrivate.requestFileSource. // Test developerPrivate.requestFileSource.
TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivateRequestFileSource) { TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivateRequestFileSource) {
// Testing of this function seems light, but that's because it basically just // Testing of this function seems light, but that's because it basically just
......
...@@ -108,7 +108,7 @@ cr.define('extensions', function() { ...@@ -108,7 +108,7 @@ cr.define('extensions', function() {
var dragTarget = document.documentElement; var dragTarget = document.documentElement;
/** @private {extensions.DragAndDropHandler} */ /** @private {extensions.DragAndDropHandler} */
this.dragWrapperHandler_ = this.dragWrapperHandler_ =
new extensions.DragAndDropHandler(true, dragTarget); new extensions.DragAndDropHandler(true, false, dragTarget);
dragTarget.addEventListener('extension-drag-started', function() { dragTarget.addEventListener('extension-drag-started', function() {
ExtensionSettings.showOverlay($('drop-target-overlay')); ExtensionSettings.showOverlay($('drop-target-overlay'));
}); });
......
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:assert', '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:assert',
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:cr', '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:cr',
'<(DEPTH)/ui/webui/resources/js/cr/ui/compiled_resources2.gyp:drag_wrapper', '<(DEPTH)/ui/webui/resources/js/cr/ui/compiled_resources2.gyp:drag_wrapper',
'<(EXTERNS_GYP):developer_private',
], ],
'includes': ['../../../../third_party/closure_compiler/compile_js2.gypi'], 'includes': ['../../../../third_party/closure_compiler/compile_js2.gypi'],
}, },
......
...@@ -7,21 +7,27 @@ cr.define('extensions', function() { ...@@ -7,21 +7,27 @@ cr.define('extensions', function() {
/** /**
* @param {boolean} dragEnabled * @param {boolean} dragEnabled
* @param {boolean} isMdExtensions
* @param {!EventTarget} target * @param {!EventTarget} target
* @constructor * @constructor
* @implements cr.ui.DragWrapperDelegate * @implements cr.ui.DragWrapperDelegate
*/ */
function DragAndDropHandler(dragEnabled, target) { function DragAndDropHandler(dragEnabled, isMdExtensions, target) {
this.dragEnabled = dragEnabled; this.dragEnabled = dragEnabled;
// Behavior is different for dropped directories between MD and non-MD
// extensions pages.
// TODO(devlin): Delete the non-MD codepath and remove this variable when
// MD extensions launches.
/** @private {boolean} */
this.isMdExtensions_ = isMdExtensions;
/** @private {!EventTarget} */ /** @private {!EventTarget} */
this.eventTarget_ = target; this.eventTarget_ = target;
} }
// TODO(devlin): Un-chrome.send-ify this implementation. // TODO(devlin): Finish un-chrome.send-ifying this implementation.
DragAndDropHandler.prototype = { DragAndDropHandler.prototype = {
/** @type {boolean} */
dragEnabled: false,
/** @override */ /** @override */
shouldAcceptDrag: function(e) { shouldAcceptDrag: function(e) {
// External Extension installation can be disabled globally, e.g. while a // External Extension installation can be disabled globally, e.g. while a
...@@ -40,6 +46,9 @@ cr.define('extensions', function() { ...@@ -40,6 +46,9 @@ cr.define('extensions', function() {
/** @override */ /** @override */
doDragEnter: function() { doDragEnter: function() {
chrome.send('startDrag'); chrome.send('startDrag');
if (this.isMdExtensions_)
chrome.developerPrivate.notifyDragInstallInProgress();
this.eventTarget_.dispatchEvent( this.eventTarget_.dispatchEvent(
new CustomEvent('extension-drag-started')); new CustomEvent('extension-drag-started'));
}, },
...@@ -61,27 +70,56 @@ cr.define('extensions', function() { ...@@ -61,27 +70,56 @@ cr.define('extensions', function() {
if (e.dataTransfer.files.length != 1) if (e.dataTransfer.files.length != 1)
return; return;
let toSend = ''; let handled = false;
// Files lack a check if they're a directory, but we can find out through // Files lack a check if they're a directory, but we can find out through
// its item entry. // its item entry.
for (let i = 0; i < e.dataTransfer.items.length; ++i) { let item = e.dataTransfer.items[0];
if (e.dataTransfer.items[i].kind == 'file' && if (item.kind === 'file' && item.webkitGetAsEntry().isDirectory) {
e.dataTransfer.items[i].webkitGetAsEntry().isDirectory) { handled = true;
toSend = 'installDroppedDirectory'; this.handleDirectoryDrop_();
break; } else if (/\.(crx|user\.js|zip)$/i.test(e.dataTransfer.files[0].name)) {
} // Only process files that look like extensions. Other files should
} // navigate the browser normally.
// Only process files that look like extensions. Other files should handled = true;
// navigate the browser normally. this.handleFileDrop_();
if (!toSend &&
/\.(crx|user\.js|zip)$/i.test(e.dataTransfer.files[0].name)) {
toSend = 'installDroppedFile';
} }
if (toSend) { if (handled)
e.preventDefault(); e.preventDefault();
chrome.send(toSend); },
/**
* Handles a dropped file.
* @private
*/
handleFileDrop_: function() {
// Packaged files always go through chrome.send (for now).
chrome.send('installDroppedFile');
},
/**
* Handles a dropped directory.
* @private
*/
handleDirectoryDrop_: function() {
// Dropped directories either go through developerPrivate or chrome.send
// depending on if this is the MD page.
if (!this.isMdExtensions_) {
chrome.send('installDroppedDirectory');
return;
} }
// TODO(devlin): Update this to use extensions.Service when it's not
// shared between the MD and non-MD pages.
chrome.developerPrivate.loadUnpacked(
{failQuietly: true, populateError: true, useDraggedPath: true},
(loadError) => {
if (loadError) {
this.eventTarget_.dispatchEvent(new CustomEvent(
'drag-and-drop-load-error', {detail: loadError}));
}
});
}, },
/** @private */ /** @private */
......
...@@ -7,17 +7,22 @@ ...@@ -7,17 +7,22 @@
Polymer({ Polymer({
is: 'extensions-drop-overlay', is: 'extensions-drop-overlay',
/** @override */
created: function() { created: function() {
this.hidden = true; this.hidden = true;
const dragTarget = document.documentElement; const dragTarget = document.documentElement;
this.dragWrapperHandler_ = this.dragWrapperHandler_ =
new extensions.DragAndDropHandler(true, dragTarget); new extensions.DragAndDropHandler(true, true, dragTarget);
dragTarget.addEventListener('extension-drag-started', () => { dragTarget.addEventListener('extension-drag-started', () => {
this.hidden = false; this.hidden = false;
}); });
dragTarget.addEventListener('extension-drag-ended', () => { dragTarget.addEventListener('extension-drag-ended', () => {
this.hidden = true; this.hidden = true;
}); });
dragTarget.addEventListener('drag-and-drop-load-error', (e) => {
this.fire('load-error', e.detail);
});
this.dragWrapper_ = this.dragWrapper_ =
new cr.ui.DragWrapper(dragTarget, this.dragWrapperHandler_); new cr.ui.DragWrapper(dragTarget, this.dragWrapperHandler_);
}, },
......
...@@ -60,7 +60,6 @@ ...@@ -60,7 +60,6 @@
on-pack-tap="onPackTap_" delegate="[[delegate]]" on-pack-tap="onPackTap_" delegate="[[delegate]]"
on-cr-toolbar-menu-tap="onMenuButtonTap_" on-cr-toolbar-menu-tap="onMenuButtonTap_"
on-search-changed="onFilterChanged_" on-search-changed="onFilterChanged_"
on-load-error="onLoadError_"
<if expr="chromeos"> <if expr="chromeos">
on-kiosk-tap="onKioskTap_" on-kiosk-tap="onKioskTap_"
kiosk-enabled="[[kioskEnabled_]]" kiosk-enabled="[[kioskEnabled_]]"
...@@ -81,8 +80,7 @@ ...@@ -81,8 +80,7 @@
delegate="[[delegate]]" in-dev-mode="[[inDevMode]]" delegate="[[delegate]]" in-dev-mode="[[inDevMode]]"
filter="[[filter]]" hidden$="[[!didInitPage_]]" slot="view" filter="[[filter]]" hidden$="[[!didInitPage_]]" slot="view"
apps="[[apps_]]" extensions="[[extensions_]]" apps="[[apps_]]" extensions="[[extensions_]]"
on-show-install-warnings="onShowInstallWarnings_" on-show-install-warnings="onShowInstallWarnings_">
on-load-error="onLoadError_">
</extensions-item-list> </extensions-item-list>
<template id="details-view" is="cr-lazy-render"> <template id="details-view" is="cr-lazy-render">
<extensions-detail-view delegate="[[delegate]]" slot="view" <extensions-detail-view delegate="[[delegate]]" slot="view"
......
...@@ -125,6 +125,7 @@ cr.define('extensions', function() { ...@@ -125,6 +125,7 @@ cr.define('extensions', function() {
}, },
listeners: { listeners: {
'load-error': 'onLoadError_',
'view-exit-finish': 'onViewExitFinish_', 'view-exit-finish': 'onViewExitFinish_',
}, },
......
...@@ -320,6 +320,11 @@ namespace developerPrivate { ...@@ -320,6 +320,11 @@ namespace developerPrivate {
// associated with the identifier will be loaded, and the file chooser // associated with the identifier will be loaded, and the file chooser
// will be skipped. // will be skipped.
DOMString? retryGuid; DOMString? retryGuid;
// True if the function should try to load an extension from the drop data
// of the page. notifyDragInstallInProgress() needs to be called prior to
// this being used. This cannot be used with |retryGuid|.
boolean? useDraggedPath;
}; };
enum PackStatus { enum PackStatus {
...@@ -490,6 +495,7 @@ namespace developerPrivate { ...@@ -490,6 +495,7 @@ namespace developerPrivate {
callback RequestFileSourceCallback = callback RequestFileSourceCallback =
void (RequestFileSourceResponse response); void (RequestFileSourceResponse response);
callback LoadErrorCallback = void (optional LoadError error); callback LoadErrorCallback = void (optional LoadError error);
callback DragInstallInProgressCallback = void (DOMString loadGuid);
interface Functions { interface Functions {
// Runs auto update for extensions and apps immediately. // Runs auto update for extensions and apps immediately.
...@@ -551,6 +557,10 @@ namespace developerPrivate { ...@@ -551,6 +557,10 @@ namespace developerPrivate {
static void loadUnpacked(optional LoadUnpackedOptions options, static void loadUnpacked(optional LoadUnpackedOptions options,
optional LoadErrorCallback callback); optional LoadErrorCallback callback);
// Notifies the browser that a user began a drag in order to install an
// extension.
static void notifyDragInstallInProgress();
// Loads an extension / app. // Loads an extension / app.
// |directory| : The directory to load the extension from. // |directory| : The directory to load the extension from.
static void loadDirectory( static void loadDirectory(
......
...@@ -1266,6 +1266,7 @@ enum HistogramValue { ...@@ -1266,6 +1266,7 @@ enum HistogramValue {
VIRTUALKEYBOARDPRIVATE_SETDRAGGABLEAREA, VIRTUALKEYBOARDPRIVATE_SETDRAGGABLEAREA,
METRICSPRIVATE_RECORDBOOLEAN, METRICSPRIVATE_RECORDBOOLEAN,
METRICSPRIVATE_RECORDENUMERATIONVALUE, METRICSPRIVATE_RECORDENUMERATIONVALUE,
DEVELOPERPRIVATE_NOTIFYDRAGINSTALLINPROGRESS,
// Last entry: Add new entries above, then run: // Last entry: Add new entries above, then run:
// python tools/metrics/histograms/update_extension_histograms.py // python tools/metrics/histograms/update_extension_histograms.py
ENUM_BOUNDARY ENUM_BOUNDARY
......
...@@ -424,7 +424,8 @@ chrome.developerPrivate.ReloadOptions; ...@@ -424,7 +424,8 @@ chrome.developerPrivate.ReloadOptions;
* @typedef {{ * @typedef {{
* failQuietly: (boolean|undefined), * failQuietly: (boolean|undefined),
* populateError: (boolean|undefined), * populateError: (boolean|undefined),
* retryGuid: (string|undefined) * retryGuid: (string|undefined),
* useDraggedPath: (boolean|undefined)
* }} * }}
* @see https://developer.chrome.com/extensions/developerPrivate#type-LoadUnpackedOptions * @see https://developer.chrome.com/extensions/developerPrivate#type-LoadUnpackedOptions
*/ */
...@@ -669,6 +670,13 @@ chrome.developerPrivate.updateExtensionConfiguration = function(update, callback ...@@ -669,6 +670,13 @@ chrome.developerPrivate.updateExtensionConfiguration = function(update, callback
*/ */
chrome.developerPrivate.loadUnpacked = function(options, callback) {}; chrome.developerPrivate.loadUnpacked = function(options, callback) {};
/**
* Notifies the browser that a user began a drag in order to install an
* extension.
* @see https://developer.chrome.com/extensions/developerPrivate#method-notifyDragInstallInProgress
*/
chrome.developerPrivate.notifyDragInstallInProgress = function() {};
/** /**
* Loads an extension / app. * Loads an extension / app.
* @param {Object} directory The directory to load the extension from. * @param {Object} directory The directory to load the extension from.
......
...@@ -14246,6 +14246,7 @@ Called by update_net_error_codes.py.--> ...@@ -14246,6 +14246,7 @@ Called by update_net_error_codes.py.-->
<int value="1203" label="VIRTUALKEYBOARDPRIVATE_SETDRAGGABLEAREA"/> <int value="1203" label="VIRTUALKEYBOARDPRIVATE_SETDRAGGABLEAREA"/>
<int value="1204" label="METRICSPRIVATE_RECORDBOOLEAN"/> <int value="1204" label="METRICSPRIVATE_RECORDBOOLEAN"/>
<int value="1205" label="METRICSPRIVATE_RECORDENUMERATIONVALUE"/> <int value="1205" label="METRICSPRIVATE_RECORDENUMERATIONVALUE"/>
<int value="1206" label="DEVELOPERPRIVATE_NOTIFYDRAGINSTALLINPROGRESS"/>
</enum> </enum>
<enum name="ExtensionIconState"> <enum name="ExtensionIconState">
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