Chromium Code Reviews| Index: chrome/browser/extensions/extension_file_browser_private_api.cc | 
| =================================================================== | 
| --- chrome/browser/extensions/extension_file_browser_private_api.cc (revision 80624) | 
| +++ chrome/browser/extensions/extension_file_browser_private_api.cc (working copy) | 
| @@ -4,20 +4,200 @@ | 
| #include "chrome/browser/extensions/extension_file_browser_private_api.h" | 
| +#include "base/base64.h" | 
| +#include "base/command_line.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/chrome_switches.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_mount_point_provider.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" | 
| +const char kContextTaskIdSchema[] = "context-task"; | 
| + | 
| +// Error messages. | 
| +const char kFileError[] = "File error %d"; | 
| +const char kInvalidFileUrl[] = "Invalid file URL"; | 
| 
 
abarth-chromium
2011/04/07 23:09:20
Does these error messages need to be localized?
 
zel
2011/04/07 23:11:00
From what I've seen in other API examples, we didn
 
 | 
| + | 
| +const int kReadOnlyFilePermissions = base::PLATFORM_FILE_OPEN | | 
| + base::PLATFORM_FILE_READ | | 
| + base::PLATFORM_FILE_EXCLUSIVE_READ | | 
| + base::PLATFORM_FILE_ASYNC; | 
| + | 
| +const int kReadWriteFilePermissions = base::PLATFORM_FILE_OPEN | | 
| + base::PLATFORM_FILE_CREATE | | 
| + base::PLATFORM_FILE_OPEN_ALWAYS | | 
| + base::PLATFORM_FILE_CREATE_ALWAYS | | 
| + base::PLATFORM_FILE_READ | | 
| + base::PLATFORM_FILE_WRITE | | 
| + base::PLATFORM_FILE_EXCLUSIVE_READ | | 
| + base::PLATFORM_FILE_EXCLUSIVE_WRITE | | 
| + base::PLATFORM_FILE_ASYNC | | 
| + base::PLATFORM_FILE_TRUNCATE | | 
| + base::PLATFORM_FILE_WRITE_ATTRIBUTES; | 
| + | 
| +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); | 
| +} | 
| + | 
| class LocalFileSystemCallbackDispatcher | 
| : public fileapi::FileSystemCallbackDispatcher { | 
| public: | 
| explicit LocalFileSystemCallbackDispatcher( | 
| - RequestLocalFileSystemFunction* function) : function_(function) { | 
| + RequestLocalFileSystemFunctionBase* function, | 
| + Profile* profile, | 
| + int child_id, | 
| + const GURL& source_url, | 
| + const GURL& file_url) | 
| + : function_(function), | 
| + profile_(profile), | 
| + child_id_(child_id), | 
| + source_url_(source_url), | 
| + file_url_(file_url) { | 
| DCHECK(function_); | 
| } | 
| // fileapi::FileSystemCallbackDispatcher overrides. | 
| @@ -38,62 +218,331 @@ | 
| } | 
| virtual void DidOpenFileSystem(const std::string& name, | 
| const FilePath& path) OVERRIDE { | 
| + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); | 
| + // Set up file permission access. | 
| + if (!file_url_.is_empty()) { | 
| + if (!SetupFileAccessPermissions()) { | 
| + DidFail(base::PLATFORM_FILE_ERROR_SECURITY); | 
| + return; | 
| + } | 
| + } else if (!SetupFileSystemAccessPermissions()) { | 
| + 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_; | 
| + | 
| + const Extension* GetExtension() { | 
| + std::string extension_id = source_url_.GetOrigin().host(); | 
| + ExtensionService* service = profile_->GetExtensionService(); | 
| + if (!service) | 
| + return NULL; | 
| + return service->GetExtensionById(extension_id, | 
| + false); // include_disabled | 
| + } | 
| + | 
| + // Checks legitimacy of file url and grants RO access permissions for that | 
| + // file to the target renderer process. | 
| + bool SetupFileAccessPermissions() { | 
| + GURL file_origin_url; | 
| + FilePath virtual_path; | 
| + fileapi::FileSystemType type; | 
| + fileapi::FileSystemPathManager* path_manager = | 
| + profile_->GetFileSystemContext()->path_manager(); | 
| + | 
| + const Extension* extension = GetExtension(); | 
| + if (!extension) | 
| + return false; | 
| + | 
| + if (!path_manager->CrackFileSystemPath(FilePath(file_url_.spec()), | 
| + &file_origin_url, | 
| + &type, | 
| + &virtual_path)) { | 
| + return false; | 
| + } | 
| + | 
| + if (type != fileapi::kFileSystemTypeLocal) | 
| + return false; | 
| + | 
| + // Make sure this url really used by the right caller extension. | 
| 
 
ericu
2011/04/08 02:12:14
s/really/is really being/ ?
 
zel
2011/04/08 04:56:48
Done.
 
 | 
| + 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), | 
| 
 
ericu
2011/04/08 02:12:14
FilePath constructor is redundant.
 
zel
2011/04/08 04:56:48
Done.
 
 | 
| + false); // create | 
| + FilePath finalFilePath = root_path.Append(virtual_path); | 
| + | 
| + // TODO(zelidrag): Add explicit R/W + R/O permissions for non-component | 
| + // extensions. | 
| + | 
| + // Grant R/O access permission to non-component extension and R/W to | 
| + // component extensions. | 
| + ChildProcessSecurityPolicy::GetInstance()->GrantPermissionsForFile( | 
| + child_id_, finalFilePath, | 
| + extension->location() != Extension::COMPONENT ? | 
| + kReadOnlyFilePermissions : kReadWriteFilePermissions); | 
| + return true; | 
| + } | 
| + | 
| + // Grants file system access permissions to file browser component. | 
| + bool SetupFileSystemAccessPermissions() { | 
| + const Extension* extension = GetExtension(); | 
| + if (!extension) | 
| + return false; | 
| + | 
| + // Make sure that only component extension can access the entire | 
| + // local file system. | 
| + if (extension->location() != Extension::COMPONENT | 
| +#ifndef NDEBUG | 
| + && !CommandLine::ForCurrentProcess()->HasSwitch( | 
| + switches::kExposePrivateExtensionApi) | 
| +#endif | 
| + ) { | 
| + NOTREACHED() << "Private method access by non-component extension " | 
| + << extension->id(); | 
| + return false; | 
| + } | 
| + | 
| + fileapi::FileSystemPathManager* path_manager = | 
| + profile_->GetFileSystemContext()->path_manager(); | 
| + fileapi::FileSystemMountPointProvider* provider = | 
| + path_manager->local_provider(); | 
| + if (!provider) | 
| + return false; | 
| + | 
| + // Grant R/W file permissions to the renderer hosting component | 
| + // extension for all paths exposed by our local file system provider. | 
| + std::vector<FilePath> root_dirs = provider->GetRootDirectories(); | 
| + for (std::vector<FilePath>::iterator iter = root_dirs.begin(); | 
| + iter != root_dirs.end(); | 
| + ++iter) { | 
| + ChildProcessSecurityPolicy::GetInstance()->GrantPermissionsForFile( | 
| + child_id_, *iter, kReadWriteFilePermissions); | 
| + } | 
| + return true; | 
| + } | 
| + | 
| + RequestLocalFileSystemFunctionBase* function_; | 
| + Profile* profile_; | 
| + // Renderer process id. | 
| + int child_id_; | 
| + // Extension source URL. | 
| + GURL source_url_; | 
| + GURL file_url_; | 
| DISALLOW_COPY_AND_ASSIGN(LocalFileSystemCallbackDispatcher); | 
| }; | 
| -RequestLocalFileSystemFunction::RequestLocalFileSystemFunction() { | 
| -} | 
| - | 
| -RequestLocalFileSystemFunction::~RequestLocalFileSystemFunction() { | 
| -} | 
| - | 
| -bool RequestLocalFileSystemFunction::RunImpl() { | 
| +void RequestLocalFileSystemFunctionBase::RequestOnFileThread( | 
| + const GURL& source_url, const GURL& 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_, | 
| + GURL(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 GURL& 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.is_empty() && file_url.is_valid()) | 
| + dict->SetString("fileUrl", file_url.spec()); | 
| SendResponse(true); | 
| } | 
| -void RequestLocalFileSystemFunction::RespondFailedOnUIThread( | 
| +void RequestLocalFileSystemFunctionBase::RespondFailedOnUIThread( | 
| base::PlatformFileError error_code) { | 
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 
| - result_.reset(new DictionaryValue()); | 
| - DictionaryValue* dict = reinterpret_cast<DictionaryValue*>(result_.get()); | 
| - dict->SetInteger("error", static_cast<int>(error_code)); | 
| + error_ = base::StringPrintf(kFileError, static_cast<int>(error_code)); | 
| + SendResponse(false); | 
| +} | 
| + | 
| +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; | 
| +} | 
| + | 
| +bool ExecuteTasksFileBrowserFunction::GrantLocalFileSystemAccess( | 
| + const GURL& origin_file_url, | 
| + const std::string& extension_id, | 
| + GURL* target_file_url) { | 
| + GURL file_origin_url; | 
| + FilePath virtual_path; | 
| + fileapi::FileSystemType type; | 
| + fileapi::FileSystemPathManager* path_manager = | 
| + profile_->GetFileSystemContext()->path_manager(); | 
| + // Breakdown file URL. | 
| + if (!path_manager->CrackFileSystemPath(FilePath(origin_file_url.spec()), | 
| + &file_origin_url, | 
| + &type, | 
| + &virtual_path)) { | 
| + return false; | 
| + } | 
| + // Grant access to this particular file to target extension. | 
| + GURL target_origin_url(base::StringPrintf( | 
| + "chrome-extension://%s/", extension_id.data())); | 
| + fileapi::FileSystemMountPointProvider* provider = | 
| + path_manager->local_provider(); | 
| + if (!provider) | 
| + return false; | 
| + provider->GrantAccess(target_origin_url, virtual_path); | 
| + *target_file_url = GURL(base::StringPrintf("filesystem:%slocal/%s", | 
| + target_origin_url.spec().data(), | 
| + virtual_path.value().data())); | 
| + return true; | 
| +} | 
| + | 
| +bool ExecuteTasksFileBrowserFunction::ExecuteContextMenuTasks( | 
| + const std::string& handler_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)) { | 
| + error_ = kInvalidFileUrl; | 
| + SendResponse(false); | 
| + return false; | 
| + } | 
| + GURL handler_file_url; | 
| + if (!GrantLocalFileSystemAccess(GURL(origin_file_url), | 
| + handler_extension_id, | 
| + &handler_file_url)) { | 
| + error_ = kInvalidFileUrl; | 
| + SendResponse(false); | 
| + return false; | 
| + } | 
| + ContextMenuParams params; | 
| + CreateContextMenuParams(source_url_, handler_file_url.spec(), ¶ms); | 
| + ExtensionMenuItem::Id menuItemId(profile_, handler_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; | 
| +} |