blob: 3c82a96b4ce6506e97836b986dba83a50995e304 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/devtools/devtools_file_helper.h"
#include <set>
#include <vector>
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/hash/md5.h"
#include "base/json/values_util.h"
#include "base/lazy_instance.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/devtools/devtools_file_watcher.h"
#include "chrome/browser/download/download_prefs.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/chrome_select_file_policy.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_security_policy.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/common/url_constants.h"
#include "storage/browser/file_system/file_system_url.h"
#include "storage/browser/file_system/isolated_context.h"
#include "storage/common/file_system/file_system_util.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/shell_dialogs/select_file_dialog.h"
#include "ui/shell_dialogs/selected_file_info.h"
#include "url/gurl.h"
#include "url/origin.h"
using content::BrowserContext;
using content::BrowserThread;
using content::DownloadManager;
using content::RenderViewHost;
using content::WebContents;
using std::set;
namespace {
static const char kRootName[] = "<root>";
static const char kPermissionDenied[] = "<permission denied>";
static const char kSelectionCancelled[] = "<selection cancelled>";
base::LazyInstance<base::FilePath>::Leaky
g_last_save_path = LAZY_INSTANCE_INITIALIZER;
typedef base::OnceCallback<void(const base::FilePath&)> SelectedCallback;
typedef base::OnceCallback<void(void)> CanceledCallback;
class SelectFileDialog : public ui::SelectFileDialog::Listener {
public:
SelectFileDialog(const SelectFileDialog&) = delete;
SelectFileDialog& operator=(const SelectFileDialog&) = delete;
static void Show(SelectedCallback selected_callback,
CanceledCallback canceled_callback,
WebContents* web_contents,
ui::SelectFileDialog::Type type,
const base::FilePath& default_path) {
// `dialog` is self-deleting.
auto* dialog = new SelectFileDialog();
dialog->ShowDialog(std::move(selected_callback),
std::move(canceled_callback), web_contents, type,
default_path);
}
// ui::SelectFileDialog::Listener implementation.
void FileSelected(const ui::SelectedFileInfo& file,
int index,
void* params) override {
std::move(selected_callback_).Run(file.path());
delete this;
}
void FileSelectionCanceled(void* params) override {
if (canceled_callback_) {
std::move(canceled_callback_).Run();
}
delete this;
}
private:
SelectFileDialog() = default;
~SelectFileDialog() override { select_file_dialog_->ListenerDestroyed(); }
void ShowDialog(SelectedCallback selected_callback,
CanceledCallback canceled_callback,
WebContents* web_contents,
ui::SelectFileDialog::Type type,
const base::FilePath& default_path) {
selected_callback_ = std::move(selected_callback);
canceled_callback_ = std::move(canceled_callback);
select_file_dialog_ = ui::SelectFileDialog::Create(
this, std::make_unique<ChromeSelectFilePolicy>(web_contents));
base::FilePath::StringType ext;
ui::SelectFileDialog::FileTypeInfo file_type_info;
if (type == ui::SelectFileDialog::SELECT_SAVEAS_FILE &&
default_path.Extension().length() > 0) {
ext = default_path.Extension().substr(1);
file_type_info.extensions.resize(1);
file_type_info.extensions[0].push_back(ext);
}
select_file_dialog_->SelectFile(
type, std::u16string(), default_path, &file_type_info, 0, ext,
platform_util::GetTopLevel(web_contents->GetNativeView()), nullptr);
}
scoped_refptr<ui::SelectFileDialog> select_file_dialog_;
SelectedCallback selected_callback_;
CanceledCallback canceled_callback_;
};
void WriteToFile(const base::FilePath& path, const std::string& content) {
DCHECK(!path.empty());
base::WriteFile(path, content);
}
void AppendToFile(const base::FilePath& path, const std::string& content) {
DCHECK(!path.empty());
base::AppendToFile(path, content);
}
storage::IsolatedContext* isolated_context() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
storage::IsolatedContext* isolated_context =
storage::IsolatedContext::GetInstance();
DCHECK(isolated_context);
return isolated_context;
}
std::string RegisterFileSystem(WebContents* web_contents,
const base::FilePath& path) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(web_contents->GetLastCommittedURL().SchemeIs(
content::kChromeDevToolsScheme));
std::string root_name(kRootName);
storage::IsolatedContext::ScopedFSHandle file_system =
isolated_context()->RegisterFileSystemForPath(
storage::kFileSystemTypeLocal, std::string(), path, &root_name);
content::ChildProcessSecurityPolicy* policy =
content::ChildProcessSecurityPolicy::GetInstance();
RenderViewHost* render_view_host =
web_contents->GetPrimaryMainFrame()->GetRenderViewHost();
int renderer_id = render_view_host->GetProcess()->GetID();
policy->GrantReadFileSystem(renderer_id, file_system.id());
policy->GrantWriteFileSystem(renderer_id, file_system.id());
policy->GrantCreateFileForFileSystem(renderer_id, file_system.id());
policy->GrantDeleteFromFileSystem(renderer_id, file_system.id());
// We only need file level access for reading FileEntries. Saving FileEntries
// just needs the file system to have read/write access, which is granted
// above if required.
if (!policy->CanReadFile(renderer_id, path))
policy->GrantReadFile(renderer_id, path);
return file_system.id();
}
DevToolsFileHelper::FileSystem CreateFileSystemStruct(
WebContents* web_contents,
const std::string& type,
const std::string& file_system_id,
const std::string& file_system_path) {
const GURL origin =
web_contents->GetLastCommittedURL().DeprecatedGetOriginAsURL();
std::string file_system_name =
storage::GetIsolatedFileSystemName(origin, file_system_id);
std::string root_url = storage::GetIsolatedFileSystemRootURIString(
origin, file_system_id, kRootName);
return DevToolsFileHelper::FileSystem(type, file_system_name, root_url,
file_system_path);
}
using PathToType = std::map<std::string, std::string>;
PathToType GetAddedFileSystemPaths(Profile* profile) {
const base::Value::Dict& file_systems_paths_value =
profile->GetPrefs()->GetDict(prefs::kDevToolsFileSystemPaths);
PathToType result;
for (auto pair : file_systems_paths_value) {
std::string type =
pair.second.is_string() ? pair.second.GetString() : std::string();
result[pair.first] = type;
}
return result;
}
} // namespace
DevToolsFileHelper::FileSystem::FileSystem() = default;
DevToolsFileHelper::FileSystem::~FileSystem() = default;
DevToolsFileHelper::FileSystem::FileSystem(const FileSystem& other) = default;
DevToolsFileHelper::FileSystem::FileSystem(const std::string& type,
const std::string& file_system_name,
const std::string& root_url,
const std::string& file_system_path)
: type(type),
file_system_name(file_system_name),
root_url(root_url),
file_system_path(file_system_path) {}
DevToolsFileHelper::DevToolsFileHelper(WebContents* web_contents,
Profile* profile,
Delegate* delegate)
: web_contents_(web_contents),
profile_(profile),
delegate_(delegate),
file_task_runner_(
base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()})) {
pref_change_registrar_.Init(profile_->GetPrefs());
}
DevToolsFileHelper::~DevToolsFileHelper() = default;
void DevToolsFileHelper::Save(const std::string& url,
const std::string& content,
bool save_as,
SaveCallback saveCallback,
base::OnceClosure cancelCallback) {
auto it = saved_files_.find(url);
if (it != saved_files_.end() && !save_as) {
SaveAsFileSelected(url, content, std::move(saveCallback), it->second);
return;
}
const base::Value::Dict& file_map =
profile_->GetPrefs()->GetDict(prefs::kDevToolsEditedFiles);
base::FilePath initial_path;
if (const base::Value* path_value = file_map.Find(base::MD5String(url))) {
std::optional<base::FilePath> path = base::ValueToFilePath(*path_value);
if (path)
initial_path = std::move(*path);
}
if (initial_path.empty()) {
GURL gurl(url);
std::string suggested_file_name;
if (gurl.is_valid()) {
url::RawCanonOutputW<1024> unescaped_content;
std::string escaped_content = gurl.ExtractFileName();
url::DecodeURLEscapeSequences(escaped_content,
url::DecodeURLMode::kUTF8OrIsomorphic,
&unescaped_content);
// TODO(crbug.com/40839171): Due to filename encoding on Windows we can't
// expect to always be able to convert to UTF8 and back
std::string unescaped_content_string =
base::UTF16ToUTF8(unescaped_content.view());
suggested_file_name = unescaped_content_string;
} else {
suggested_file_name = url;
}
// TODO(crbug.com/40839171): Truncate a UTF8 string in a better way
if (suggested_file_name.length() > 64)
suggested_file_name = suggested_file_name.substr(0, 64);
// TODO(crbug.com/40839171): Ensure suggested_file_name is an ASCII string
if (!g_last_save_path.Pointer()->empty()) {
initial_path = g_last_save_path.Pointer()->DirName().AppendASCII(
suggested_file_name);
} else {
base::FilePath download_path =
DownloadPrefs::FromDownloadManager(profile_->GetDownloadManager())
->DownloadPath();
initial_path = download_path.AppendASCII(suggested_file_name);
}
}
SelectFileDialog::Show(base::BindOnce(&DevToolsFileHelper::SaveAsFileSelected,
weak_factory_.GetWeakPtr(), url,
content, std::move(saveCallback)),
std::move(cancelCallback), web_contents_,
ui::SelectFileDialog::SELECT_SAVEAS_FILE,
initial_path);
}
void DevToolsFileHelper::Append(const std::string& url,
const std::string& content,
base::OnceClosure callback) {
auto it = saved_files_.find(url);
if (it == saved_files_.end())
return;
std::move(callback).Run();
file_task_runner_->PostTask(FROM_HERE,
BindOnce(&AppendToFile, it->second, content));
}
void DevToolsFileHelper::SaveAsFileSelected(const std::string& url,
const std::string& content,
SaveCallback callback,
const base::FilePath& path) {
*g_last_save_path.Pointer() = path;
saved_files_[url] = path;
ScopedDictPrefUpdate update(profile_->GetPrefs(),
prefs::kDevToolsEditedFiles);
base::Value::Dict& files_map = update.Get();
files_map.Set(base::MD5String(url), base::FilePathToValue(path));
std::string file_system_path = path.AsUTF8Unsafe();
std::move(callback).Run(file_system_path);
file_task_runner_->PostTask(FROM_HERE, BindOnce(&WriteToFile, path, content));
}
void DevToolsFileHelper::AddFileSystem(
const std::string& type,
const ShowInfoBarCallback& show_info_bar_callback) {
SelectFileDialog::Show(
base::BindOnce(&DevToolsFileHelper::InnerAddFileSystem,
weak_factory_.GetWeakPtr(), show_info_bar_callback, type),
base::BindOnce(&DevToolsFileHelper::FailedToAddFileSystem,
weak_factory_.GetWeakPtr(), kSelectionCancelled),
web_contents_, ui::SelectFileDialog::SELECT_FOLDER, base::FilePath());
}
void DevToolsFileHelper::UpgradeDraggedFileSystemPermissions(
const std::string& file_system_url,
const ShowInfoBarCallback& show_info_bar_callback) {
const GURL gurl(file_system_url);
storage::FileSystemURL root_url = isolated_context()->CrackURL(
gurl, blink::StorageKey::CreateFirstParty(url::Origin::Create(gurl)));
if (!root_url.is_valid() || !root_url.path().empty())
return;
std::vector<storage::MountPoints::MountPointInfo> mount_points;
isolated_context()->GetDraggedFileInfo(root_url.filesystem_id(),
&mount_points);
std::vector<storage::MountPoints::MountPointInfo>::const_iterator it =
mount_points.begin();
for (; it != mount_points.end(); ++it)
InnerAddFileSystem(show_info_bar_callback, std::string(), it->path);
}
void DevToolsFileHelper::InnerAddFileSystem(
const ShowInfoBarCallback& show_info_bar_callback,
const std::string& type,
const base::FilePath& path) {
std::string file_system_path = path.AsUTF8Unsafe();
if (IsFileSystemAdded(file_system_path))
RemoveFileSystem(file_system_path);
std::string path_display_name = path.AsEndingWithSeparator().AsUTF8Unsafe();
std::u16string message =
l10n_util::GetStringFUTF16(IDS_DEV_TOOLS_CONFIRM_ADD_FILE_SYSTEM_MESSAGE,
base::UTF8ToUTF16(path_display_name));
show_info_bar_callback.Run(
message, BindOnce(&DevToolsFileHelper::AddUserConfirmedFileSystem,
weak_factory_.GetWeakPtr(), type, path));
}
void DevToolsFileHelper::AddUserConfirmedFileSystem(const std::string& type,
const base::FilePath& path,
bool allowed) {
if (!allowed) {
FailedToAddFileSystem(kPermissionDenied);
return;
}
std::string file_system_id = RegisterFileSystem(web_contents_, path);
std::string file_system_path = path.AsUTF8Unsafe();
ScopedDictPrefUpdate update(profile_->GetPrefs(),
prefs::kDevToolsFileSystemPaths);
base::Value::Dict& file_systems_paths_value = update.Get();
file_systems_paths_value.Set(file_system_path, type);
}
void DevToolsFileHelper::FailedToAddFileSystem(const std::string& error) {
delegate_->FileSystemAdded(error, nullptr);
}
namespace {
void RunOnUIThread(base::OnceClosure callback) {
content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE, std::move(callback));
}
} // namespace
std::vector<DevToolsFileHelper::FileSystem>
DevToolsFileHelper::GetFileSystems() {
file_system_paths_ = GetAddedFileSystemPaths(profile_);
std::vector<FileSystem> file_systems;
if (!file_watcher_) {
file_watcher_.reset(new DevToolsFileWatcher(
base::BindRepeating(&DevToolsFileHelper::FilePathsChanged,
weak_factory_.GetWeakPtr()),
base::SequencedTaskRunner::GetCurrentDefault()));
auto change_handler_on_ui = base::BindRepeating(
&DevToolsFileHelper::FileSystemPathsSettingChangedOnUI,
weak_factory_.GetWeakPtr());
pref_change_registrar_.Add(
prefs::kDevToolsFileSystemPaths,
base::BindRepeating(RunOnUIThread, change_handler_on_ui));
}
for (auto file_system_path : file_system_paths_) {
base::FilePath path =
base::FilePath::FromUTF8Unsafe(file_system_path.first);
std::string file_system_id = RegisterFileSystem(web_contents_, path);
FileSystem filesystem =
CreateFileSystemStruct(web_contents_, file_system_path.second,
file_system_id, file_system_path.first);
file_systems.push_back(filesystem);
file_watcher_->AddWatch(std::move(path));
}
return file_systems;
}
void DevToolsFileHelper::RemoveFileSystem(const std::string& file_system_path) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
base::FilePath path = base::FilePath::FromUTF8Unsafe(file_system_path);
isolated_context()->RevokeFileSystemByPath(path);
ScopedDictPrefUpdate update(profile_->GetPrefs(),
prefs::kDevToolsFileSystemPaths);
base::Value::Dict& file_systems_paths_value = update.Get();
file_systems_paths_value.Remove(file_system_path);
}
bool DevToolsFileHelper::IsFileSystemAdded(
const std::string& file_system_path) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
const base::Value::Dict& file_systems_paths_value =
profile_->GetPrefs()->GetDict(prefs::kDevToolsFileSystemPaths);
return file_systems_paths_value.Find(file_system_path);
}
void DevToolsFileHelper::OnOpenItemComplete(
const base::FilePath& path,
platform_util::OpenOperationResult result) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (result == platform_util::OPEN_FAILED_INVALID_TYPE)
platform_util::ShowItemInFolder(profile_, path);
}
void DevToolsFileHelper::ShowItemInFolder(const std::string& file_system_path) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (file_system_path.empty())
return;
base::FilePath path = base::FilePath::FromUTF8Unsafe(file_system_path);
platform_util::OpenItem(
profile_, path, platform_util::OPEN_FOLDER,
base::BindOnce(&DevToolsFileHelper::OnOpenItemComplete,
weak_factory_.GetWeakPtr(), path));
}
void DevToolsFileHelper::FileSystemPathsSettingChangedOnUI() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
PathToType remaining;
remaining.swap(file_system_paths_);
DCHECK(file_watcher_.get());
for (auto file_system : GetAddedFileSystemPaths(profile_)) {
if (remaining.find(file_system.first) == remaining.end()) {
base::FilePath path = base::FilePath::FromUTF8Unsafe(file_system.first);
std::string file_system_id = RegisterFileSystem(web_contents_, path);
FileSystem filesystem = CreateFileSystemStruct(
web_contents_, file_system.second, file_system_id, file_system.first);
delegate_->FileSystemAdded(std::string(), &filesystem);
file_watcher_->AddWatch(std::move(path));
} else {
remaining.erase(file_system.first);
}
file_system_paths_[file_system.first] = file_system.second;
}
for (auto file_system : remaining) {
delegate_->FileSystemRemoved(file_system.first);
base::FilePath path = base::FilePath::FromUTF8Unsafe(file_system.first);
file_watcher_->RemoveWatch(std::move(path));
}
}
void DevToolsFileHelper::FilePathsChanged(
const std::vector<std::string>& changed_paths,
const std::vector<std::string>& added_paths,
const std::vector<std::string>& removed_paths) {
delegate_->FilePathsChanged(changed_paths, added_paths, removed_paths);
}