Index: chrome/browser/extensions/api/file_system/file_system_api.cc |
diff --git a/chrome/browser/extensions/api/file_system/file_system_api.cc b/chrome/browser/extensions/api/file_system/file_system_api.cc |
index c02d5e011a633df8a2da4569854f4b0246ebfbcc..e347fcf7675118211deb07f259fc51e4d284c104 100644 |
--- a/chrome/browser/extensions/api/file_system/file_system_api.cc |
+++ b/chrome/browser/extensions/api/file_system/file_system_api.cc |
@@ -12,6 +12,7 @@ |
#include "base/logging.h" |
#include "base/path_service.h" |
#include "base/strings/string_util.h" |
+#include "base/strings/stringprintf.h" |
#include "base/strings/sys_string_conversions.h" |
#include "base/strings/utf_string_conversions.h" |
#include "base/value_conversions.h" |
@@ -60,10 +61,13 @@ const char kSecurityError[] = "Security error"; |
const char kInvalidCallingPage[] = "Invalid calling page. This function can't " |
"be called from a background page."; |
const char kUserCancelled[] = "User cancelled"; |
-const char kWritableFileError[] = |
+const char kWritableFileRestrictedLocationError[] = |
"Cannot write to file in a restricted location"; |
+const char kWritableFileErrorFormat[] = "Error opening %s"; |
const char kRequiresFileSystemWriteError[] = |
"Operation requires fileSystem.write permission"; |
+const char kMultipleUnsupportedError[] = |
+ "acceptsMultiple: true is not supported for 'saveFile'"; |
const char kUnknownIdError[] = "Unknown id"; |
namespace file_system = extensions::api::file_system; |
@@ -156,6 +160,7 @@ base::FilePath PrettifyPath(const base::FilePath& source_path) { |
bool g_skip_picker_for_test = false; |
bool g_use_suggested_path_for_test = false; |
base::FilePath* g_path_to_be_picked_for_test; |
+std::vector<base::FilePath>* g_paths_to_be_picked_for_test; |
bool GetFileSystemAndPathOfFileEntry( |
const std::string& filesystem_name, |
@@ -210,13 +215,19 @@ bool GetFilePathOfFileEntry(const std::string& filesystem_name, |
} |
bool DoCheckWritableFile(const base::FilePath& path, |
- const base::FilePath& extension_directory) { |
+ const base::FilePath& extension_directory, |
+ std::string* error_message) { |
// Don't allow links. |
- if (base::PathExists(path) && file_util::IsLink(path)) |
+ if (base::PathExists(path) && file_util::IsLink(path)) { |
+ *error_message = base::StringPrintf(kWritableFileErrorFormat, |
+ path.BaseName().AsUTF8Unsafe().c_str()); |
return false; |
+ } |
- if (extension_directory == path || extension_directory.IsParent(path)) |
+ if (extension_directory == path || extension_directory.IsParent(path)) { |
+ *error_message = kWritableFileRestrictedLocationError; |
return false; |
+ } |
bool is_whitelisted_path = false; |
@@ -236,6 +247,7 @@ bool DoCheckWritableFile(const base::FilePath& path, |
base::FilePath blacklisted_path; |
if (PathService::Get(kBlacklistedPaths[i], &blacklisted_path) && |
(blacklisted_path == path || blacklisted_path.IsParent(path))) { |
+ *error_message = kWritableFileRestrictedLocationError; |
return false; |
} |
} |
@@ -251,29 +263,120 @@ bool DoCheckWritableFile(const base::FilePath& path, |
// Close the file so we don't keep a lock open. |
if (file != base::kInvalidPlatformFileValue) |
base::ClosePlatformFile(file); |
- return error == base::PLATFORM_FILE_OK || |
- error == base::PLATFORM_FILE_ERROR_EXISTS; |
-} |
+ if (error != base::PLATFORM_FILE_OK && |
+ error != base::PLATFORM_FILE_ERROR_EXISTS) { |
+ *error_message = base::StringPrintf(kWritableFileErrorFormat, |
+ path.BaseName().AsUTF8Unsafe().c_str()); |
+ return false; |
+ } |
-void CheckLocalWritableFile(const base::FilePath& path, |
- const base::FilePath& extension_directory, |
- const base::Closure& on_success, |
- const base::Closure& on_failure) { |
- DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); |
- content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE, |
- DoCheckWritableFile(path, extension_directory) ? on_success : on_failure); |
+ return true; |
} |
+// Checks whether a list of paths are all OK for writing and calls a provided |
+// on_success or on_failure callback when done. A file is OK for writing if it |
+// is not a symlink, is not in a blacklisted path and can be opened for writing; |
+// files are created if they do not exist. |
+class WritableFileChecker |
+ : public base::RefCountedThreadSafe<WritableFileChecker> { |
+ public: |
+ WritableFileChecker( |
+ const std::vector<base::FilePath>& paths, |
+ Profile* profile, |
+ const base::FilePath& extension_path, |
+ const base::Closure& on_success, |
+ const base::Callback<void(const std::string&)>& on_failure) |
+ : outstanding_tasks_(1), |
+ extension_path_(extension_path), |
+ on_success_(on_success), |
+ on_failure_(on_failure) { |
#if defined(OS_CHROMEOS) |
-void CheckRemoteWritableFile(const base::Closure& on_success, |
- const base::Closure& on_failure, |
- drive::FileError error, |
- const base::FilePath& path) { |
- content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE, |
- error == drive::FILE_ERROR_OK ? on_success : on_failure); |
-} |
+ if (drive::util::IsUnderDriveMountPoint(paths[0])) { |
+ outstanding_tasks_ = paths.size(); |
+ for (std::vector<base::FilePath>::const_iterator it = paths.begin(); |
+ it != paths.end(); ++it) { |
+ DCHECK(drive::util::IsUnderDriveMountPoint(*it)); |
+ drive::util::PrepareWritableFileAndRun( |
+ profile, |
+ *it, |
+ base::Bind(&WritableFileChecker::CheckRemoteWritableFile, this)); |
+ } |
+ return; |
+ } |
+#endif |
+ content::BrowserThread::PostTask( |
+ content::BrowserThread::FILE, |
+ FROM_HERE, |
+ base::Bind(&WritableFileChecker::CheckLocalWritableFiles, this, paths)); |
+ } |
+ |
+ private: |
+ friend class base::RefCountedThreadSafe<WritableFileChecker>; |
+ virtual ~WritableFileChecker() {} |
+ |
+ // Called when a work item is completed. If all work items are done, this |
+ // posts a task to run AllTasksDone on the UI thread. |
+ void TaskDone() { |
+ DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); |
+ if (--outstanding_tasks_ == 0) { |
+ content::BrowserThread::PostTask( |
+ content::BrowserThread::UI, |
+ FROM_HERE, |
+ base::Bind(&WritableFileChecker::AllTasksDone, this)); |
+ } |
+ } |
+ |
+ // Called on the UI thread when all tasks are done. |
+ void AllTasksDone() { |
+ DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
+ if (error_.empty()) |
+ on_success_.Run(); |
+ else |
+ on_failure_.Run(error_); |
+ } |
+ |
+ // Reports an error in completing a work item. This may be called more than |
+ // once, but only the last message will be retained. |
+ void Error(const std::string& message) { |
+ DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); |
+ DCHECK(!message.empty()); |
+ error_ = message; |
+ TaskDone(); |
+ } |
+ |
+ void CheckLocalWritableFiles(const std::vector<base::FilePath>& paths) { |
+ DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); |
+ std::string error; |
+ for (std::vector<base::FilePath>::const_iterator it = paths.begin(); |
+ it != paths.end(); ++it) { |
+ if (!DoCheckWritableFile(*it, extension_path_, &error)) { |
+ Error(error); |
+ return; |
+ } |
+ } |
+ TaskDone(); |
+ } |
+ |
+#if defined(OS_CHROMEOS) |
+ void CheckRemoteWritableFile(drive::FileError error, |
+ const base::FilePath& path) { |
+ DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); |
+ if (error == drive::FILE_ERROR_OK) { |
+ TaskDone(); |
+ } else { |
+ Error(base::StringPrintf(kWritableFileErrorFormat, |
+ path.BaseName().AsUTF8Unsafe().c_str())); |
+ } |
+ } |
#endif |
+ int outstanding_tasks_; |
+ const base::FilePath extension_path_; |
+ std::string error_; |
+ base::Closure on_success_; |
+ base::Callback<void(const std::string&)> on_failure_; |
+}; |
+ |
// Expand the mime-types and extensions provided in an AcceptOption, returning |
// them within the passed extension vector. Returns false if no valid types |
// were found. |
@@ -387,6 +490,11 @@ bool FileSystemGetDisplayPathFunction::RunImpl() { |
return true; |
} |
+FileSystemEntryFunction::FileSystemEntryFunction() |
+ : multiple_(false), |
+ entry_type_(READ_ONLY), |
+ response_(NULL) {} |
+ |
bool FileSystemEntryFunction::HasFileSystemWritePermission() { |
const extensions::Extension* extension = GetExtension(); |
if (!extension) |
@@ -395,60 +503,68 @@ bool FileSystemEntryFunction::HasFileSystemWritePermission() { |
return extension->HasAPIPermission(APIPermission::kFileSystemWrite); |
} |
-void FileSystemEntryFunction::CheckWritableFile(const base::FilePath& path) { |
+void FileSystemEntryFunction::CheckWritableFiles( |
+ const std::vector<base::FilePath>& paths) { |
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
- base::Closure on_success = |
- base::Bind(&FileSystemEntryFunction::RegisterFileSystemAndSendResponse, |
- this, path, WRITABLE); |
- base::Closure on_failure = |
- base::Bind(&FileSystemEntryFunction::HandleWritableFileError, this); |
+ scoped_refptr<WritableFileChecker> helper = new WritableFileChecker( |
+ paths, profile_, extension_->path(), |
+ base::Bind( |
+ &FileSystemEntryFunction::RegisterFileSystemsAndSendResponse, |
+ this, paths), |
+ base::Bind(&FileSystemEntryFunction::HandleWritableFileError, this)); |
+} |
-#if defined(OS_CHROMEOS) |
- if (drive::util::IsUnderDriveMountPoint(path)) { |
- drive::util::PrepareWritableFileAndRun(profile_, path, |
- base::Bind(&CheckRemoteWritableFile, on_success, on_failure)); |
- return; |
+void FileSystemEntryFunction::RegisterFileSystemsAndSendResponse( |
+ const std::vector<base::FilePath>& paths) { |
+ DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
+ |
+ CreateResponse(); |
+ for (std::vector<base::FilePath>::const_iterator it = paths.begin(); |
+ it != paths.end(); ++it) { |
+ AddEntryToResponse(*it, ""); |
} |
-#endif |
- content::BrowserThread::PostTask(content::BrowserThread::FILE, FROM_HERE, |
- base::Bind(&CheckLocalWritableFile, path, extension_->path(), on_success, |
- on_failure)); |
+ SendResponse(true); |
} |
-void FileSystemEntryFunction::RegisterFileSystemAndSendResponse( |
- const base::FilePath& path, EntryType entry_type) { |
- RegisterFileSystemAndSendResponseWithIdOverride(path, entry_type, ""); |
+void FileSystemEntryFunction::CreateResponse() { |
+ DCHECK(!response_); |
+ response_ = new base::DictionaryValue(); |
+ base::ListValue* list = new base::ListValue(); |
+ response_->Set("entries", list); |
+ response_->SetBoolean("multiple", multiple_); |
+ SetResult(response_); |
} |
-void FileSystemEntryFunction::RegisterFileSystemAndSendResponseWithIdOverride( |
- const base::FilePath& path, EntryType entry_type, const std::string& id) { |
- DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
- |
- fileapi::IsolatedContext* isolated_context = |
- fileapi::IsolatedContext::GetInstance(); |
- DCHECK(isolated_context); |
- |
- bool writable = entry_type == WRITABLE; |
+void FileSystemEntryFunction::AddEntryToResponse( |
+ const base::FilePath& path, |
+ const std::string& id_override) { |
+ DCHECK(response_); |
+ bool writable = entry_type_ == WRITABLE; |
extensions::app_file_handler_util::GrantedFileEntry file_entry = |
- extensions::app_file_handler_util::CreateFileEntry(profile(), |
- GetExtension()->id(), render_view_host_->GetProcess()->GetID(), path, |
+ extensions::app_file_handler_util::CreateFileEntry( |
+ profile(), |
+ GetExtension()->id(), |
+ render_view_host_->GetProcess()->GetID(), |
+ path, |
writable); |
- |
- base::DictionaryValue* dict = new base::DictionaryValue(); |
- SetResult(dict); |
- dict->SetString("fileSystemId", file_entry.filesystem_id); |
- dict->SetString("baseName", file_entry.registered_name); |
- if (id.empty()) |
- dict->SetString("id", file_entry.id); |
+ base::ListValue* entries; |
+ bool success = response_->GetList("entries", &entries); |
+ DCHECK(success); |
+ |
+ base::DictionaryValue* entry = new base::DictionaryValue(); |
+ entry->SetString("fileSystemId", file_entry.filesystem_id); |
+ entry->SetString("baseName", file_entry.registered_name); |
+ if (id_override.empty()) |
+ entry->SetString("id", file_entry.id); |
else |
- dict->SetString("id", id); |
- |
- SendResponse(true); |
+ entry->SetString("id", id_override); |
+ entries->Append(entry); |
} |
-void FileSystemEntryFunction::HandleWritableFileError() { |
+void FileSystemEntryFunction::HandleWritableFileError( |
+ const std::string& error) { |
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
- error_ = kWritableFileError; |
+ error_ = error; |
SendResponse(false); |
} |
@@ -462,13 +578,16 @@ bool FileSystemGetWritableEntryFunction::RunImpl() { |
error_ = kRequiresFileSystemWriteError; |
return false; |
} |
+ entry_type_ = WRITABLE; |
base::FilePath path; |
if (!GetFilePathOfFileEntry(filesystem_name, filesystem_path, |
render_view_host_, &path, &error_)) |
return false; |
- CheckWritableFile(path); |
+ std::vector<base::FilePath> paths; |
+ paths.push_back(path); |
+ CheckWritableFiles(paths); |
return true; |
} |
@@ -503,10 +622,8 @@ class FileSystemChooseEntryFunction::FilePicker |
content::WebContents* web_contents, |
const base::FilePath& suggested_name, |
const ui::SelectFileDialog::FileTypeInfo& file_type_info, |
- ui::SelectFileDialog::Type picker_type, |
- EntryType entry_type) |
- : entry_type_(entry_type), |
- function_(function) { |
+ ui::SelectFileDialog::Type picker_type) |
+ : function_(function) { |
select_file_dialog_ = ui::SelectFileDialog::Create( |
this, new ChromeSelectFilePolicy(web_contents)); |
gfx::NativeWindow owning_window = web_contents ? |
@@ -521,11 +638,21 @@ class FileSystemChooseEntryFunction::FilePicker |
base::Unretained(this), suggested_name, 1, |
static_cast<void*>(NULL))); |
} else if (g_path_to_be_picked_for_test) { |
- content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE, |
+ content::BrowserThread::PostTask( |
+ content::BrowserThread::UI, FROM_HERE, |
base::Bind( |
&FileSystemChooseEntryFunction::FilePicker::FileSelected, |
base::Unretained(this), *g_path_to_be_picked_for_test, 1, |
static_cast<void*>(NULL))); |
+ } else if (g_paths_to_be_picked_for_test) { |
+ content::BrowserThread::PostTask( |
+ content::BrowserThread::UI, |
+ FROM_HERE, |
+ base::Bind( |
+ &FileSystemChooseEntryFunction::FilePicker::MultiFilesSelected, |
+ base::Unretained(this), |
+ *g_paths_to_be_picked_for_test, |
+ static_cast<void*>(NULL))); |
} else { |
content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE, |
base::Bind( |
@@ -553,8 +680,9 @@ class FileSystemChooseEntryFunction::FilePicker |
virtual void FileSelected(const base::FilePath& path, |
int index, |
void* params) OVERRIDE { |
- function_->FileSelected(path, entry_type_); |
- delete this; |
+ std::vector<base::FilePath> paths; |
+ paths.push_back(path); |
+ MultiFilesSelected(paths, params); |
} |
virtual void FileSelectedWithExtraInfo(const ui::SelectedFileInfo& file, |
@@ -568,17 +696,31 @@ class FileSystemChooseEntryFunction::FilePicker |
// |
// TODO(kinaba): remove this, once after the file picker implements proper |
// switch of the path treatment depending on the |support_drive| flag. |
- function_->FileSelected(file.file_path, entry_type_); |
+ FileSelected(file.file_path, index, params); |
+ } |
+ |
+ virtual void MultiFilesSelected(const std::vector<base::FilePath>& files, |
+ void* params) OVERRIDE { |
+ function_->FilesSelected(files); |
delete this; |
} |
+ virtual void MultiFilesSelectedWithExtraInfo( |
+ const std::vector<ui::SelectedFileInfo>& files, |
+ void* params) OVERRIDE { |
+ std::vector<base::FilePath> paths; |
+ for (std::vector<ui::SelectedFileInfo>::const_iterator it = files.begin(); |
+ it != files.end(); ++it) { |
+ paths.push_back(it->file_path); |
+ } |
+ MultiFilesSelected(paths, params); |
+ } |
+ |
virtual void FileSelectionCanceled(void* params) OVERRIDE { |
function_->FileSelectionCanceled(); |
delete this; |
} |
- EntryType entry_type_; |
- |
scoped_refptr<ui::SelectFileDialog> select_file_dialog_; |
scoped_refptr<FileSystemChooseEntryFunction> function_; |
@@ -587,8 +729,7 @@ class FileSystemChooseEntryFunction::FilePicker |
void FileSystemChooseEntryFunction::ShowPicker( |
const ui::SelectFileDialog::FileTypeInfo& file_type_info, |
- ui::SelectFileDialog::Type picker_type, |
- EntryType entry_type) { |
+ ui::SelectFileDialog::Type picker_type) { |
// TODO(asargent/benwells) - As a short term remediation for crbug.com/179010 |
// we're adding the ability for a whitelisted extension to use this API since |
// chrome.fileBrowserHandler.selectFile is ChromeOS-only. Eventually we'd |
@@ -613,8 +754,8 @@ void FileSystemChooseEntryFunction::ShowPicker( |
// its destruction (and subsequent sending of the function response) until the |
// user has selected a file or cancelled the picker. At that point, the picker |
// will delete itself, which will also free the function instance. |
- new FilePicker(this, web_contents, initial_path_, file_type_info, |
- picker_type, entry_type); |
+ new FilePicker( |
+ this, web_contents, initial_path_, file_type_info, picker_type); |
} |
// static |
@@ -623,6 +764,14 @@ void FileSystemChooseEntryFunction::SkipPickerAndAlwaysSelectPathForTest( |
g_skip_picker_for_test = true; |
g_use_suggested_path_for_test = false; |
g_path_to_be_picked_for_test = path; |
+ g_paths_to_be_picked_for_test = NULL; |
+} |
+ |
+void FileSystemChooseEntryFunction::SkipPickerAndAlwaysSelectPathsForTest( |
+ std::vector<base::FilePath>* paths) { |
+ g_skip_picker_for_test = true; |
+ g_use_suggested_path_for_test = false; |
+ g_paths_to_be_picked_for_test = paths; |
} |
// static |
@@ -630,6 +779,7 @@ void FileSystemChooseEntryFunction::SkipPickerAndSelectSuggestedPathForTest() { |
g_skip_picker_for_test = true; |
g_use_suggested_path_for_test = true; |
g_path_to_be_picked_for_test = NULL; |
+ g_paths_to_be_picked_for_test = NULL; |
} |
// static |
@@ -637,6 +787,7 @@ void FileSystemChooseEntryFunction::SkipPickerAndAlwaysCancelForTest() { |
g_skip_picker_for_test = true; |
g_use_suggested_path_for_test = false; |
g_path_to_be_picked_for_test = NULL; |
+ g_paths_to_be_picked_for_test = NULL; |
} |
// static |
@@ -670,19 +821,18 @@ void FileSystemChooseEntryFunction::SetInitialPathOnFileThread( |
} |
} |
-void FileSystemChooseEntryFunction::FileSelected(const base::FilePath& path, |
- EntryType entry_type) { |
+void FileSystemChooseEntryFunction::FilesSelected( |
+ const std::vector<base::FilePath>& paths) { |
+ DCHECK(!paths.empty()); |
file_system_api::SetLastChooseEntryDirectory( |
- ExtensionPrefs::Get(profile()), |
- GetExtension()->id(), |
- path.DirName()); |
- if (entry_type == WRITABLE) { |
- CheckWritableFile(path); |
+ ExtensionPrefs::Get(profile()), GetExtension()->id(), paths[0].DirName()); |
+ if (entry_type_ == WRITABLE) { |
+ CheckWritableFiles(paths); |
return; |
} |
// Don't need to check the file, it's for reading. |
- RegisterFileSystemAndSendResponse(path, READ_ONLY); |
+ RegisterFileSystemsAndSendResponse(paths); |
} |
void FileSystemChooseEntryFunction::FileSelectionCanceled() { |
@@ -756,16 +906,22 @@ bool FileSystemChooseEntryFunction::RunImpl() { |
base::FilePath suggested_name; |
ui::SelectFileDialog::FileTypeInfo file_type_info; |
- EntryType entry_type = READ_ONLY; |
ui::SelectFileDialog::Type picker_type = |
ui::SelectFileDialog::SELECT_OPEN_FILE; |
file_system::ChooseEntryOptions* options = params->options.get(); |
if (options) { |
+ multiple_ = options->accepts_multiple; |
+ if (multiple_) |
+ picker_type = ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE; |
if (options->type == file_system::CHOOSE_ENTRY_TYPE_OPENWRITABLEFILE) { |
- entry_type = WRITABLE; |
+ entry_type_ = WRITABLE; |
} else if (options->type == file_system::CHOOSE_ENTRY_TYPE_SAVEFILE) { |
- entry_type = WRITABLE; |
+ if (multiple_) { |
+ error_ = kMultipleUnsupportedError; |
+ return false; |
+ } |
+ entry_type_ = WRITABLE; |
picker_type = ui::SelectFileDialog::SELECT_SAVEAS_FILE; |
} |
@@ -777,7 +933,7 @@ bool FileSystemChooseEntryFunction::RunImpl() { |
options->accepts.get(), options->accepts_all_types.get()); |
} |
- if (entry_type == WRITABLE && !HasFileSystemWritePermission()) { |
+ if (entry_type_ == WRITABLE && !HasFileSystemWritePermission()) { |
error_ = kRequiresFileSystemWriteError; |
return false; |
} |
@@ -798,7 +954,7 @@ bool FileSystemChooseEntryFunction::RunImpl() { |
suggested_name, previous_path), |
base::Bind( |
&FileSystemChooseEntryFunction::ShowPicker, this, file_type_info, |
- picker_type, entry_type)); |
+ picker_type)); |
return true; |
} |
@@ -868,13 +1024,11 @@ bool FileSystemRestoreEntryFunction::RunImpl() { |
// |needs_new_entry| will be false if the renderer already has an Entry for |
// |entry_id|. |
if (needs_new_entry) { |
- // Reuse the ID of the retained file entry so retainEntry returns the same |
- // ID that was passed to restoreEntry. |
- RegisterFileSystemAndSendResponseWithIdOverride( |
- file_entry->path, |
- file_entry->writable ? WRITABLE : READ_ONLY, |
- file_entry->id); |
+ entry_type_ = file_entry->writable ? WRITABLE : READ_ONLY; |
+ CreateResponse(); |
+ AddEntryToResponse(file_entry->path, file_entry->id); |
} |
+ SendResponse(true); |
return true; |
} |