| // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/download/save_package.h" |
| |
| #include <algorithm> |
| |
| #include "base/file_path.h" |
| #include "base/file_util.h" |
| #include "base/i18n/file_util_icu.h" |
| #include "base/logging.h" |
| #include "base/message_loop.h" |
| #include "base/stl_util-inl.h" |
| #include "base/string_piece.h" |
| #include "base/string_split.h" |
| #include "base/sys_string_conversions.h" |
| #include "base/task.h" |
| #include "base/threading/thread.h" |
| #include "base/utf_string_conversions.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/download/download_item.h" |
| #include "chrome/browser/download/download_item_model.h" |
| #include "chrome/browser/download/download_manager.h" |
| #include "chrome/browser/download/download_prefs.h" |
| #include "chrome/browser/download/download_shelf.h" |
| #include "chrome/browser/download/download_util.h" |
| #include "chrome/browser/download/save_file.h" |
| #include "chrome/browser/download/save_file_manager.h" |
| #include "chrome/browser/download/save_item.h" |
| #include "chrome/browser/net/url_fixer_upper.h" |
| #include "chrome/browser/platform_util.h" |
| #include "chrome/browser/prefs/pref_member.h" |
| #include "chrome/browser/prefs/pref_service.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/tab_contents/tab_util.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/common/render_messages.h" |
| #include "chrome/common/url_constants.h" |
| #include "content/browser/browser_thread.h" |
| #include "content/browser/renderer_host/render_process_host.h" |
| #include "content/browser/renderer_host/render_view_host.h" |
| #include "content/browser/renderer_host/render_view_host_delegate.h" |
| #include "content/browser/renderer_host/resource_dispatcher_host.h" |
| #include "content/browser/tab_contents/tab_contents.h" |
| #include "content/common/notification_service.h" |
| #include "content/common/notification_type.h" |
| #include "grit/generated_resources.h" |
| #include "net/base/io_buffer.h" |
| #include "net/base/mime_util.h" |
| #include "net/base/net_util.h" |
| #include "net/url_request/url_request_context.h" |
| #include "third_party/WebKit/Source/WebKit/chromium/public/WebPageSerializerClient.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "net/url_request/url_request_context_getter.h" |
| |
| using base::Time; |
| using WebKit::WebPageSerializerClient; |
| |
| namespace { |
| |
| // A counter for uniquely identifying each save package. |
| int g_save_package_id = 0; |
| |
| // Default name which will be used when we can not get proper name from |
| // resource URL. |
| const char kDefaultSaveName[] = "saved_resource"; |
| |
| const FilePath::CharType kDefaultHtmlExtension[] = |
| #if defined(OS_WIN) |
| FILE_PATH_LITERAL("htm"); |
| #else |
| FILE_PATH_LITERAL("html"); |
| #endif |
| |
| // Maximum number of file ordinal number. I think it's big enough for resolving |
| // name-conflict files which has same base file name. |
| const int32 kMaxFileOrdinalNumber = 9999; |
| |
| // Maximum length for file path. Since Windows have MAX_PATH limitation for |
| // file path, we need to make sure length of file path of every saved file |
| // is less than MAX_PATH |
| #if defined(OS_WIN) |
| const uint32 kMaxFilePathLength = MAX_PATH - 1; |
| #elif defined(OS_POSIX) |
| const uint32 kMaxFilePathLength = PATH_MAX - 1; |
| #endif |
| |
| // Maximum length for file ordinal number part. Since we only support the |
| // maximum 9999 for ordinal number, which means maximum file ordinal number part |
| // should be "(9998)", so the value is 6. |
| const uint32 kMaxFileOrdinalNumberPartLength = 6; |
| |
| // If false, we don't prompt the user as to where to save the file. This |
| // exists only for testing. |
| bool g_should_prompt_for_filename = true; |
| |
| // Indexes used for specifying which element in the extensions dropdown |
| // the user chooses when picking a save type. |
| const int kSelectFileHtmlOnlyIndex = 1; |
| const int kSelectFileCompleteIndex = 2; |
| |
| // Used for mapping between SavePackageType constants and the indexes above. |
| const SavePackage::SavePackageType kIndexToSaveType[] = { |
| SavePackage::SAVE_TYPE_UNKNOWN, |
| SavePackage::SAVE_AS_ONLY_HTML, |
| SavePackage::SAVE_AS_COMPLETE_HTML, |
| }; |
| |
| // Used for mapping between the IDS_ string identifiers and the indexes above. |
| const int kIndexToIDS[] = { |
| 0, IDS_SAVE_PAGE_DESC_HTML_ONLY, IDS_SAVE_PAGE_DESC_COMPLETE, |
| }; |
| |
| int SavePackageTypeToIndex(SavePackage::SavePackageType type) { |
| for (size_t i = 0; i < arraysize(kIndexToSaveType); ++i) { |
| if (kIndexToSaveType[i] == type) |
| return i; |
| } |
| NOTREACHED(); |
| return -1; |
| } |
| |
| // Strip current ordinal number, if any. Should only be used on pure |
| // file names, i.e. those stripped of their extensions. |
| // TODO(estade): improve this to not choke on alternate encodings. |
| FilePath::StringType StripOrdinalNumber( |
| const FilePath::StringType& pure_file_name) { |
| FilePath::StringType::size_type r_paren_index = |
| pure_file_name.rfind(FILE_PATH_LITERAL(')')); |
| FilePath::StringType::size_type l_paren_index = |
| pure_file_name.rfind(FILE_PATH_LITERAL('(')); |
| if (l_paren_index >= r_paren_index) |
| return pure_file_name; |
| |
| for (FilePath::StringType::size_type i = l_paren_index + 1; |
| i != r_paren_index; ++i) { |
| if (!IsAsciiDigit(pure_file_name[i])) |
| return pure_file_name; |
| } |
| |
| return pure_file_name.substr(0, l_paren_index); |
| } |
| |
| // Check whether we can save page as complete-HTML for the contents which |
| // have specified a MIME type. Now only contents which have the MIME type |
| // "text/html" can be saved as complete-HTML. |
| bool CanSaveAsComplete(const std::string& contents_mime_type) { |
| return contents_mime_type == "text/html" || |
| contents_mime_type == "application/xhtml+xml"; |
| } |
| |
| } // namespace |
| |
| SavePackage::SavePackage(TabContents* tab_contents, |
| SavePackageType save_type, |
| const FilePath& file_full_path, |
| const FilePath& directory_full_path) |
| : TabContentsObserver(tab_contents), |
| file_manager_(NULL), |
| download_(NULL), |
| page_url_(GetUrlToBeSaved()), |
| saved_main_file_path_(file_full_path), |
| saved_main_directory_path_(directory_full_path), |
| title_(tab_contents->GetTitle()), |
| finished_(false), |
| user_canceled_(false), |
| disk_error_occurred_(false), |
| save_type_(save_type), |
| all_save_items_count_(0), |
| wait_state_(INITIALIZE), |
| tab_id_(tab_contents->GetRenderProcessHost()->id()), |
| unique_id_(g_save_package_id++), |
| ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)) { |
| DCHECK(page_url_.is_valid()); |
| DCHECK(save_type_ == SAVE_AS_ONLY_HTML || |
| save_type_ == SAVE_AS_COMPLETE_HTML); |
| DCHECK(!saved_main_file_path_.empty() && |
| saved_main_file_path_.value().length() <= kMaxFilePathLength); |
| DCHECK(!saved_main_directory_path_.empty() && |
| saved_main_directory_path_.value().length() < kMaxFilePathLength); |
| InternalInit(); |
| } |
| |
| SavePackage::SavePackage(TabContents* tab_contents) |
| : TabContentsObserver(tab_contents), |
| file_manager_(NULL), |
| download_(NULL), |
| page_url_(GetUrlToBeSaved()), |
| title_(tab_contents->GetTitle()), |
| finished_(false), |
| user_canceled_(false), |
| disk_error_occurred_(false), |
| save_type_(SAVE_TYPE_UNKNOWN), |
| all_save_items_count_(0), |
| wait_state_(INITIALIZE), |
| tab_id_(tab_contents->GetRenderProcessHost()->id()), |
| unique_id_(g_save_package_id++), |
| ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)) { |
| DCHECK(page_url_.is_valid()); |
| InternalInit(); |
| } |
| |
| // This is for testing use. Set |finished_| as true because we don't want |
| // method Cancel to be be called in destructor in test mode. |
| // We also don't call InternalInit(). |
| SavePackage::SavePackage(TabContents* tab_contents, |
| const FilePath& file_full_path, |
| const FilePath& directory_full_path) |
| : TabContentsObserver(tab_contents), |
| file_manager_(NULL), |
| download_(NULL), |
| saved_main_file_path_(file_full_path), |
| saved_main_directory_path_(directory_full_path), |
| finished_(true), |
| user_canceled_(false), |
| disk_error_occurred_(false), |
| save_type_(SAVE_TYPE_UNKNOWN), |
| all_save_items_count_(0), |
| wait_state_(INITIALIZE), |
| tab_id_(0), |
| unique_id_(g_save_package_id++), |
| ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)) { |
| } |
| |
| SavePackage::~SavePackage() { |
| // Stop receiving saving job's updates |
| if (!finished_ && !canceled()) { |
| // Unexpected quit. |
| Cancel(true); |
| } |
| |
| DCHECK(all_save_items_count_ == (waiting_item_queue_.size() + |
| completed_count() + |
| in_process_count())); |
| // Free all SaveItems. |
| while (!waiting_item_queue_.empty()) { |
| // We still have some items which are waiting for start to save. |
| SaveItem* save_item = waiting_item_queue_.front(); |
| waiting_item_queue_.pop(); |
| delete save_item; |
| } |
| |
| STLDeleteValues(&saved_success_items_); |
| STLDeleteValues(&in_progress_items_); |
| STLDeleteValues(&saved_failed_items_); |
| |
| // The DownloadItem is owned by DownloadManager. |
| download_ = NULL; |
| |
| file_manager_ = NULL; |
| |
| // If there's an outstanding save dialog, make sure it doesn't call us back |
| // now that we're gone. |
| if (select_file_dialog_.get()) |
| select_file_dialog_->ListenerDestroyed(); |
| } |
| |
| // Retrieves the URL to be saved from tab_contents_ variable. |
| GURL SavePackage::GetUrlToBeSaved() { |
| // Instead of using tab_contents_.GetURL here, we use url() |
| // (which is the "real" url of the page) |
| // from the NavigationEntry because it reflects its' origin |
| // rather than the displayed one (returned by GetURL) which may be |
| // different (like having "view-source:" on the front). |
| NavigationEntry* active_entry = |
| tab_contents()->controller().GetActiveEntry(); |
| return active_entry->url(); |
| } |
| |
| // Cancel all in progress request, might be called by user or internal error. |
| void SavePackage::Cancel(bool user_action) { |
| if (!canceled()) { |
| if (user_action) |
| user_canceled_ = true; |
| else |
| disk_error_occurred_ = true; |
| Stop(); |
| } |
| } |
| |
| // Init() can be called directly, or indirectly via GetSaveInfo(). In both |
| // cases, we need file_manager_ to be initialized, so we do this first. |
| void SavePackage::InternalInit() { |
| ResourceDispatcherHost* rdh = g_browser_process->resource_dispatcher_host(); |
| if (!rdh) { |
| NOTREACHED(); |
| return; |
| } |
| |
| file_manager_ = rdh->save_file_manager(); |
| if (!file_manager_) { |
| NOTREACHED(); |
| return; |
| } |
| } |
| |
| // Initialize the SavePackage. |
| bool SavePackage::Init() { |
| // Set proper running state. |
| if (wait_state_ != INITIALIZE) |
| return false; |
| |
| wait_state_ = START_PROCESS; |
| |
| // Initialize the request context and resource dispatcher. |
| Profile* profile = tab_contents()->profile(); |
| if (!profile) { |
| NOTREACHED(); |
| return false; |
| } |
| |
| request_context_getter_ = profile->GetRequestContext(); |
| |
| // Create the fake DownloadItem and display the view. |
| DownloadManager* download_manager = |
| tab_contents()->profile()->GetDownloadManager(); |
| download_ = new DownloadItem(download_manager, |
| saved_main_file_path_, |
| page_url_, |
| profile->IsOffTheRecord()); |
| |
| // Transfer the ownership to the download manager. We need the DownloadItem |
| // to be alive as long as the Profile is alive. |
| download_manager->SavePageAsDownloadStarted(download_); |
| |
| tab_contents()->OnStartDownload(download_); |
| |
| // Check save type and process the save page job. |
| if (save_type_ == SAVE_AS_COMPLETE_HTML) { |
| // Get directory |
| DCHECK(!saved_main_directory_path_.empty()); |
| GetAllSavableResourceLinksForCurrentPage(); |
| } else { |
| wait_state_ = NET_FILES; |
| SaveFileCreateInfo::SaveFileSource save_source = page_url_.SchemeIsFile() ? |
| SaveFileCreateInfo::SAVE_FILE_FROM_FILE : |
| SaveFileCreateInfo::SAVE_FILE_FROM_NET; |
| SaveItem* save_item = new SaveItem(page_url_, |
| GURL(), |
| this, |
| save_source); |
| // Add this item to waiting list. |
| waiting_item_queue_.push(save_item); |
| all_save_items_count_ = 1; |
| download_->set_total_bytes(1); |
| |
| DoSavingProcess(); |
| } |
| |
| return true; |
| } |
| |
| // On POSIX, the length of |pure_file_name| + |file_name_ext| is further |
| // restricted by NAME_MAX. The maximum allowed path looks like: |
| // '/path/to/save_dir' + '/' + NAME_MAX. |
| uint32 SavePackage::GetMaxPathLengthForDirectory(const FilePath& base_dir) { |
| #if defined(OS_POSIX) |
| return std::min(kMaxFilePathLength, |
| static_cast<uint32>(base_dir.value().length()) + |
| NAME_MAX + 1); |
| #else |
| return kMaxFilePathLength; |
| #endif |
| } |
| |
| // File name is considered being consist of pure file name, dot and file |
| // extension name. File name might has no dot and file extension, or has |
| // multiple dot inside file name. The dot, which separates the pure file |
| // name and file extension name, is last dot in the whole file name. |
| // This function is for making sure the length of specified file path is not |
| // great than the specified maximum length of file path and getting safe pure |
| // file name part if the input pure file name is too long. |
| // The parameter |dir_path| specifies directory part of the specified |
| // file path. The parameter |file_name_ext| specifies file extension |
| // name part of the specified file path (including start dot). The parameter |
| // |max_file_path_len| specifies maximum length of the specified file path. |
| // The parameter |pure_file_name| input pure file name part of the specified |
| // file path. If the length of specified file path is great than |
| // |max_file_path_len|, the |pure_file_name| will output new pure file name |
| // part for making sure the length of specified file path is less than |
| // specified maximum length of file path. Return false if the function can |
| // not get a safe pure file name, otherwise it returns true. |
| bool SavePackage::GetSafePureFileName(const FilePath& dir_path, |
| const FilePath::StringType& file_name_ext, |
| uint32 max_file_path_len, |
| FilePath::StringType* pure_file_name) { |
| DCHECK(!pure_file_name->empty()); |
| int available_length = static_cast<int>(max_file_path_len - |
| dir_path.value().length() - |
| file_name_ext.length()); |
| // Need an extra space for the separator. |
| if (!file_util::EndsWithSeparator(dir_path)) |
| --available_length; |
| |
| // Plenty of room. |
| if (static_cast<int>(pure_file_name->length()) <= available_length) |
| return true; |
| |
| // Limited room. Truncate |pure_file_name| to fit. |
| if (available_length > 0) { |
| *pure_file_name = pure_file_name->substr(0, available_length); |
| return true; |
| } |
| |
| // Not enough room to even use a shortened |pure_file_name|. |
| pure_file_name->clear(); |
| return false; |
| } |
| |
| // Generate name for saving resource. |
| bool SavePackage::GenerateFileName(const std::string& disposition, |
| const GURL& url, |
| bool need_html_ext, |
| FilePath::StringType* generated_name) { |
| // TODO(jungshik): Figure out the referrer charset when having one |
| // makes sense and pass it to GetSuggestedFilename. |
| string16 suggested_name = |
| net::GetSuggestedFilename(url, disposition, "", |
| ASCIIToUTF16(kDefaultSaveName)); |
| |
| // TODO(evan): this code is totally wrong -- we should just generate |
| // Unicode filenames and do all this encoding switching at the end. |
| // However, I'm just shuffling wrong code around, at least not adding |
| // to it. |
| #if defined(OS_WIN) |
| FilePath file_path = FilePath(suggested_name); |
| #else |
| FilePath file_path = FilePath( |
| base::SysWideToNativeMB(UTF16ToWide(suggested_name))); |
| #endif |
| |
| DCHECK(!file_path.empty()); |
| FilePath::StringType pure_file_name = |
| file_path.RemoveExtension().BaseName().value(); |
| FilePath::StringType file_name_ext = file_path.Extension(); |
| |
| // If it is HTML resource, use ".htm{l,}" as its extension. |
| if (need_html_ext) { |
| file_name_ext = FILE_PATH_LITERAL("."); |
| file_name_ext.append(kDefaultHtmlExtension); |
| } |
| |
| // Need to make sure the suggested file name is not too long. |
| uint32 max_path = GetMaxPathLengthForDirectory(saved_main_directory_path_); |
| |
| // Get safe pure file name. |
| if (!GetSafePureFileName(saved_main_directory_path_, file_name_ext, |
| max_path, &pure_file_name)) |
| return false; |
| |
| FilePath::StringType file_name = pure_file_name + file_name_ext; |
| |
| // Check whether we already have same name. |
| if (file_name_set_.find(file_name) == file_name_set_.end()) { |
| file_name_set_.insert(file_name); |
| } else { |
| // Found same name, increase the ordinal number for the file name. |
| FilePath::StringType base_file_name = StripOrdinalNumber(pure_file_name); |
| |
| // We need to make sure the length of base file name plus maximum ordinal |
| // number path will be less than or equal to kMaxFilePathLength. |
| if (!GetSafePureFileName(saved_main_directory_path_, file_name_ext, |
| max_path - kMaxFileOrdinalNumberPartLength, &base_file_name)) |
| return false; |
| |
| // Prepare the new ordinal number. |
| uint32 ordinal_number; |
| FileNameCountMap::iterator it = file_name_count_map_.find(base_file_name); |
| if (it == file_name_count_map_.end()) { |
| // First base-name-conflict resolving, use 1 as initial ordinal number. |
| file_name_count_map_[base_file_name] = 1; |
| ordinal_number = 1; |
| } else { |
| // We have met same base-name conflict, use latest ordinal number. |
| ordinal_number = it->second; |
| } |
| |
| if (ordinal_number > (kMaxFileOrdinalNumber - 1)) { |
| // Use a random file from temporary file. |
| FilePath temp_file; |
| file_util::CreateTemporaryFile(&temp_file); |
| file_name = temp_file.RemoveExtension().BaseName().value(); |
| // Get safe pure file name. |
| if (!GetSafePureFileName(saved_main_directory_path_, |
| FilePath::StringType(), |
| max_path, &file_name)) |
| return false; |
| } else { |
| for (int i = ordinal_number; i < kMaxFileOrdinalNumber; ++i) { |
| FilePath::StringType new_name = base_file_name + |
| StringPrintf(FILE_PATH_LITERAL("(%d)"), i) + file_name_ext; |
| if (file_name_set_.find(new_name) == file_name_set_.end()) { |
| // Resolved name conflict. |
| file_name = new_name; |
| file_name_count_map_[base_file_name] = ++i; |
| break; |
| } |
| } |
| } |
| |
| file_name_set_.insert(file_name); |
| } |
| |
| DCHECK(!file_name.empty()); |
| generated_name->assign(file_name); |
| |
| return true; |
| } |
| |
| // We have received a message from SaveFileManager about a new saving job. We |
| // create a SaveItem and store it in our in_progress list. |
| void SavePackage::StartSave(const SaveFileCreateInfo* info) { |
| DCHECK(info && !info->url.is_empty()); |
| |
| SaveUrlItemMap::iterator it = in_progress_items_.find(info->url.spec()); |
| if (it == in_progress_items_.end()) { |
| // If not found, we must have cancel action. |
| DCHECK(canceled()); |
| return; |
| } |
| SaveItem* save_item = it->second; |
| |
| DCHECK(!saved_main_file_path_.empty()); |
| |
| save_item->SetSaveId(info->save_id); |
| save_item->SetTotalBytes(info->total_bytes); |
| |
| // Determine the proper path for a saving job, by choosing either the default |
| // save directory, or prompting the user. |
| DCHECK(!save_item->has_final_name()); |
| if (info->url != page_url_) { |
| FilePath::StringType generated_name; |
| // For HTML resource file, make sure it will have .htm as extension name, |
| // otherwise, when you open the saved page in Chrome again, download |
| // file manager will treat it as downloadable resource, and download it |
| // instead of opening it as HTML. |
| bool need_html_ext = |
| info->save_source == SaveFileCreateInfo::SAVE_FILE_FROM_DOM; |
| if (!GenerateFileName(info->content_disposition, |
| GURL(info->url), |
| need_html_ext, |
| &generated_name)) { |
| // We can not generate file name for this SaveItem, so we cancel the |
| // saving page job if the save source is from serialized DOM data. |
| // Otherwise, it means this SaveItem is sub-resource type, we treat it |
| // as an error happened on saving. We can ignore this type error for |
| // sub-resource links which will be resolved as absolute links instead |
| // of local links in final saved contents. |
| if (info->save_source == SaveFileCreateInfo::SAVE_FILE_FROM_DOM) |
| Cancel(true); |
| else |
| SaveFinished(save_item->save_id(), 0, false); |
| return; |
| } |
| |
| // When saving page as only-HTML, we only have a SaveItem whose url |
| // must be page_url_. |
| DCHECK(save_type_ == SAVE_AS_COMPLETE_HTML); |
| DCHECK(!saved_main_directory_path_.empty()); |
| |
| // Now we get final name retrieved from GenerateFileName, we will use it |
| // rename the SaveItem. |
| FilePath final_name = saved_main_directory_path_.Append(generated_name); |
| save_item->Rename(final_name); |
| } else { |
| // It is the main HTML file, use the name chosen by the user. |
| save_item->Rename(saved_main_file_path_); |
| } |
| |
| // If the save source is from file system, inform SaveFileManager to copy |
| // corresponding file to the file path which this SaveItem specifies. |
| if (info->save_source == SaveFileCreateInfo::SAVE_FILE_FROM_FILE) { |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod(file_manager_, |
| &SaveFileManager::SaveLocalFile, |
| save_item->url(), |
| save_item->save_id(), |
| tab_id())); |
| return; |
| } |
| |
| // Check whether we begin to require serialized HTML data. |
| if (save_type_ == SAVE_AS_COMPLETE_HTML && wait_state_ == HTML_DATA) { |
| // Inform backend to serialize the all frames' DOM and send serialized |
| // HTML data back. |
| GetSerializedHtmlDataForCurrentPageWithLocalLinks(); |
| } |
| } |
| |
| // Look up SaveItem by save id from in progress map. |
| SaveItem* SavePackage::LookupItemInProcessBySaveId(int32 save_id) { |
| if (in_process_count()) { |
| for (SaveUrlItemMap::iterator it = in_progress_items_.begin(); |
| it != in_progress_items_.end(); ++it) { |
| SaveItem* save_item = it->second; |
| DCHECK(save_item->state() == SaveItem::IN_PROGRESS); |
| if (save_item->save_id() == save_id) |
| return save_item; |
| } |
| } |
| return NULL; |
| } |
| |
| // Remove SaveItem from in progress map and put it to saved map. |
| void SavePackage::PutInProgressItemToSavedMap(SaveItem* save_item) { |
| SaveUrlItemMap::iterator it = in_progress_items_.find( |
| save_item->url().spec()); |
| DCHECK(it != in_progress_items_.end()); |
| DCHECK(save_item == it->second); |
| in_progress_items_.erase(it); |
| |
| if (save_item->success()) { |
| // Add it to saved_success_items_. |
| DCHECK(saved_success_items_.find(save_item->save_id()) == |
| saved_success_items_.end()); |
| saved_success_items_[save_item->save_id()] = save_item; |
| } else { |
| // Add it to saved_failed_items_. |
| DCHECK(saved_failed_items_.find(save_item->url().spec()) == |
| saved_failed_items_.end()); |
| saved_failed_items_[save_item->url().spec()] = save_item; |
| } |
| } |
| |
| // Called for updating saving state. |
| bool SavePackage::UpdateSaveProgress(int32 save_id, |
| int64 size, |
| bool write_success) { |
| // Because we might have canceled this saving job before, |
| // so we might not find corresponding SaveItem. |
| SaveItem* save_item = LookupItemInProcessBySaveId(save_id); |
| if (!save_item) |
| return false; |
| |
| save_item->Update(size); |
| |
| // If we got disk error, cancel whole save page job. |
| if (!write_success) { |
| // Cancel job with reason of disk error. |
| Cancel(false); |
| } |
| return true; |
| } |
| |
| // Stop all page saving jobs that are in progress and instruct the file thread |
| // to delete all saved files. |
| void SavePackage::Stop() { |
| // If we haven't moved out of the initial state, there's nothing to cancel and |
| // there won't be valid pointers for file_manager_ or download_. |
| if (wait_state_ == INITIALIZE) |
| return; |
| |
| // When stopping, if it still has some items in in_progress, cancel them. |
| DCHECK(canceled()); |
| if (in_process_count()) { |
| SaveUrlItemMap::iterator it = in_progress_items_.begin(); |
| for (; it != in_progress_items_.end(); ++it) { |
| SaveItem* save_item = it->second; |
| DCHECK(save_item->state() == SaveItem::IN_PROGRESS); |
| save_item->Cancel(); |
| } |
| // Remove all in progress item to saved map. For failed items, they will |
| // be put into saved_failed_items_, for successful item, they will be put |
| // into saved_success_items_. |
| while (in_process_count()) |
| PutInProgressItemToSavedMap(in_progress_items_.begin()->second); |
| } |
| |
| // This vector contains the save ids of the save files which SaveFileManager |
| // needs to remove from its save_file_map_. |
| SaveIDList save_ids; |
| for (SavedItemMap::iterator it = saved_success_items_.begin(); |
| it != saved_success_items_.end(); ++it) |
| save_ids.push_back(it->first); |
| for (SaveUrlItemMap::iterator it = saved_failed_items_.begin(); |
| it != saved_failed_items_.end(); ++it) |
| save_ids.push_back(it->second->save_id()); |
| |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod(file_manager_, |
| &SaveFileManager::RemoveSavedFileFromFileMap, |
| save_ids)); |
| |
| finished_ = true; |
| wait_state_ = FAILED; |
| |
| // Inform the DownloadItem we have canceled whole save page job. |
| download_->Cancel(false); |
| } |
| |
| void SavePackage::CheckFinish() { |
| if (in_process_count() || finished_) |
| return; |
| |
| FilePath dir = (save_type_ == SAVE_AS_COMPLETE_HTML && |
| saved_success_items_.size() > 1) ? |
| saved_main_directory_path_ : FilePath(); |
| |
| // This vector contains the final names of all the successfully saved files |
| // along with their save ids. It will be passed to SaveFileManager to do the |
| // renaming job. |
| FinalNameList final_names; |
| for (SavedItemMap::iterator it = saved_success_items_.begin(); |
| it != saved_success_items_.end(); ++it) |
| final_names.push_back(std::make_pair(it->first, |
| it->second->full_path())); |
| |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod(file_manager_, |
| &SaveFileManager::RenameAllFiles, |
| final_names, |
| dir, |
| tab_contents()->GetRenderProcessHost()->id(), |
| tab_contents()->render_view_host()->routing_id(), |
| id())); |
| } |
| |
| // Successfully finished all items of this SavePackage. |
| void SavePackage::Finish() { |
| // User may cancel the job when we're moving files to the final directory. |
| if (canceled()) |
| return; |
| |
| wait_state_ = SUCCESSFUL; |
| finished_ = true; |
| |
| // This vector contains the save ids of the save files which SaveFileManager |
| // needs to remove from its save_file_map_. |
| SaveIDList save_ids; |
| for (SaveUrlItemMap::iterator it = saved_failed_items_.begin(); |
| it != saved_failed_items_.end(); ++it) |
| save_ids.push_back(it->second->save_id()); |
| |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod(file_manager_, |
| &SaveFileManager::RemoveSavedFileFromFileMap, |
| save_ids)); |
| |
| download_->OnAllDataSaved(all_save_items_count_); |
| download_->MarkAsComplete(); |
| |
| NotificationService::current()->Notify( |
| NotificationType::SAVE_PACKAGE_SUCCESSFULLY_FINISHED, |
| Source<SavePackage>(this), |
| Details<GURL>(&page_url_)); |
| } |
| |
| // Called for updating end state. |
| void SavePackage::SaveFinished(int32 save_id, int64 size, bool is_success) { |
| // Because we might have canceled this saving job before, |
| // so we might not find corresponding SaveItem. Just ignore it. |
| SaveItem* save_item = LookupItemInProcessBySaveId(save_id); |
| if (!save_item) |
| return; |
| |
| // Let SaveItem set end state. |
| save_item->Finish(size, is_success); |
| // Remove the associated save id and SavePackage. |
| file_manager_->RemoveSaveFile(save_id, save_item->url(), this); |
| |
| PutInProgressItemToSavedMap(save_item); |
| |
| // Inform the DownloadItem to update UI. |
| // We use the received bytes as number of saved files. |
| download_->Update(completed_count()); |
| |
| if (save_item->save_source() == SaveFileCreateInfo::SAVE_FILE_FROM_DOM && |
| save_item->url() == page_url_ && !save_item->received_bytes()) { |
| // If size of main HTML page is 0, treat it as disk error. |
| Cancel(false); |
| return; |
| } |
| |
| if (canceled()) { |
| DCHECK(finished_); |
| return; |
| } |
| |
| // Continue processing the save page job. |
| DoSavingProcess(); |
| |
| // Check whether we can successfully finish whole job. |
| CheckFinish(); |
| } |
| |
| // Sometimes, the net io will only call SaveFileManager::SaveFinished with |
| // save id -1 when it encounters error. Since in this case, save id will be |
| // -1, so we can only use URL to find which SaveItem is associated with |
| // this error. |
| // Saving an item failed. If it's a sub-resource, ignore it. If the error comes |
| // from serializing HTML data, then cancel saving page. |
| void SavePackage::SaveFailed(const GURL& save_url) { |
| SaveUrlItemMap::iterator it = in_progress_items_.find(save_url.spec()); |
| if (it == in_progress_items_.end()) { |
| NOTREACHED(); // Should not exist! |
| return; |
| } |
| SaveItem* save_item = it->second; |
| |
| save_item->Finish(0, false); |
| |
| PutInProgressItemToSavedMap(save_item); |
| |
| // Inform the DownloadItem to update UI. |
| // We use the received bytes as number of saved files. |
| download_->Update(completed_count()); |
| |
| if (save_type_ == SAVE_AS_ONLY_HTML || |
| save_item->save_source() == SaveFileCreateInfo::SAVE_FILE_FROM_DOM) { |
| // We got error when saving page. Treat it as disk error. |
| Cancel(true); |
| } |
| |
| if (canceled()) { |
| DCHECK(finished_); |
| return; |
| } |
| |
| // Continue processing the save page job. |
| DoSavingProcess(); |
| |
| CheckFinish(); |
| } |
| |
| void SavePackage::SaveCanceled(SaveItem* save_item) { |
| // Call the RemoveSaveFile in UI thread. |
| file_manager_->RemoveSaveFile(save_item->save_id(), |
| save_item->url(), |
| this); |
| if (save_item->save_id() != -1) |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod(file_manager_, |
| &SaveFileManager::CancelSave, |
| save_item->save_id())); |
| } |
| |
| // Initiate a saving job of a specific URL. We send the request to |
| // SaveFileManager, which will dispatch it to different approach according to |
| // the save source. Parameter process_all_remaining_items indicates whether |
| // we need to save all remaining items. |
| void SavePackage::SaveNextFile(bool process_all_remaining_items) { |
| DCHECK(tab_contents()); |
| DCHECK(waiting_item_queue_.size()); |
| |
| do { |
| // Pop SaveItem from waiting list. |
| SaveItem* save_item = waiting_item_queue_.front(); |
| waiting_item_queue_.pop(); |
| |
| // Add the item to in_progress_items_. |
| SaveUrlItemMap::iterator it = in_progress_items_.find( |
| save_item->url().spec()); |
| DCHECK(it == in_progress_items_.end()); |
| in_progress_items_[save_item->url().spec()] = save_item; |
| save_item->Start(); |
| file_manager_->SaveURL(save_item->url(), |
| save_item->referrer(), |
| tab_contents()->GetRenderProcessHost()->id(), |
| routing_id(), |
| save_item->save_source(), |
| save_item->full_path(), |
| request_context_getter_.get(), |
| this); |
| } while (process_all_remaining_items && waiting_item_queue_.size()); |
| } |
| |
| |
| // Open download page in windows explorer on file thread, to avoid blocking the |
| // user interface. |
| void SavePackage::ShowDownloadInShell() { |
| DCHECK(file_manager_); |
| DCHECK(finished_ && !canceled() && !saved_main_file_path_.empty()); |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| #if defined(OS_MACOSX) |
| // Mac OS X requires opening downloads on the UI thread. |
| platform_util::ShowItemInFolder(saved_main_file_path_); |
| #else |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod(file_manager_, |
| &SaveFileManager::OnShowSavedFileInShell, |
| saved_main_file_path_)); |
| #endif |
| } |
| |
| // Calculate the percentage of whole save page job. |
| int SavePackage::PercentComplete() { |
| if (!all_save_items_count_) |
| return 0; |
| else if (!in_process_count()) |
| return 100; |
| else |
| return completed_count() / all_save_items_count_; |
| } |
| |
| // Continue processing the save page job after one SaveItem has been |
| // finished. |
| void SavePackage::DoSavingProcess() { |
| if (save_type_ == SAVE_AS_COMPLETE_HTML) { |
| // We guarantee that images and JavaScripts must be downloaded first. |
| // So when finishing all those sub-resources, we will know which |
| // sub-resource's link can be replaced with local file path, which |
| // sub-resource's link need to be replaced with absolute URL which |
| // point to its internet address because it got error when saving its data. |
| SaveItem* save_item = NULL; |
| // Start a new SaveItem job if we still have job in waiting queue. |
| if (waiting_item_queue_.size()) { |
| DCHECK(wait_state_ == NET_FILES); |
| save_item = waiting_item_queue_.front(); |
| if (save_item->save_source() != SaveFileCreateInfo::SAVE_FILE_FROM_DOM) { |
| SaveNextFile(false); |
| } else if (!in_process_count()) { |
| // If there is no in-process SaveItem, it means all sub-resources |
| // have been processed. Now we need to start serializing HTML DOM |
| // for the current page to get the generated HTML data. |
| wait_state_ = HTML_DATA; |
| // All non-HTML resources have been finished, start all remaining |
| // HTML files. |
| SaveNextFile(true); |
| } |
| } else if (in_process_count()) { |
| // Continue asking for HTML data. |
| DCHECK(wait_state_ == HTML_DATA); |
| } |
| } else { |
| // Save as HTML only. |
| DCHECK(wait_state_ == NET_FILES); |
| DCHECK(save_type_ == SAVE_AS_ONLY_HTML); |
| if (waiting_item_queue_.size()) { |
| DCHECK(all_save_items_count_ == waiting_item_queue_.size()); |
| SaveNextFile(false); |
| } |
| } |
| } |
| |
| bool SavePackage::OnMessageReceived(const IPC::Message& message) { |
| bool handled = true; |
| IPC_BEGIN_MESSAGE_MAP(SavePackage, message) |
| IPC_MESSAGE_HANDLER(ViewHostMsg_SendCurrentPageAllSavableResourceLinks, |
| OnReceivedSavableResourceLinksForCurrentPage) |
| IPC_MESSAGE_HANDLER(ViewHostMsg_SendSerializedHtmlData, |
| OnReceivedSerializedHtmlData) |
| IPC_MESSAGE_UNHANDLED(handled = false) |
| IPC_END_MESSAGE_MAP() |
| return handled; |
| } |
| |
| // After finishing all SaveItems which need to get data from net. |
| // We collect all URLs which have local storage and send the |
| // map:(originalURL:currentLocalPath) to render process (backend). |
| // Then render process will serialize DOM and send data to us. |
| void SavePackage::GetSerializedHtmlDataForCurrentPageWithLocalLinks() { |
| if (wait_state_ != HTML_DATA) |
| return; |
| std::vector<GURL> saved_links; |
| std::vector<FilePath> saved_file_paths; |
| int successful_started_items_count = 0; |
| |
| // Collect all saved items which have local storage. |
| // First collect the status of all the resource files and check whether they |
| // have created local files although they have not been completely saved. |
| // If yes, the file can be saved. Otherwise, there is a disk error, so we |
| // need to cancel the page saving job. |
| for (SaveUrlItemMap::iterator it = in_progress_items_.begin(); |
| it != in_progress_items_.end(); ++it) { |
| DCHECK(it->second->save_source() == |
| SaveFileCreateInfo::SAVE_FILE_FROM_DOM); |
| if (it->second->has_final_name()) |
| successful_started_items_count++; |
| saved_links.push_back(it->second->url()); |
| saved_file_paths.push_back(it->second->file_name()); |
| } |
| |
| // If not all file of HTML resource have been started, then wait. |
| if (successful_started_items_count != in_process_count()) |
| return; |
| |
| // Collect all saved success items. |
| for (SavedItemMap::iterator it = saved_success_items_.begin(); |
| it != saved_success_items_.end(); ++it) { |
| DCHECK(it->second->has_final_name()); |
| saved_links.push_back(it->second->url()); |
| saved_file_paths.push_back(it->second->file_name()); |
| } |
| |
| // Get the relative directory name. |
| FilePath relative_dir_name = saved_main_directory_path_.BaseName(); |
| |
| tab_contents()->render_view_host()-> |
| GetSerializedHtmlDataForCurrentPageWithLocalLinks( |
| saved_links, saved_file_paths, relative_dir_name); |
| } |
| |
| // Process the serialized HTML content data of a specified web page |
| // retrieved from render process. |
| void SavePackage::OnReceivedSerializedHtmlData(const GURL& frame_url, |
| const std::string& data, |
| int32 status) { |
| WebPageSerializerClient::PageSerializationStatus flag = |
| static_cast<WebPageSerializerClient::PageSerializationStatus>(status); |
| // Check current state. |
| if (wait_state_ != HTML_DATA) |
| return; |
| |
| int id = tab_id(); |
| // If the all frames are finished saving, we need to close the |
| // remaining SaveItems. |
| if (flag == WebPageSerializerClient::AllFramesAreFinished) { |
| for (SaveUrlItemMap::iterator it = in_progress_items_.begin(); |
| it != in_progress_items_.end(); ++it) { |
| VLOG(20) << " " << __FUNCTION__ << "()" |
| << " save_id = " << it->second->save_id() |
| << " url = \"" << it->second->url().spec() << "\""; |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod(file_manager_, |
| &SaveFileManager::SaveFinished, |
| it->second->save_id(), |
| it->second->url(), |
| id, |
| true)); |
| } |
| return; |
| } |
| |
| SaveUrlItemMap::iterator it = in_progress_items_.find(frame_url.spec()); |
| if (it == in_progress_items_.end()) |
| return; |
| SaveItem* save_item = it->second; |
| DCHECK(save_item->save_source() == SaveFileCreateInfo::SAVE_FILE_FROM_DOM); |
| |
| if (!data.empty()) { |
| // Prepare buffer for saving HTML data. |
| scoped_refptr<net::IOBuffer> new_data(new net::IOBuffer(data.size())); |
| memcpy(new_data->data(), data.data(), data.size()); |
| |
| // Call write file functionality in file thread. |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod(file_manager_, |
| &SaveFileManager::UpdateSaveProgress, |
| save_item->save_id(), |
| new_data, |
| static_cast<int>(data.size()))); |
| } |
| |
| // Current frame is completed saving, call finish in file thread. |
| if (flag == WebPageSerializerClient::CurrentFrameIsFinished) { |
| VLOG(20) << " " << __FUNCTION__ << "()" |
| << " save_id = " << save_item->save_id() |
| << " url = \"" << save_item->url().spec() << "\""; |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod(file_manager_, |
| &SaveFileManager::SaveFinished, |
| save_item->save_id(), |
| save_item->url(), |
| id, |
| true)); |
| } |
| } |
| |
| // Ask for all savable resource links from backend, include main frame and |
| // sub-frame. |
| void SavePackage::GetAllSavableResourceLinksForCurrentPage() { |
| if (wait_state_ != START_PROCESS) |
| return; |
| |
| wait_state_ = RESOURCES_LIST; |
| tab_contents()->render_view_host()-> |
| GetAllSavableResourceLinksForCurrentPage(page_url_); |
| } |
| |
| // Give backend the lists which contain all resource links that have local |
| // storage, after which, render process will serialize DOM for generating |
| // HTML data. |
| void SavePackage::OnReceivedSavableResourceLinksForCurrentPage( |
| const std::vector<GURL>& resources_list, |
| const std::vector<GURL>& referrers_list, |
| const std::vector<GURL>& frames_list) { |
| if (wait_state_ != RESOURCES_LIST) |
| return; |
| |
| DCHECK(resources_list.size() == referrers_list.size()); |
| all_save_items_count_ = static_cast<int>(resources_list.size()) + |
| static_cast<int>(frames_list.size()); |
| |
| // We use total bytes as the total number of files we want to save. |
| download_->set_total_bytes(all_save_items_count_); |
| |
| if (all_save_items_count_) { |
| // Put all sub-resources to wait list. |
| for (int i = 0; i < static_cast<int>(resources_list.size()); ++i) { |
| const GURL& u = resources_list[i]; |
| DCHECK(u.is_valid()); |
| SaveFileCreateInfo::SaveFileSource save_source = u.SchemeIsFile() ? |
| SaveFileCreateInfo::SAVE_FILE_FROM_FILE : |
| SaveFileCreateInfo::SAVE_FILE_FROM_NET; |
| SaveItem* save_item = new SaveItem(u, referrers_list[i], |
| this, save_source); |
| waiting_item_queue_.push(save_item); |
| } |
| // Put all HTML resources to wait list. |
| for (int i = 0; i < static_cast<int>(frames_list.size()); ++i) { |
| const GURL& u = frames_list[i]; |
| DCHECK(u.is_valid()); |
| SaveItem* save_item = new SaveItem(u, GURL(), |
| this, SaveFileCreateInfo::SAVE_FILE_FROM_DOM); |
| waiting_item_queue_.push(save_item); |
| } |
| wait_state_ = NET_FILES; |
| DoSavingProcess(); |
| } else { |
| // No resource files need to be saved, treat it as user cancel. |
| Cancel(true); |
| } |
| } |
| |
| void SavePackage::SetShouldPromptUser(bool should_prompt) { |
| g_should_prompt_for_filename = should_prompt; |
| } |
| |
| FilePath SavePackage::GetSuggestedNameForSaveAs( |
| bool can_save_as_complete, |
| const std::string& contents_mime_type) { |
| FilePath name_with_proper_ext = |
| FilePath::FromWStringHack(UTF16ToWideHack(title_)); |
| |
| // If the page's title matches its URL, use the URL. Try to use the last path |
| // component or if there is none, the domain as the file name. |
| // Normally we want to base the filename on the page title, or if it doesn't |
| // exist, on the URL. It's not easy to tell if the page has no title, because |
| // if the page has no title, TabContents::GetTitle() will return the page's |
| // URL (adjusted for display purposes). Therefore, we convert the "title" |
| // back to a URL, and if it matches the original page URL, we know the page |
| // had no title (or had a title equal to its URL, which is fine to treat |
| // similarly). |
| GURL fixed_up_title_url = |
| URLFixerUpper::FixupURL(UTF16ToUTF8(title_), std::string()); |
| |
| if (page_url_ == fixed_up_title_url) { |
| std::string url_path; |
| std::vector<std::string> url_parts; |
| base::SplitString(page_url_.path(), '/', &url_parts); |
| if (!url_parts.empty()) { |
| for (int i = static_cast<int>(url_parts.size()) - 1; i >= 0; --i) { |
| url_path = url_parts[i]; |
| if (!url_path.empty()) |
| break; |
| } |
| } |
| if (url_path.empty()) |
| url_path = page_url_.host(); |
| name_with_proper_ext = FilePath::FromWStringHack(UTF8ToWide(url_path)); |
| } |
| |
| // Ask user for getting final saving name. |
| name_with_proper_ext = EnsureMimeExtension(name_with_proper_ext, |
| contents_mime_type); |
| // Adjust extension for complete types. |
| if (can_save_as_complete) |
| name_with_proper_ext = EnsureHtmlExtension(name_with_proper_ext); |
| |
| FilePath::StringType file_name = name_with_proper_ext.value(); |
| file_util::ReplaceIllegalCharactersInPath(&file_name, ' '); |
| return FilePath(file_name); |
| } |
| |
| FilePath SavePackage::EnsureHtmlExtension(const FilePath& name) { |
| // If the file name doesn't have an extension suitable for HTML files, |
| // append one. |
| FilePath::StringType ext = name.Extension(); |
| if (!ext.empty()) |
| ext.erase(ext.begin()); // Erase preceding '.'. |
| std::string mime_type; |
| if (!net::GetMimeTypeFromExtension(ext, &mime_type) || |
| !CanSaveAsComplete(mime_type)) { |
| return FilePath(name.value() + FILE_PATH_LITERAL(".") + |
| kDefaultHtmlExtension); |
| } |
| return name; |
| } |
| |
| FilePath SavePackage::EnsureMimeExtension(const FilePath& name, |
| const std::string& contents_mime_type) { |
| // Start extension at 1 to skip over period if non-empty. |
| FilePath::StringType ext = name.Extension().length() ? |
| name.Extension().substr(1) : name.Extension(); |
| FilePath::StringType suggested_extension = |
| ExtensionForMimeType(contents_mime_type); |
| std::string mime_type; |
| if (!suggested_extension.empty() && |
| (!net::GetMimeTypeFromExtension(ext, &mime_type) || |
| !IsSavableContents(mime_type))) { |
| // Extension is absent or needs to be updated. |
| return FilePath(name.value() + FILE_PATH_LITERAL(".") + |
| suggested_extension); |
| } |
| return name; |
| } |
| |
| const FilePath::CharType* SavePackage::ExtensionForMimeType( |
| const std::string& contents_mime_type) { |
| static const struct { |
| const FilePath::CharType *mime_type; |
| const FilePath::CharType *suggested_extension; |
| } extensions[] = { |
| { FILE_PATH_LITERAL("text/html"), kDefaultHtmlExtension }, |
| { FILE_PATH_LITERAL("text/xml"), FILE_PATH_LITERAL("xml") }, |
| { FILE_PATH_LITERAL("application/xhtml+xml"), FILE_PATH_LITERAL("xhtml") }, |
| { FILE_PATH_LITERAL("text/plain"), FILE_PATH_LITERAL("txt") }, |
| { FILE_PATH_LITERAL("text/css"), FILE_PATH_LITERAL("css") }, |
| }; |
| #if defined(OS_POSIX) |
| FilePath::StringType mime_type(contents_mime_type); |
| #elif defined(OS_WIN) |
| FilePath::StringType mime_type(UTF8ToWide(contents_mime_type)); |
| #endif // OS_WIN |
| for (uint32 i = 0; i < ARRAYSIZE_UNSAFE(extensions); ++i) { |
| if (mime_type == extensions[i].mime_type) |
| return extensions[i].suggested_extension; |
| } |
| return FILE_PATH_LITERAL(""); |
| } |
| |
| |
| |
| // static. |
| // Check whether the preference has the preferred directory for saving file. If |
| // not, initialize it with default directory. |
| FilePath SavePackage::GetSaveDirPreference(PrefService* prefs) { |
| DCHECK(prefs); |
| |
| if (!prefs->FindPreference(prefs::kSaveFileDefaultDirectory)) { |
| DCHECK(prefs->FindPreference(prefs::kDownloadDefaultDirectory)); |
| FilePath default_save_path = prefs->GetFilePath( |
| prefs::kDownloadDefaultDirectory); |
| prefs->RegisterFilePathPref(prefs::kSaveFileDefaultDirectory, |
| default_save_path); |
| } |
| |
| // Get the directory from preference. |
| FilePath save_file_path = prefs->GetFilePath( |
| prefs::kSaveFileDefaultDirectory); |
| DCHECK(!save_file_path.empty()); |
| |
| return save_file_path; |
| } |
| |
| void SavePackage::GetSaveInfo() { |
| // Can't use tab_contents_ in the file thread, so get the data that we need |
| // before calling to it. |
| PrefService* prefs = tab_contents()->profile()->GetPrefs(); |
| FilePath website_save_dir = GetSaveDirPreference(prefs); |
| FilePath download_save_dir = prefs->GetFilePath( |
| prefs::kDownloadDefaultDirectory); |
| std::string mime_type = tab_contents()->contents_mime_type(); |
| |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod(this, &SavePackage::CreateDirectoryOnFileThread, |
| website_save_dir, download_save_dir, mime_type)); |
| } |
| |
| void SavePackage::CreateDirectoryOnFileThread( |
| const FilePath& website_save_dir, |
| const FilePath& download_save_dir, |
| const std::string& mime_type) { |
| FilePath save_dir; |
| // If the default html/websites save folder doesn't exist... |
| if (!file_util::DirectoryExists(website_save_dir)) { |
| // If the default download dir doesn't exist, create it. |
| if (!file_util::DirectoryExists(download_save_dir)) |
| file_util::CreateDirectory(download_save_dir); |
| save_dir = download_save_dir; |
| } else { |
| // If it does exist, use the default save dir param. |
| save_dir = website_save_dir; |
| } |
| |
| bool can_save_as_complete = CanSaveAsComplete(mime_type); |
| FilePath suggested_filename = GetSuggestedNameForSaveAs(can_save_as_complete, |
| mime_type); |
| FilePath::StringType pure_file_name = |
| suggested_filename.RemoveExtension().BaseName().value(); |
| FilePath::StringType file_name_ext = suggested_filename.Extension(); |
| |
| // Need to make sure the suggested file name is not too long. |
| uint32 max_path = GetMaxPathLengthForDirectory(save_dir); |
| |
| if (GetSafePureFileName(save_dir, file_name_ext, max_path, &pure_file_name)) { |
| save_dir = save_dir.Append(pure_file_name + file_name_ext); |
| } else { |
| // Cannot create a shorter filename. This will cause the save as operation |
| // to fail unless the user pick a shorter name. Continuing even though it |
| // will fail because returning means no save as popup for the user, which |
| // is even more confusing. This case should be rare though. |
| save_dir = save_dir.Append(suggested_filename); |
| } |
| |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| NewRunnableMethod(this, &SavePackage::ContinueGetSaveInfo, save_dir, |
| can_save_as_complete)); |
| } |
| |
| void SavePackage::ContinueGetSaveInfo(const FilePath& suggested_path, |
| bool can_save_as_complete) { |
| // The TabContents which owns this SavePackage may have disappeared during |
| // the UI->FILE->UI thread hop of |
| // GetSaveInfo->CreateDirectoryOnFileThread->ContinueGetSaveInfo. |
| if (!tab_contents()) |
| return; |
| DownloadPrefs* download_prefs = |
| tab_contents()->profile()->GetDownloadManager()->download_prefs(); |
| int file_type_index = |
| SavePackageTypeToIndex( |
| static_cast<SavePackageType>(download_prefs->save_file_type())); |
| |
| SelectFileDialog::FileTypeInfo file_type_info; |
| FilePath::StringType default_extension; |
| |
| // If the contents can not be saved as complete-HTML, do not show the |
| // file filters. |
| if (can_save_as_complete) { |
| bool add_extra_extension = false; |
| FilePath::StringType extra_extension; |
| if (!suggested_path.Extension().empty() && |
| suggested_path.Extension().compare(FILE_PATH_LITERAL("htm")) && |
| suggested_path.Extension().compare(FILE_PATH_LITERAL("html"))) { |
| add_extra_extension = true; |
| extra_extension = suggested_path.Extension().substr(1); |
| } |
| |
| file_type_info.extensions.resize(2); |
| file_type_info.extensions[kSelectFileHtmlOnlyIndex - 1].push_back( |
| FILE_PATH_LITERAL("htm")); |
| file_type_info.extensions[kSelectFileHtmlOnlyIndex - 1].push_back( |
| FILE_PATH_LITERAL("html")); |
| |
| if (add_extra_extension) { |
| file_type_info.extensions[kSelectFileHtmlOnlyIndex - 1].push_back( |
| extra_extension); |
| } |
| |
| file_type_info.extension_description_overrides.push_back( |
| l10n_util::GetStringUTF16(kIndexToIDS[kSelectFileCompleteIndex - 1])); |
| file_type_info.extensions[kSelectFileCompleteIndex - 1].push_back( |
| FILE_PATH_LITERAL("htm")); |
| file_type_info.extensions[kSelectFileCompleteIndex - 1].push_back( |
| FILE_PATH_LITERAL("html")); |
| |
| if (add_extra_extension) { |
| file_type_info.extensions[kSelectFileCompleteIndex - 1].push_back( |
| extra_extension); |
| } |
| |
| file_type_info.extension_description_overrides.push_back( |
| l10n_util::GetStringUTF16(kIndexToIDS[kSelectFileCompleteIndex])); |
| file_type_info.include_all_files = false; |
| default_extension = kDefaultHtmlExtension; |
| } else { |
| file_type_info.extensions.resize(1); |
| file_type_info.extensions[kSelectFileHtmlOnlyIndex - 1].push_back( |
| suggested_path.Extension()); |
| |
| if (!file_type_info.extensions[kSelectFileHtmlOnlyIndex - 1][0].empty()) { |
| // Drop the . |
| file_type_info.extensions[kSelectFileHtmlOnlyIndex - 1][0].erase(0, 1); |
| } |
| |
| file_type_info.include_all_files = true; |
| file_type_index = 1; |
| } |
| |
| if (g_should_prompt_for_filename) { |
| if (!select_file_dialog_.get()) |
| select_file_dialog_ = SelectFileDialog::Create(this); |
| select_file_dialog_->SelectFile(SelectFileDialog::SELECT_SAVEAS_FILE, |
| string16(), |
| suggested_path, |
| &file_type_info, |
| file_type_index, |
| default_extension, |
| tab_contents(), |
| platform_util::GetTopLevel( |
| tab_contents()->GetNativeView()), |
| NULL); |
| } else { |
| // Just use 'suggested_path' instead of opening the dialog prompt. |
| ContinueSave(suggested_path, file_type_index); |
| } |
| } |
| |
| // Called after the save file dialog box returns. |
| void SavePackage::ContinueSave(const FilePath& final_name, |
| int index) { |
| // Ensure the filename is safe. |
| saved_main_file_path_ = final_name; |
| download_util::GenerateSafeFileName(tab_contents()->contents_mime_type(), |
| &saved_main_file_path_); |
| |
| // The option index is not zero-based. |
| DCHECK(index >= kSelectFileHtmlOnlyIndex && |
| index <= kSelectFileCompleteIndex); |
| |
| saved_main_directory_path_ = saved_main_file_path_.DirName(); |
| |
| PrefService* prefs = tab_contents()->profile()->GetPrefs(); |
| StringPrefMember save_file_path; |
| save_file_path.Init(prefs::kSaveFileDefaultDirectory, prefs, NULL); |
| #if defined(OS_POSIX) |
| std::string path_string = saved_main_directory_path_.value(); |
| #elif defined(OS_WIN) |
| std::string path_string = WideToUTF8(saved_main_directory_path_.value()); |
| #endif |
| // If user change the default saving directory, we will remember it just |
| // like IE and FireFox. |
| if (!tab_contents()->profile()->IsOffTheRecord() && |
| save_file_path.GetValue() != path_string) { |
| save_file_path.SetValue(path_string); |
| } |
| |
| save_type_ = kIndexToSaveType[index]; |
| |
| prefs->SetInteger(prefs::kSaveFileType, save_type_); |
| |
| if (save_type_ == SavePackage::SAVE_AS_COMPLETE_HTML) { |
| // Make new directory for saving complete file. |
| saved_main_directory_path_ = saved_main_directory_path_.Append( |
| saved_main_file_path_.RemoveExtension().BaseName().value() + |
| FILE_PATH_LITERAL("_files")); |
| } |
| |
| Init(); |
| } |
| |
| // Static |
| bool SavePackage::IsSavableURL(const GURL& url) { |
| for (int i = 0; chrome::kSavableSchemes[i] != NULL; ++i) { |
| if (url.SchemeIs(chrome::kSavableSchemes[i])) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Static |
| bool SavePackage::IsSavableContents(const std::string& contents_mime_type) { |
| // WebKit creates Document object when MIME type is application/xhtml+xml, |
| // so we also support this MIME type. |
| return contents_mime_type == "text/html" || |
| contents_mime_type == "text/xml" || |
| contents_mime_type == "application/xhtml+xml" || |
| contents_mime_type == "text/plain" || |
| contents_mime_type == "text/css" || |
| net::IsSupportedJavascriptMimeType(contents_mime_type.c_str()); |
| } |
| |
| // SelectFileDialog::Listener interface. |
| void SavePackage::FileSelected(const FilePath& path, |
| int index, void* params) { |
| ContinueSave(path, index); |
| } |
| |
| void SavePackage::FileSelectionCanceled(void* params) { |
| } |