| Index: chrome/browser/extensions/extension_file_browser_private_api.cc
|
| ===================================================================
|
| --- chrome/browser/extensions/extension_file_browser_private_api.cc (revision 79991)
|
| +++ chrome/browser/extensions/extension_file_browser_private_api.cc (working copy)
|
| @@ -4,20 +4,221 @@
|
|
|
| #include "chrome/browser/extensions/extension_file_browser_private_api.h"
|
|
|
| +#include "base/base64.h"
|
| +#include "base/crypto/symmetric_key.h"
|
| +#include "base/hmac.h"
|
| #include "base/json/json_writer.h"
|
| +#include "base/memory/singleton.h"
|
| +#include "base/stringprintf.h"
|
| #include "base/task.h"
|
| #include "base/values.h"
|
| #include "chrome/browser/profiles/profile.h"
|
| +#include "chrome/browser/extensions/extension_function_dispatcher.h"
|
| +#include "chrome/browser/extensions/extension_service.h"
|
| +#include "chrome/browser/tab_contents/context_menu_utils.h"
|
| +#include "chrome/browser/ui/webui/extension_icon_source.h"
|
| #include "chrome/common/extensions/extension.h"
|
| #include "content/browser/browser_thread.h"
|
| +#include "content/browser/child_process_security_policy.h"
|
| +#include "content/browser/renderer_host/render_process_host.h"
|
| +#include "content/browser/renderer_host/render_view_host.h"
|
| +#include "content/browser/tab_contents/tab_contents.h"
|
| +#include "webkit/fileapi/file_system_context.h"
|
| #include "webkit/fileapi/file_system_operation.h"
|
| +#include "webkit/fileapi/file_system_path_manager.h"
|
| #include "webkit/fileapi/file_system_types.h"
|
| +#include "webkit/glue/context_menu.h"
|
|
|
| +#define SHA1_SIZE_IN_BITS 160
|
| +
|
| +const char kContextTaskIdSchema[] = "context-task";
|
| +
|
| +bool GetContextMenuItems(Profile* profile,
|
| + const ContextMenuParams& params,
|
| + ExtensionMenuItem::List* results) {
|
| + ExtensionService* service = profile->GetExtensionService();
|
| + if (!service)
|
| + return false; // In unit-tests, we may not have an ExtensionService.
|
| +
|
| + // Get a list of extension id's that have context menu items, and sort it by
|
| + // the extension's name.
|
| + ExtensionMenuManager* menu_manager = service->menu_manager();
|
| + std::set<std::string> ids = menu_manager->ExtensionIds();
|
| + std::vector<std::pair<std::string, std::string> > sorted_ids;
|
| + for (std::set<std::string>::iterator i = ids.begin(); i != ids.end(); ++i) {
|
| + const Extension* extension = service->GetExtensionById(*i, false);
|
| + if (extension)
|
| + sorted_ids.push_back(
|
| + std::pair<std::string, std::string>(extension->name(), *i));
|
| + }
|
| +
|
| + if (sorted_ids.empty())
|
| + return true;
|
| +
|
| + std::vector<std::pair<std::string, std::string> >::const_iterator i;
|
| + for (i = sorted_ids.begin(); i != sorted_ids.end(); ++i) {
|
| + const std::string& extension_id = i->second;
|
| + const Extension* extension = service->GetExtensionById(extension_id, false);
|
| + bool can_cross_incognito = service->CanCrossIncognito(extension);
|
| + const ExtensionMenuItem::List* all_items =
|
| + menu_manager->MenuItems(extension_id);
|
| + ExtensionMenuItem::List relevant_items =
|
| + ContextMenuUtils::GetRelevantExtensionItems(*all_items,
|
| + params,
|
| + profile,
|
| + can_cross_incognito);
|
| + results->insert(results->end(), relevant_items.begin(),
|
| + relevant_items.end());
|
| + }
|
| + return true;
|
| +}
|
| +
|
| +void CreateContextMenuParams(const GURL& source_url,
|
| + const std::string& file_url,
|
| + ContextMenuParams* params) {
|
| + DCHECK(params);
|
| + params->is_image_blocked = false;
|
| + params->spellcheck_enabled = false;
|
| + params->is_editable = false;
|
| + params->media_flags = 0;
|
| + params->edit_flags = 0;
|
| + params->media_type = WebKit::WebContextMenuData::MediaTypeFile;
|
| + params->src_url = GURL(file_url);
|
| + params->page_url = source_url;
|
| +}
|
| +
|
| +// Given the list of selected files, returns array of context menu tasks
|
| +// that are sahred
|
| +bool FindCommonTasks(Profile* profile,
|
| + const GURL& source_url,
|
| + ListValue* files_list,
|
| + ExtensionMenuItem::List* common_tasks) {
|
| + common_tasks->clear();
|
| + for (size_t i = 0; i < files_list->GetSize(); ++i) {
|
| + std::string file_url;
|
| + if (!files_list->GetString(i, &file_url))
|
| + return false;
|
| +
|
| + ContextMenuParams params;
|
| + CreateContextMenuParams(source_url, file_url, ¶ms);
|
| +
|
| + ExtensionMenuItem::List file_actions;
|
| + if (!GetContextMenuItems(profile, params, &file_actions))
|
| + return false;
|
| + // If there is nothing to do for one file, the intersection of tasks for all
|
| + // files will be empty at the end.
|
| + if (!file_actions.size()) {
|
| + common_tasks->clear();
|
| + return true;
|
| + }
|
| + // For the very first file, just copy elements.
|
| + if (i == 0) {
|
| + common_tasks->insert(common_tasks->begin(),
|
| + file_actions.begin(),
|
| + file_actions.end());
|
| + std::sort(common_tasks->begin(), common_tasks->end());
|
| + } else if (common_tasks->size()) {
|
| + // For all additional files, find intersection between the accumulated
|
| + // and file specific set.
|
| + std::sort(file_actions.begin(), file_actions.end());
|
| + ExtensionMenuItem::List intersection(common_tasks->size());
|
| + ExtensionMenuItem::List::iterator intersection_end =
|
| + std::set_intersection(common_tasks->begin(),
|
| + common_tasks->end(),
|
| + file_actions.begin(),
|
| + file_actions.end(),
|
| + intersection.begin());
|
| + common_tasks->clear();
|
| + common_tasks->insert(common_tasks->begin(),
|
| + intersection.begin(),
|
| + intersection_end);
|
| + std::sort(common_tasks->begin(), common_tasks->end());
|
| + }
|
| + }
|
| + return true;
|
| +}
|
| +
|
| +// Breaks down task_id that is used between getFileTasks() and executeTask() on
|
| +// its building blocks. task_id field the following structure:
|
| +// <task-type>:<extension-id>/<task-action-id>
|
| +// Currently, the only supported task-type is of 'context'.
|
| +bool CrackTaskIdentifier(const std::string& task_id,
|
| + std::string* task_type,
|
| + std::string* target_extension_id,
|
| + std::string* action_id) {
|
| + std::string::size_type pos_col = task_id.find(':');
|
| + if (pos_col == std::string::npos)
|
| + return false;
|
| + *task_type = task_id.substr(0, pos_col);
|
| + std::string::size_type pos_slash = task_id.find('/');
|
| + if (pos_slash == std::string::npos)
|
| + return false;
|
| + *target_extension_id = task_id.substr(pos_col + 1, pos_slash - pos_col - 1);
|
| + *action_id = task_id.substr(pos_slash + 1);
|
| + return true;
|
| +}
|
| +
|
| +std::string MakeTaskID(const char* task_schema,
|
| + const char* extension_id,
|
| + int action_id) {
|
| + return base::StringPrintf("%s:%s/%d", task_schema, extension_id, action_id);
|
| +}
|
| +
|
| +// Generates hashes used for filesystem: urls that are sent to 3rd party
|
| +// extension. Hashes generated from extension id and file url.
|
| +// They are valid only during the lifetime of the browser instance.
|
| +class FileHashGenerator {
|
| + public:
|
| + ~FileHashGenerator() {}
|
| + static FileHashGenerator* GetInstance() {
|
| + return Singleton<FileHashGenerator>::get();
|
| + }
|
| + // Generate hash for given url and extension combination.
|
| + std::string GenerateFileHash(const std::string& file_url,
|
| + const std::string& extension_id) {
|
| + std::string data(file_url);
|
| + data = data.append(extension_id);
|
| + scoped_ptr<unsigned char> digest(new unsigned char[SHA1_SIZE_IN_BITS/8]);
|
| + if (!hmac_.Sign(data, digest.get(), SHA1_SIZE_IN_BITS/8))
|
| + return std::string();
|
| + std::string output;
|
| + if (!base::Base64Encode(std::string(reinterpret_cast<char*>(digest.get()),
|
| + SHA1_SIZE_IN_BITS/8),
|
| + &output))
|
| + return std::string();
|
| + return output;
|
| + }
|
| +
|
| + private:
|
| + friend struct DefaultSingletonTraits<FileHashGenerator>;
|
| +
|
| + FileHashGenerator() : hmac_(base::HMAC::SHA1) {
|
| + scoped_ptr<base::SymmetricKey> sym_key(
|
| + base::SymmetricKey::GenerateRandomKey(base::SymmetricKey::AES,
|
| + SHA1_SIZE_IN_BITS));
|
| + std::string raw_key;
|
| + sym_key->GetRawKey(&raw_key);
|
| + hmac_.Init(raw_key);
|
| + }
|
| +
|
| + base::HMAC hmac_;
|
| + DISALLOW_COPY_AND_ASSIGN(FileHashGenerator);
|
| +};
|
| +
|
| class LocalFileSystemCallbackDispatcher
|
| : public fileapi::FileSystemCallbackDispatcher {
|
| public:
|
| explicit LocalFileSystemCallbackDispatcher(
|
| - RequestLocalFileSystemFunction* function) : function_(function) {
|
| + RequestLocalFileSystemFunctionBase* function,
|
| + Profile* profile,
|
| + int child_id,
|
| + const GURL& source_url,
|
| + const std::string& file_url)
|
| + : function_(function),
|
| + profile_(profile),
|
| + child_id_(child_id),
|
| + source_url_(source_url),
|
| + file_url_(file_url) {
|
| DCHECK(function_);
|
| }
|
| // fileapi::FileSystemCallbackDispatcher overrides.
|
| @@ -38,57 +239,132 @@
|
| }
|
| virtual void DidOpenFileSystem(const std::string& name,
|
| const FilePath& path) OVERRIDE {
|
| + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
|
| + // Set up file permission access.
|
| + if (file_url_.length()) {
|
| + if (!SetupFileAccessPermissions()) {
|
| + DidFail(base::PLATFORM_FILE_ERROR_SECURITY);
|
| + return;
|
| + }
|
| + }
|
| +
|
| BrowserThread::PostTask(
|
| BrowserThread::UI, FROM_HERE,
|
| NewRunnableMethod(function_,
|
| - &RequestLocalFileSystemFunction::RespondSuccessOnUIThread,
|
| + &RequestLocalFileSystemFunctionBase::RespondSuccessOnUIThread,
|
| name,
|
| - path));
|
| + path,
|
| + file_url_));
|
| }
|
| virtual void DidFail(base::PlatformFileError error_code) OVERRIDE {
|
| BrowserThread::PostTask(
|
| BrowserThread::UI, FROM_HERE,
|
| NewRunnableMethod(function_,
|
| - &RequestLocalFileSystemFunction::RespondFailedOnUIThread,
|
| + &RequestLocalFileSystemFunctionBase::RespondFailedOnUIThread,
|
| error_code));
|
| }
|
| private:
|
| - RequestLocalFileSystemFunction* function_;
|
| +
|
| + // Checks legitimacy of file url and grants access permissions.
|
| + bool SetupFileAccessPermissions() {
|
| + GURL file_origin_url;
|
| + FilePath virtual_path;
|
| + fileapi::FileSystemType type;
|
| + fileapi::FileSystemPathManager* path_manager =
|
| + profile_->GetFileSystemContext()->path_manager();
|
| +
|
| + // Check file url hash first.
|
| + std::string::size_type pos = file_url_.find('#');
|
| + if (pos == std::string::npos)
|
| + return false;
|
| + std::string url_hash = file_url_.substr(pos + 1);
|
| + std::string file_url = file_url_.substr(0, pos);
|
| + std::string extension_id = source_url_.GetOrigin().host();
|
| + std::string expected_hash = FileHashGenerator::GetInstance()->
|
| + GenerateFileHash(file_url, extension_id);
|
| + if (expected_hash != url_hash)
|
| + return false;
|
| +
|
| + if (!path_manager->CrackFileSystemPath(FilePath(file_url),
|
| + &file_origin_url,
|
| + &type,
|
| + &virtual_path)) {
|
| + return false;
|
| + }
|
| + // Make sure this url really used by the right caller extension.
|
| + if (source_url_.GetOrigin() != file_origin_url) {
|
| + DidFail(base::PLATFORM_FILE_ERROR_SECURITY);
|
| + return false;
|
| + }
|
| + FilePath root_path = path_manager->GetFileSystemRootPathOnFileThread(
|
| + file_origin_url,
|
| + fileapi::kFileSystemTypeLocal,
|
| + FilePath(virtual_path),
|
| + false); // create
|
| + FilePath finalFilePath = root_path.Append(virtual_path);
|
| + // Grant read access permission to this file.
|
| + ChildProcessSecurityPolicy::GetInstance()->GrantReadFile(child_id_,
|
| + finalFilePath);
|
| + return true;
|
| + }
|
| +
|
| + RequestLocalFileSystemFunctionBase* function_;
|
| + Profile* profile_;
|
| + // Renderer process id.
|
| + int child_id_;
|
| + // Extension source URL.
|
| + GURL source_url_;
|
| + std::string file_url_;
|
| DISALLOW_COPY_AND_ASSIGN(LocalFileSystemCallbackDispatcher);
|
| };
|
|
|
| -RequestLocalFileSystemFunction::RequestLocalFileSystemFunction() {
|
| -}
|
| -
|
| -RequestLocalFileSystemFunction::~RequestLocalFileSystemFunction() {
|
| -}
|
| -
|
| -bool RequestLocalFileSystemFunction::RunImpl() {
|
| +void RequestLocalFileSystemFunctionBase::RequestOnFileThread(
|
| + const GURL& source_url, const std::string& file_url) {
|
| fileapi::FileSystemOperation* operation =
|
| new fileapi::FileSystemOperation(
|
| - new LocalFileSystemCallbackDispatcher(this),
|
| + new LocalFileSystemCallbackDispatcher(
|
| + this,
|
| + profile(),
|
| + dispatcher()->render_view_host()->process()->id(),
|
| + source_url,
|
| + file_url),
|
| BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE),
|
| profile()->GetFileSystemContext(),
|
| NULL);
|
| - GURL origin_url = source_url().GetOrigin();
|
| + GURL origin_url = source_url.GetOrigin();
|
| operation->OpenFileSystem(origin_url, fileapi::kFileSystemTypeLocal,
|
| false); // create
|
| +}
|
| +
|
| +bool RequestLocalFileSystemFunctionBase::RunImpl() {
|
| + std::string file_url;
|
| + if (args_->GetSize())
|
| + args_->GetString(0, &file_url);
|
| + BrowserThread::PostTask(
|
| + BrowserThread::FILE, FROM_HERE,
|
| + NewRunnableMethod(this,
|
| + &RequestLocalFileSystemFunctionBase::RequestOnFileThread,
|
| + source_url_,
|
| + file_url));
|
| // Will finish asynchronously.
|
| return true;
|
| }
|
|
|
| -void RequestLocalFileSystemFunction::RespondSuccessOnUIThread(
|
| - const std::string& name, const FilePath& path) {
|
| +void RequestLocalFileSystemFunctionBase::RespondSuccessOnUIThread(
|
| + const std::string& name, const FilePath& path,
|
| + const std::string& file_url) {
|
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
| result_.reset(new DictionaryValue());
|
| DictionaryValue* dict = reinterpret_cast<DictionaryValue*>(result_.get());
|
| dict->SetString("name", name);
|
| dict->SetString("path", path.value());
|
| dict->SetInteger("error", base::PLATFORM_FILE_OK);
|
| + if (file_url.size())
|
| + dict->SetString("fileUrl", file_url);
|
| SendResponse(true);
|
| }
|
|
|
| -void RequestLocalFileSystemFunction::RespondFailedOnUIThread(
|
| +void RequestLocalFileSystemFunctionBase::RespondFailedOnUIThread(
|
| base::PlatformFileError error_code) {
|
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
| result_.reset(new DictionaryValue());
|
| @@ -97,3 +373,130 @@
|
| SendResponse(true);
|
| }
|
|
|
| +bool GetFileTasksFileBrowserFunction::RunImpl() {
|
| + ListValue* files_list = NULL;
|
| + if (!args_->GetList(0, &files_list))
|
| + return false;
|
| +
|
| + result_.reset(new ListValue());
|
| + ListValue* result_list = reinterpret_cast<ListValue*>(result_.get());
|
| +
|
| + ExtensionMenuItem::List common_tasks;
|
| + if (!FindCommonTasks(profile_, source_url_, files_list, &common_tasks))
|
| + return false;
|
| +
|
| + ExtensionService* service = profile_->GetExtensionService();
|
| + for (ExtensionMenuItem::List::iterator iter = common_tasks.begin();
|
| + iter != common_tasks.end();
|
| + ++iter) {
|
| + if ((*iter)->type() != ExtensionMenuItem::NORMAL)
|
| + continue;
|
| + const std::string extension_id = (*iter)->extension_id();
|
| + const Extension* extension = service->GetExtensionById(extension_id, false);
|
| + if (!extension) {
|
| + LOG(WARNING) << "Disabled extension" << extension_id;
|
| + continue;
|
| + }
|
| + DictionaryValue* task = new DictionaryValue();
|
| + task->SetString("taskId", MakeTaskID(kContextTaskIdSchema,
|
| + extension_id.c_str(),
|
| + (*iter)->id().uid));
|
| + task->SetString("title", (*iter)->title());
|
| + GURL icon =
|
| + ExtensionIconSource::GetIconURL(extension,
|
| + Extension::EXTENSION_ICON_SMALLISH,
|
| + ExtensionIconSet::MATCH_BIGGER,
|
| + false); // grayscale
|
| + task->SetString("iconUrl", icon.spec());
|
| + result_list->Append(task);
|
| + }
|
| +
|
| + // TODO(zelidrag, serya): Add intent content tasks to result_list once we
|
| + // implement that API.
|
| + SendResponse(true);
|
| + return true;
|
| +}
|
| +
|
| +bool ExecuteTasksFileBrowserFunction::RunImpl() {
|
| + // First param is task id that was to the extension with getFileTasks call.
|
| + std::string task_id;
|
| + if (!args_->GetString(0, &task_id) || !task_id.size())
|
| + return false;
|
| +
|
| + // The second param is the list of files that need to be executed with this
|
| + // task.
|
| + ListValue* files_list = NULL;
|
| + if (!args_->GetList(1, &files_list))
|
| + return false;
|
| +
|
| + if (!files_list->GetSize())
|
| + return true;
|
| +
|
| + std::string task_type;
|
| + std::string target_extension_id;
|
| + std::string action_id;
|
| + if (!CrackTaskIdentifier(task_id, &task_type, &target_extension_id,
|
| + &action_id)) {
|
| + return false;
|
| + }
|
| +
|
| + if (task_type == kContextTaskIdSchema) {
|
| + ExecuteContextMenuTasks(target_extension_id, action_id, files_list);
|
| + } else {
|
| + LOG(WARNING) << "Unsupported task type of: " << task_type;
|
| + // TODO(zelidrag, serya): Add intent content tasks here once we implement
|
| + // that API.
|
| + return false;
|
| + }
|
| + SendResponse(true);
|
| + return true;
|
| +}
|
| +
|
| +std::string ExecuteTasksFileBrowserFunction::MakeSafeFileUrl(
|
| + const std::string& origin_file_url, const std::string& extension_id) {
|
| + // Replace extension part of the url with one from the target.
|
| + std::string::size_type pos = origin_file_url.find("/local/");
|
| + if (pos == std::string::npos)
|
| + return std::string();
|
| + std::string file_url = base::StringPrintf(
|
| + "filesystem:chrome-extension://%s/%s",
|
| + extension_id.c_str(),
|
| + origin_file_url.substr(pos + 1).c_str());
|
| + std::string hash =
|
| + FileHashGenerator::GetInstance()->GenerateFileHash(file_url,
|
| + extension_id);
|
| + file_url = file_url.append("#");
|
| + return file_url.append(hash);
|
| +}
|
| +
|
| +bool ExecuteTasksFileBrowserFunction::ExecuteContextMenuTasks(
|
| + const std::string& extension_id, const std::string& action_id,
|
| + ListValue* files_list) {
|
| + ExtensionMenuManager* manager =
|
| + profile_->GetExtensionService()->menu_manager();
|
| + for (size_t i = 0; i < files_list->GetSize(); i++) {
|
| + std::string origin_file_url;
|
| + if (!files_list->GetString(i, &origin_file_url)) {
|
| + result_.reset(new FundamentalValue(false));
|
| + SendResponse(true);
|
| + return false;
|
| + }
|
| + std::string file_url = MakeSafeFileUrl(origin_file_url, extension_id);
|
| + if (!file_url.size()) {
|
| + result_.reset(new FundamentalValue(false));
|
| + SendResponse(true);
|
| + return false;
|
| + }
|
| + ContextMenuParams params;
|
| + CreateContextMenuParams(source_url_, file_url, ¶ms);
|
| + ExtensionMenuItem::Id menuItemId(profile_, extension_id,
|
| + atoi(action_id.c_str()));
|
| + manager->ExecuteCommand(profile_,
|
| + NULL, // tab_contents, not needed in args.
|
| + params,
|
| + menuItemId);
|
| + }
|
| + result_.reset(new FundamentalValue(true));
|
| + SendResponse(true);
|
| + return true;
|
| +}
|
|
|