Chromium Code Reviews| Index: chrome/browser/extensions/extension_file_browser_private_api.cc |
| =================================================================== |
| --- chrome/browser/extensions/extension_file_browser_private_api.cc (revision 81212) |
| +++ chrome/browser/extensions/extension_file_browser_private_api.cc (working copy) |
| @@ -4,106 +4,550 @@ |
| #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/logging.h" |
| +#include "base/memory/singleton.h" |
| +#include "base/stringprintf.h" |
| +#include "base/string_util.h" |
| #include "base/task.h" |
| #include "base/values.h" |
| #include "chrome/browser/profiles/profile.h" |
| +#include "chrome/browser/extensions/extension_event_router.h" |
| +#include "chrome/browser/extensions/extension_function_dispatcher.h" |
| +#include "chrome/browser/extensions/extension_service.h" |
| +#include "chrome/browser/ui/webui/extension_icon_source.h" |
| +#include "chrome/common/chrome_switches.h" |
| #include "chrome/common/extensions/extension.h" |
| +#include "chrome/common/extensions/file_browser_action.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 "grit/generated_resources.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/fileapi/file_system_util.h" |
| #include "ui/base/l10n/l10n_util.h" |
| +// 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; |
| + |
| +typedef std::vector< |
| + std::pair<std::string, const FileBrowserAction* > > |
|
Aaron Boodman
2011/04/12 22:47:21
Consider defining a struct. It is a bit more reada
|
| + NamedActionList; |
| + |
| +typedef std::vector<const FileBrowserAction*> ActionList; |
| + |
| +bool GetFileBrowserActions(Profile* profile, |
| + const GURL& selected_file_url, |
| + ActionList* results) { |
| + ExtensionService* service = profile->GetExtensionService(); |
| + if (!service) |
| + return false; // In unit-tests, we may not have an ExtensionService. |
| + |
| + for (ExtensionList::const_iterator iter = service->extensions()->begin(); |
| + iter != service->extensions()->end(); |
| + ++iter) { |
| + const Extension* extension = iter->get(); |
| + if (!extension->file_browser_actions()) |
| + continue; |
| + |
| + for (Extension::FileBrowserActionList::const_iterator action_iter = |
| + extension->file_browser_actions()->begin(); |
| + action_iter != extension->file_browser_actions()->end(); |
| + ++action_iter) { |
| + const FileBrowserAction* action = action_iter->get(); |
| + if (!action->MatchesURL(selected_file_url)) |
| + continue; |
| + |
| + results->push_back(action_iter->get()); |
| + } |
| + } |
| + return true; |
| +} |
| + |
| +// Given the list of selected files, returns array of context menu tasks |
| +// that are shared |
| +bool FindCommonTasks(Profile* profile, |
| + ListValue* files_list, |
| + NamedActionList* named_action_list) { |
| + named_action_list->clear(); |
| + ActionList common_tasks; |
| + for (size_t i = 0; i < files_list->GetSize(); ++i) { |
| + std::string file_url; |
| + if (!files_list->GetString(i, &file_url)) |
| + return false; |
| + |
| + ActionList file_actions; |
| + if (!GetFileBrowserActions(profile, GURL(file_url), &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(), |
|
Aaron Boodman
2011/04/12 22:47:21
indent--
zel
2011/04/13 17:49:55
Done.
|
| + 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()); |
| + ActionList intersection(common_tasks.size()); |
| + ActionList::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(), |
|
Aaron Boodman
2011/04/12 22:47:21
indent--
zel
2011/04/13 17:49:55
Done.
|
| + intersection_end); |
| + std::sort(common_tasks.begin(), common_tasks.end()); |
| + } |
| + } |
| + |
| + // At the end, sort the results by task title. |
| + // TODO(zelidrag): Wire this with ICU to make this sort I18N happy. |
| + for (ActionList::const_iterator iter = |
| + common_tasks.begin(); |
|
Aaron Boodman
2011/04/12 22:47:21
Move this line onto previous?
zel
2011/04/13 17:49:55
Done.
|
| + iter != common_tasks.end(); ++iter) { |
| + named_action_list->push_back( |
| + std::pair<std::string, const FileBrowserAction* >( |
| + (*iter)->default_title(), *iter)); |
| + } |
| + std::sort(named_action_list->begin(), named_action_list->end()); |
|
Aaron Boodman
2011/04/12 22:47:21
Is the only reason for named_action_list so that y
zel
2011/04/13 17:49:55
I know, that is what I was planning to do with my
|
| + 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* target_extension_id, |
| + std::string* action_id) { |
| + std::vector<std::string> result; |
| + int count = Tokenize(task_id, std::string("|"), &result); |
| + if (count != 2) |
| + return false; |
| + *target_extension_id = result[0]; |
| + *action_id = result[1]; |
| + return true; |
| +} |
| + |
| +std::string MakeTaskID(const char* extension_id, |
| + const char* action_id) { |
| + return base::StringPrintf("%s|%s", 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. |
| virtual void DidSucceed() OVERRIDE { |
| NOTREACHED(); |
| } |
| + |
| virtual void DidReadMetadata(const base::PlatformFileInfo& info, |
| const FilePath& unused) OVERRIDE { |
| NOTREACHED(); |
| } |
| + |
| virtual void DidReadDirectory( |
| const std::vector<base::FileUtilProxy::Entry>& entries, |
| bool has_more) OVERRIDE { |
| NOTREACHED(); |
| } |
| + |
| virtual void DidWrite(int64 bytes, bool complete) OVERRIDE { |
| NOTREACHED(); |
| } |
| + |
| virtual void DidOpenFileSystem(const std::string& name, |
| - const FilePath& path) OVERRIDE { |
| + const FilePath& root_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)); |
| + root_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() { |
|
Aaron Boodman
2011/04/12 22:47:21
You can also just pass Extension* into LocalFileSy
zel
2011/04/13 17:49:55
Done.
|
| + 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() { |
| + |
|
Aaron Boodman
2011/04/12 22:47:21
Unnecessary whitespace.
zel
2011/04/13 17:49:55
this method is no more
|
| + const Extension* extension = GetExtension(); |
| + if (!extension) |
| + return false; |
| + |
| + GURL file_origin_url; |
| + FilePath virtual_path; |
| + fileapi::FileSystemType type; |
| + fileapi::FileSystemPathManager* path_manager = |
| + profile_->GetFileSystemContext()->path_manager(); |
| + 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 being 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, |
| + 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) |
|
Aaron Boodman
2011/04/12 22:47:21
Same question with this as in other places.
zel
2011/04/13 17:49:55
same issue with compilation, will look at it later
|
| +#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& root_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->SetString("path", root_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()); |
|
Aaron Boodman
2011/04/12 22:47:21
You can avoid the cast with:
ListValue* result_li
zel
2011/04/13 17:49:55
Done.
|
| + |
| + NamedActionList common_tasks; |
| + if (!FindCommonTasks(profile_, files_list, &common_tasks)) |
| + return false; |
| + |
| + ExtensionService* service = profile_->GetExtensionService(); |
| + for (NamedActionList::iterator iter = common_tasks.begin(); |
| + iter != common_tasks.end(); |
| + ++iter) { |
| + const std::string extension_id = iter->second->extension_id(); |
| + const Extension* extension = service->GetExtensionById(extension_id, false); |
| + if (!extension) { |
|
Aaron Boodman
2011/04/12 22:47:21
This should probably be a CHECK. Is there any legi
zel
2011/04/13 17:49:55
Done.
|
| + LOG(WARNING) << "Disabled extension!?" << extension_id; |
| + continue; |
| + } |
| + DictionaryValue* task = new DictionaryValue(); |
| + task->SetString("taskId", MakeTaskID(extension_id.data(), |
| + iter->second->id().data())); |
| + task->SetString("title", iter->second->default_title()); |
| + // TODO(zelidrag): Figure out how to expose icon URL that task defined in |
| + // manifest instead of the default extension icon. |
| + 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 target_extension_id; |
| + std::string action_id; |
| + if (!CrackTaskIdentifier(task_id, &target_extension_id, |
| + &action_id)) { |
| + return false; |
| + } |
| + |
| + ExecuteContextMenuTasks(target_extension_id, action_id, files_list); |
| + 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; |
| + // Breakdown file URL. |
| + fileapi::FileSystemPathManager* path_manager = |
| + profile_->GetFileSystemContext()->path_manager(); |
| + if (!path_manager->CrackFileSystemPath(FilePath(origin_file_url.spec()), |
| + &file_origin_url, &type, |
| + &virtual_path)) { |
| + return false; |
| + } |
| + |
| + if (type != fileapi::kFileSystemTypeLocal) |
| + return false; |
| + |
| + // Grant access to this particular file to target extension. |
| + GURL target_origin_url(Extension::GetBaseURLFromExtensionId(extension_id)); |
| + fileapi::FileSystemMountPointProvider* provider = |
| + path_manager->local_provider(); |
| + if (!provider) |
| + return false; |
| + provider->GrantAccess(target_origin_url, virtual_path); |
| + GURL base_url = fileapi::GetFileSystemRootURI(target_origin_url, |
| + fileapi::kFileSystemTypeLocal); |
| + *target_file_url = GURL(base_url.spec() + virtual_path.value()); |
| + return true; |
| +} |
| + |
| +bool ExecuteTasksFileBrowserFunction::ExecuteContextMenuTasks( |
| + const std::string& handler_extension_id, const std::string& action_id, |
| + ListValue* files_list) { |
| + ExtensionService* service = profile_->GetExtensionService(); |
| + if (!service) |
| + return NULL; |
| + |
| + const Extension* extension = service->GetExtensionById(handler_extension_id, |
| + false); |
|
Aaron Boodman
2011/04/12 22:47:21
ExtensionFunction has a GetExtension() method.
zel
2011/04/13 17:49:55
yes, but that one gives me extension that is execu
|
| + if (!extension) |
| + return NULL; |
| + |
| + ExtensionEventRouter* event_router = profile_->GetExtensionEventRouter(); |
| + if (!event_router) |
| + return false; |
| + |
| + scoped_ptr<ListValue> event_args(new ListValue()); |
| + ListValue* files_urls = new ListValue(); |
| + event_args->Append(Value::CreateStringValue(action_id)); |
| + event_args->Append(files_urls); |
| + 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; |
| + } |
| + files_urls->Append(Value::CreateStringValue(handler_file_url.spec())); |
| + } |
| + std::string json_args; |
| + base::JSONWriter::Write(event_args.get(), false, &json_args); |
| + std::string event_name = "contextMenus"; |
| + event_router->DispatchEventToExtension( |
| + handler_extension_id, std::string("fileSystem.onExecuteAction"), |
| + json_args, profile_, |
| + GURL()); |
| + |
| + result_.reset(new FundamentalValue(true)); |
| + SendResponse(true); |
| + return true; |
| +} |
| + |
| FileDialogFunction::FileDialogFunction() { |
| } |
| @@ -296,3 +740,4 @@ |
| SendResponse(true); |
| return true; |
| } |
| + |
|
Aaron Boodman
2011/04/12 22:47:21
Unnecessary newline?
zel
2011/04/13 17:49:55
Done.
|