Index: chrome/browser/extensions/extension_file_browser_private_api.cc |
=================================================================== |
--- chrome/browser/extensions/extension_file_browser_private_api.cc (revision 80410) |
+++ chrome/browser/extensions/extension_file_browser_private_api.cc (working copy) |
@@ -4,20 +4,246 @@ |
#include "chrome/browser/extensions/extension_file_browser_private_api.h" |
+#include "base/base64.h" |
+#include "base/command_line.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/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" |
+static const int kSHA1HashSizeInBits = 160; |
+static const int kAESKeyLengthInBits = 256; |
+ |
+const char kContextTaskIdSchema[] = "context-task"; |
+ |
+// Error messages. |
+const char kFileError[] = "File error %d"; |
+const char kInvalidFileUrl[] = "Invalid file URL"; |
+ |
+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); |
+} |
+ |
+// 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 { |
ericu
2011/04/07 01:40:58
I thought you'd completely removed the hash stuff.
zel
2011/04/07 02:54:42
Yes, it's removed now.
|
+ 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[kSHA1HashSizeInBits/8]); |
+ if (!hmac_.Sign(data, digest.get(), kSHA1HashSizeInBits/8)) |
+ return std::string(); |
+ std::string output; |
+ if (!base::Base64Encode(std::string(reinterpret_cast<char*>(digest.get()), |
+ kSHA1HashSizeInBits/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, |
+ kAESKeyLengthInBits)); |
+ 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 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 +264,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. |
+ 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); |
+ |
+ // 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; |
+} |