diff options
28 files changed, 1066 insertions, 153 deletions
diff --git a/base/task.h b/base/task.h index 9d4e6e2..c1b25cf 100644 --- a/base/task.h +++ b/base/task.h @@ -373,6 +373,20 @@ inline CancelableTask* NewRunnableMethod(T* object, Method method, MakeTuple(a, b, c, d, e)); } +template <class T, class Method, class A, class B, class C, class D, class E, + class F> +inline CancelableTask* NewRunnableMethod(T* object, Method method, + const A& a, const B& b, + const C& c, const D& d, const E& e, + const F& f) { + return new RunnableMethod<T, + Method, + Tuple6<A, B, C, D, E, F> >(object, + method, + MakeTuple(a, b, c, d, e, + f)); +} + // RunnableFunction and NewRunnableFunction implementation --------------------- template <class Function, class Params> diff --git a/chrome/browser/bookmarks/bookmark_codec.cc b/chrome/browser/bookmarks/bookmark_codec.cc index 1bea43e..70aa410 100644 --- a/chrome/browser/bookmarks/bookmark_codec.cc +++ b/chrome/browser/bookmarks/bookmark_codec.cc @@ -14,21 +14,18 @@ using base::Time; -// Key names. -static const wchar_t* kRootsKey = L"roots"; -static const wchar_t* kRootFolderNameKey = L"bookmark_bar"; -static const wchar_t* kOtherBookmarFolderNameKey = L"other"; -static const wchar_t* kVersionKey = L"version"; -static const wchar_t* kTypeKey = L"type"; -static const wchar_t* kNameKey = L"name"; -static const wchar_t* kDateAddedKey = L"date_added"; -static const wchar_t* kURLKey = L"url"; -static const wchar_t* kDateModifiedKey = L"date_modified"; -static const wchar_t* kChildrenKey = L"children"; - -// Possible values for kTypeKey. -static const wchar_t* kTypeURL = L"url"; -static const wchar_t* kTypeFolder = L"folder"; +const wchar_t* BookmarkCodec::kRootsKey = L"roots"; +const wchar_t* BookmarkCodec::kRootFolderNameKey = L"bookmark_bar"; +const wchar_t* BookmarkCodec::kOtherBookmarFolderNameKey = L"other"; +const wchar_t* BookmarkCodec::kVersionKey = L"version"; +const wchar_t* BookmarkCodec::kTypeKey = L"type"; +const wchar_t* BookmarkCodec::kNameKey = L"name"; +const wchar_t* BookmarkCodec::kDateAddedKey = L"date_added"; +const wchar_t* BookmarkCodec::kURLKey = L"url"; +const wchar_t* BookmarkCodec::kDateModifiedKey = L"date_modified"; +const wchar_t* BookmarkCodec::kChildrenKey = L"children"; +const wchar_t* BookmarkCodec::kTypeURL = L"url"; +const wchar_t* BookmarkCodec::kTypeFolder = L"folder"; // Current version of the file. static const int kCurrentVersion = 1; diff --git a/chrome/browser/bookmarks/bookmark_codec.h b/chrome/browser/bookmarks/bookmark_codec.h index 2865514..4e1fb81 100644 --- a/chrome/browser/bookmarks/bookmark_codec.h +++ b/chrome/browser/bookmarks/bookmark_codec.h @@ -43,6 +43,22 @@ class BookmarkCodec { // nodes. bool Decode(BookmarkModel* model, const Value& value); + // Names of the various keys written to the Value. + static const wchar_t* kRootsKey; + static const wchar_t* kRootFolderNameKey; + static const wchar_t* kOtherBookmarFolderNameKey; + static const wchar_t* kVersionKey; + static const wchar_t* kTypeKey; + static const wchar_t* kNameKey; + static const wchar_t* kDateAddedKey; + static const wchar_t* kURLKey; + static const wchar_t* kDateModifiedKey; + static const wchar_t* kChildrenKey; + + // Possible values for kTypeKey. + static const wchar_t* kTypeURL; + static const wchar_t* kTypeFolder; + private: // Encodes node and all its children into a Value object and returns it. // The caller takes ownership of the returned object. diff --git a/chrome/browser/bookmarks/bookmark_context_menu.cc b/chrome/browser/bookmarks/bookmark_context_menu.cc index e25f4fa..1841f2b 100644 --- a/chrome/browser/bookmarks/bookmark_context_menu.cc +++ b/chrome/browser/bookmarks/bookmark_context_menu.cc @@ -409,6 +409,7 @@ void BookmarkContextMenu::ExecuteCommand(int id) { break; case IDS_BOOKMARK_MANAGER: + UserMetrics::RecordAction(L"ShowBookmarkManager", profile_); BookmarkManagerView::Show(profile_); break; diff --git a/chrome/browser/bookmarks/bookmark_html_writer.cc b/chrome/browser/bookmarks/bookmark_html_writer.cc new file mode 100644 index 0000000..b6c26ef --- /dev/null +++ b/chrome/browser/bookmarks/bookmark_html_writer.cc @@ -0,0 +1,332 @@ +// Copyright (c) 2006-2008 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/bookmarks/bookmark_html_writer.h" + +#include "base/message_loop.h" +#include "base/scoped_handle.h" +#include "base/scoped_ptr.h" +#include "base/string_util.h" +#include "base/time.h" +#include "base/values.h" +#include "chrome/browser/bookmarks/bookmark_codec.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/history/history_types.h" +#include "net/base/escape.h" + +namespace bookmark_html_writer { + +namespace { + +// File header. +const char kHeader[] = + "<!DOCTYPE NETSCAPE-Bookmark-file-1>\r\n" + "<!-- This is an automatically generated file.\r\n" + " It will be read and overwritten.\r\n" + " DO NOT EDIT! -->\r\n" + "<META HTTP-EQUIV=\"Content-Type\"" + " CONTENT=\"text/html; charset=UTF-8\">\r\n" + "<TITLE>Bookmarks</TITLE>\r\n" + "<H1>Bookmarks</H1>\r\n" + "<DL><p>\r\n"; + +// Newline separator. +const char kNewline[] = "\r\n"; + +// The following are used for bookmarks. + +// Start of a bookmark. +const char kBookmarkStart[] = "<DT><A HREF=\""; +// After kBookmarkStart. +const char kAddDate[] = "\" ADD_DATE=\""; +// After kAddDate. +const char kBookmarkAttributeEnd[] = "\">"; +// End of a bookmark. +const char kBookmarkEnd[] = "</A>"; + +// The following are used when writing folders. + +// Start of a folder. +const char kFolderStart[] = "<DT><H3 ADD_DATE=\""; +// After kFolderStart. +const char kLastModified[] = "\" LAST_MODIFIED=\""; +// After kLastModified when writing the bookmark bar. +const char kBookmarkBar[] = "\" PERSONAL_TOOLBAR_FOLDER=\"true\">"; +// After kLastModified when writing a user created folder. +const char kFolderAttributeEnd[] = "\">"; +// End of the folder. +const char kFolderEnd[] = "</H3>"; +// Start of the children of a folder. +const char kFolderChildren[] = "<DL><p>"; +// End of the children for a folder. +const char kFolderChildrenEnd[] = "</DL><p>"; + +// Number of characters to indent by. +const size_t kIndentSize = 4; + +// Class responsible for the actual writing. +class Writer : public Task { + public: + Writer(Value* bookmarks, const std::wstring& path) + : bookmarks_(bookmarks), + path_(path) { + } + + virtual void Run() { + if (!OpenFile()) + return; + + Value* roots; + if (!Write(kHeader) || + bookmarks_->GetType() != Value::TYPE_DICTIONARY || + !static_cast<DictionaryValue*>(bookmarks_.get())->Get( + BookmarkCodec::kRootsKey, &roots) || + roots->GetType() != Value::TYPE_DICTIONARY) { + NOTREACHED(); + return; + } + + DictionaryValue* roots_d_value = static_cast<DictionaryValue*>(roots); + Value* root_folder_value; + Value* other_folder_value; + if (!roots_d_value->Get(BookmarkCodec::kRootFolderNameKey, + &root_folder_value) || + root_folder_value->GetType() != Value::TYPE_DICTIONARY || + !roots_d_value->Get(BookmarkCodec::kOtherBookmarFolderNameKey, + &other_folder_value) || + other_folder_value->GetType() != Value::TYPE_DICTIONARY) { + NOTREACHED(); + return; // Invalid type for root folder and/or other folder. + } + + IncrementIndent(); + + if (!WriteNode(*static_cast<DictionaryValue*>(root_folder_value), + history::StarredEntry::BOOKMARK_BAR) || + !WriteNode(*static_cast<DictionaryValue*>(other_folder_value), + history::StarredEntry::OTHER)) { + return; + } + + DecrementIndent(); + + Write(kFolderChildrenEnd); + Write(kNewline); + } + + private: + // Types of text being written out. The type dictates how the text is + // escaped. + enum TextType { + // The text is the value of an html attribute, eg foo in + // <a href="foo">. + ATTRIBUTE_VALUE, + + // Actual content, eg foo in <h1>foo</h2>. + CONTENT + }; + + // Opens the file, returning true on success. + bool OpenFile() { + handle_.Set( + CreateFile(path_.c_str(), GENERIC_WRITE, 0, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL)); + if (!handle_.IsValid()) + return false; + return true; + } + + // Increments the indent. + void IncrementIndent() { + indent_.resize(indent_.size() + kIndentSize, ' '); + } + + // Decrements the indent. + void DecrementIndent() { + DCHECK(!indent_.empty()); + indent_.resize(indent_.size() - kIndentSize, ' '); + } + + // Writes raw text out returning true on success. This does not escape + // the text in anyway. + bool Write(const std::string& text) { + DWORD wrote; + bool result = + (WriteFile(handle_, text.c_str(), text.length(), &wrote, NULL) && + wrote == text.length()); + DCHECK(result); + return result; + } + + // Writes out the text string (as UTF8). The text is escaped based on + // type. + bool Write(const std::wstring& text, TextType type) { + std::string utf8_string; + + switch (type) { + case ATTRIBUTE_VALUE: + // Convert " to \" + if (text.find(L"\"") != std::wstring::npos) { + std::wstring replaced_string = text; + ReplaceSubstringsAfterOffset(&replaced_string, 0, L"\"", L"\\\""); + utf8_string = WideToUTF8(replaced_string); + } else { + utf8_string = WideToUTF8(text); + } + break; + + case CONTENT: + utf8_string = WideToUTF8(EscapeForHTML(text)); + break; + + default: + NOTREACHED(); + } + + return Write(utf8_string); + } + + // Indents the current line. + bool WriteIndent() { + return Write(indent_); + } + + // Converts a time string written to the JSON codec into a time_t string + // (used by bookmarks.html) and writes it. + bool WriteTime(const std::wstring& time_string) { + base::Time time = + base::Time::FromInternalValue(StringToInt64(time_string)); + return Write(Int64ToString(time.ToTimeT())); + } + + // Writes the node and all its children, returning true on success. + bool WriteNode(const DictionaryValue& value, + history::StarredEntry::Type folder_type) { + std::wstring title, date_added_string, type_string; + if (!value.GetString(BookmarkCodec::kNameKey, &title) || + !value.GetString(BookmarkCodec::kDateAddedKey, &date_added_string) || + !value.GetString(BookmarkCodec::kTypeKey, &type_string) || + (type_string != BookmarkCodec::kTypeURL && + type_string != BookmarkCodec::kTypeFolder)) { + NOTREACHED(); + return false; + } + + if (type_string == BookmarkCodec::kTypeURL) { + std::wstring url_string; + if (!value.GetString(BookmarkCodec::kURLKey, &url_string)) { + NOTREACHED(); + return false; + } + if (!WriteIndent() || + !Write(kBookmarkStart) || + !Write(url_string, ATTRIBUTE_VALUE) || + !Write(kAddDate) || + !WriteTime(date_added_string) || + !Write(kBookmarkAttributeEnd) || + !Write(title, CONTENT) || + !Write(kBookmarkEnd) || + !Write(kNewline)) { + return false; + } + return true; + } + + // Folder. + std::wstring last_modified_date; + Value* child_values; + if (!value.GetString(BookmarkCodec::kDateModifiedKey, + &last_modified_date) || + !value.Get(BookmarkCodec::kChildrenKey, &child_values) || + child_values->GetType() != Value::TYPE_LIST) { + NOTREACHED(); + return false; + } + if (folder_type != history::StarredEntry::OTHER) { + // The other folder name is not written out. This gives the effect of + // making the contents of the 'other folder' be a sibling to the bookmark + // bar folder. + if (!WriteIndent() || + !Write(kFolderStart) || + !WriteTime(date_added_string) || + !Write(kLastModified) || + !WriteTime(last_modified_date)) { + return false; + } + if (folder_type == history::StarredEntry::BOOKMARK_BAR) { + if (!Write(kBookmarkBar)) + return false; + title = L"Bookmark Bar"; + } else if (!Write(kFolderAttributeEnd)) { + return false; + } + if (!Write(title, CONTENT) || + !Write(kFolderEnd) || + !Write(kNewline) || + !WriteIndent() || + !Write(kFolderChildren) || + !Write(kNewline)) { + return false; + } + IncrementIndent(); + } + + // Write the children. + ListValue* children = static_cast<ListValue*>(child_values); + for (size_t i = 0; i < children->GetSize(); ++i) { + Value* child_value; + if (!children->Get(i, &child_value) || + child_value->GetType() != Value::TYPE_DICTIONARY) { + NOTREACHED(); + return false; + } + if (!WriteNode(*static_cast<DictionaryValue*>(child_value), + history::StarredEntry::USER_GROUP)) { + return false; + } + } + if (folder_type != history::StarredEntry::OTHER) { + // Close out the folder. + DecrementIndent(); + if (!WriteIndent() || + !Write(kFolderChildrenEnd) || + !Write(kNewline)) { + return false; + } + } + return true; + } + + // The BookmarkModel as a Value. This value was generated from the + // BookmarkCodec. + scoped_ptr<Value> bookmarks_; + + // Path we're writing to. + std::wstring path_; + + // File we're writing to. + ScopedHandle handle_; + + // How much we indent when writing a bookmark/folder. This is modified + // via IncrementIndent and DecrementIndent. + std::string indent_; +}; + +} // namespace + +void WriteBookmarks(MessageLoop* thread, + BookmarkModel* model, + const std::wstring& path) { + // BookmarkModel isn't thread safe (nor would we want to lock it down + // for the duration of the write), as such we make a copy of the + // BookmarkModel using BookmarkCodec then write from that. + BookmarkCodec codec; + scoped_ptr<Writer> writer(new Writer(codec.Encode(model), path)); + if (thread) + thread->PostTask(FROM_HERE, writer.release()); + else + writer->Run(); +} + +} // namespace bookmark_html_writer diff --git a/chrome/browser/bookmarks/bookmark_html_writer.h b/chrome/browser/bookmarks/bookmark_html_writer.h new file mode 100644 index 0000000..f6b1830 --- /dev/null +++ b/chrome/browser/bookmarks/bookmark_html_writer.h @@ -0,0 +1,27 @@ +// Copyright (c) 2006-2008 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. + +#ifndef CHROME_BROWSER_BOOKMARKS_BOOKMARK_HTML_WRITER_H_ +#define CHROME_BROWSER_BOOKMARKS_BOOKMARK_HTML_WRITER_H_ + +#include <string> + +class BookmarkModel; +class MessageLoop; + +namespace bookmark_html_writer { + +// Writes the bookmarks out in the 'bookmarks.html' format understood by +// Firefox and IE. The results are written to the file at |path|. +// If |thread| is non-null, writing is done on that thread, otherwise +// writing is synchronous. +// +// TODO(sky): need a callback on failure. +void WriteBookmarks(MessageLoop* thread, + BookmarkModel* model, + const std::wstring& path); + +} + +#endif // CHROME_BROWSER_BOOKMARKS_BOOKMARK_HTML_WRITER_H_ diff --git a/chrome/browser/bookmarks/bookmark_html_writer_unittest.cc b/chrome/browser/bookmarks/bookmark_html_writer_unittest.cc new file mode 100644 index 0000000..403ad16 --- /dev/null +++ b/chrome/browser/bookmarks/bookmark_html_writer_unittest.cc @@ -0,0 +1,125 @@ +// Copyright (c) 2006-2008 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 "testing/gtest/include/gtest/gtest.h" + +#include "base/file_util.h" +#include "base/path_service.h" +#include "base/time.h" +#include "chrome/browser/bookmarks/bookmark_html_writer.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/importer/firefox2_importer.h" + +class BookmarkHTMLWriterTest : public testing::Test { + protected: + virtual void SetUp() { + ASSERT_TRUE(PathService::Get(base::DIR_TEMP, &path_)); + file_util::AppendToPath(&path_, L"bookmarks.html"); + file_util::Delete(path_, true); + + } + + virtual void TearDown() { + if (!path_.empty()) + file_util::Delete(path_, true); + } + + void AssertBookmarkEntryEquals(const ProfileWriter::BookmarkEntry& entry, + bool on_toolbar, + const GURL& url, + const std::wstring& title, + base::Time creation_time, + const std::wstring& f1, + const std::wstring& f2, + const std::wstring& f3) { + EXPECT_EQ(on_toolbar, entry.in_toolbar); + EXPECT_TRUE(url == entry.url); + EXPECT_EQ(title, entry.title); + EXPECT_TRUE(creation_time.ToTimeT() == entry.creation_time.ToTimeT()); + size_t path_count = 0; + if (!f3.empty()) + path_count = 3; + else if (!f2.empty()) + path_count = 2; + else if (!f1.empty()) + path_count = 1; + // The first path element should always be 'x', as that is what we passed + // to the importer. + ASSERT_EQ(path_count + 1, entry.path.size()); + EXPECT_EQ(L"x", entry.path[0]); + EXPECT_TRUE(path_count < 1 || entry.path[1] == f1); + EXPECT_TRUE(path_count < 2 || entry.path[2] == f2); + EXPECT_TRUE(path_count < 3 || entry.path[3] == f3); + } + + std::wstring path_; +}; + +// Tests bookmark_html_writer by populating a BookmarkModel, writing it out by +// way of bookmark_html_writer, then using the importer to read it back in. +TEST_F(BookmarkHTMLWriterTest, Test) { + // Populate the BookmarkModel. This creates the following bookmark structure: + // Bookmark Bar + // F1 + // url1 + // F2 + // url2 + // url3 + // Other + // url1 + // url2 + // F3 + // F4 + // url1 + std::wstring f1_title = L"F\"&;<1\""; + std::wstring f2_title = L"F2"; + std::wstring f3_title = L"F 3"; + std::wstring f4_title = L"F4"; + std::wstring url1_title = L"url 1"; + std::wstring url2_title = L"url&2"; + std::wstring url3_title = L"url\"3"; + GURL url1("http://url1"); + GURL url2("http://url2"); + GURL url3("http://url3"); + BookmarkModel model(NULL); + base::Time t1(base::Time::Now()); + base::Time t2(t1 + base::TimeDelta::FromHours(1)); + base::Time t3(t1 + base::TimeDelta::FromHours(1)); + BookmarkNode* f1 = model.AddGroup(model.GetBookmarkBarNode(), 0, f1_title); + model.AddURLWithCreationTime(f1, 0, url1_title, url1, t1); + BookmarkNode* f2 = model.AddGroup(f1, 1, f2_title); + model.AddURLWithCreationTime(f2, 0, url2_title, url2, t2); + model.AddURLWithCreationTime(model.GetBookmarkBarNode(), 1, url3_title, url3, + t3); + + model.AddURLWithCreationTime(model.other_node(), 0, url1_title, url1, t1); + model.AddURLWithCreationTime(model.other_node(), 1, url2_title, url2, t2); + BookmarkNode* f3 = model.AddGroup(model.other_node(), 2, f3_title); + BookmarkNode* f4 = model.AddGroup(f3, 0, f4_title); + model.AddURLWithCreationTime(f4, 0, url1_title, url1, t1); + + // Write to a temp file. + bookmark_html_writer::WriteBookmarks(NULL, &model, path_); + + // Read the bookmarks back in. + std::vector<ProfileWriter::BookmarkEntry> parsed_bookmarks; + Firefox2Importer::ImportBookmarksFile(path_, std::set<GURL>(), false, + L"x", NULL, &parsed_bookmarks, NULL, + NULL); + + // Verify we got back what we wrote. + ASSERT_EQ(6, parsed_bookmarks.size()); + AssertBookmarkEntryEquals(parsed_bookmarks[0], false, url1, url1_title, t1, + L"Bookmark Bar", f1_title, std::wstring()); + AssertBookmarkEntryEquals(parsed_bookmarks[1], false, url2, url2_title, t2, + L"Bookmark Bar", f1_title, f2_title); + AssertBookmarkEntryEquals(parsed_bookmarks[2], false, url3, url3_title, t3, + L"Bookmark Bar", std::wstring(), std::wstring()); + AssertBookmarkEntryEquals(parsed_bookmarks[3], false, url1, url1_title, t1, + std::wstring(), std::wstring(), std::wstring()); + AssertBookmarkEntryEquals(parsed_bookmarks[4], false, url2, url2_title, t2, + std::wstring(), std::wstring(), std::wstring()); + AssertBookmarkEntryEquals(parsed_bookmarks[5], false, url1, url1_title, t1, + f3_title, f4_title, std::wstring()); +} diff --git a/chrome/browser/browser.scons b/chrome/browser/browser.scons index 0b9bb44..42633fa 100644 --- a/chrome/browser/browser.scons +++ b/chrome/browser/browser.scons @@ -119,6 +119,7 @@ if env['PLATFORM'] == 'win32': 'bookmarks/bookmark_drag_data.cc', 'bookmarks/bookmark_drop_info.cc', 'bookmarks/bookmark_folder_tree_model.cc', + 'bookmarks/bookmark_html_writer.cc', 'bookmarks/bookmark_model.cc', 'bookmarks/bookmark_storage.cc', 'bookmarks/bookmark_table_model.cc', diff --git a/chrome/browser/browser.vcproj b/chrome/browser/browser.vcproj index 921f371..f83558f 100644 --- a/chrome/browser/browser.vcproj +++ b/chrome/browser/browser.vcproj @@ -766,6 +766,14 @@ > </File> <File + RelativePath=".\bookmarks\bookmark_html_writer.h" + > + </File> + <File + RelativePath=".\bookmarks\bookmark_html_writer.cc" + > + </File> + <File RelativePath=".\bookmarks\bookmark_model.h" > </File> diff --git a/chrome/browser/importer/firefox2_importer.cc b/chrome/browser/importer/firefox2_importer.cc index e85d347..aef2ba2 100644 --- a/chrome/browser/importer/firefox2_importer.cc +++ b/chrome/browser/importer/firefox2_importer.cc @@ -22,7 +22,7 @@ using base::Time; // Firefox2Importer. -Firefox2Importer::Firefox2Importer() { +Firefox2Importer::Firefox2Importer() : parsing_bookmarks_html_file_(false) { } Firefox2Importer::~Firefox2Importer() { @@ -37,6 +37,8 @@ void Firefox2Importer::StartImport(ProfileInfo profile_info, app_path_ = profile_info.app_path; importer_host_ = host; + parsing_bookmarks_html_file_ = (profile_info.browser_type == BOOKMARKS_HTML); + // The order here is important! NotifyStarted(); if ((items & HOME_PAGE) && !cancelled()) @@ -120,30 +122,29 @@ TemplateURL* Firefox2Importer::CreateTemplateURL(const std::wstring& title, return t_url; } -void Firefox2Importer::ImportBookmarks() { - // Read the whole file. - std::wstring file = source_path_; - file_util::AppendToPath(&file, L"bookmarks.html"); +// static +void Firefox2Importer::ImportBookmarksFile( + const std::wstring& file_path, + const std::set<GURL>& default_urls, + bool first_run, + const std::wstring& first_folder_name, + Importer* importer, + std::vector<ProfileWriter::BookmarkEntry>* bookmarks, + std::vector<TemplateURL*>* template_urls, + std::vector<history::ImportedFavIconUsage>* favicons) { std::string content; - file_util::ReadFileToString(file, &content); + file_util::ReadFileToString(file_path, &content); std::vector<std::string> lines; SplitString(content, '\n', &lines); - - // Load the default bookmarks. - std::set<GURL> default_urls; - LoadDefaultBookmarks(app_path_, &default_urls); - - // Parse the bookmarks.html file. - std::vector<ProfileWriter::BookmarkEntry> bookmarks, toolbar_bookmarks; - std::vector<TemplateURL*> template_urls; - std::vector<history::ImportedFavIconUsage> favicons; - std::wstring last_folder - = l10n_util::GetString(IDS_BOOKMARK_GROUP_FROM_FIREFOX); + + std::vector<ProfileWriter::BookmarkEntry> toolbar_bookmarks; + std::wstring last_folder = first_folder_name; bool last_folder_on_toolbar = false; std::vector<std::wstring> path; size_t toolbar_folder = 0; std::string charset; - for (size_t i = 0; i < lines.size() && !cancelled(); ++i) { + for (size_t i = 0; i < lines.size() && (!importer || !importer->cancelled()); + ++i) { std::string line; TrimString(lines[i], " ", &line); @@ -179,7 +180,7 @@ void Firefox2Importer::ImportBookmarks() { entry.url = url; entry.title = title; - if (first_run() && toolbar_folder) { + if (first_run && toolbar_folder) { // Flatten the items in toolbar. entry.in_toolbar = true; entry.path.assign(path.begin() + toolbar_folder, path.end()); @@ -188,20 +189,23 @@ void Firefox2Importer::ImportBookmarks() { // Insert the item into the "Imported from Firefox" folder after // the first run. entry.path.assign(path.begin(), path.end()); - if (first_run()) + if (first_run) entry.path.erase(entry.path.begin()); - bookmarks.push_back(entry); + bookmarks->push_back(entry); } // Save the favicon. DataURLToFaviconUsage will handle the case where // there is no favicon. - DataURLToFaviconUsage(url, favicon, &favicons); - - // If there is a SHORTCUT attribute for this bookmark, we - // add it as our keywords. - TemplateURL* t_url = CreateTemplateURL(title, shortcut, url); - if (t_url) - template_urls.push_back(t_url); + if (favicons) + DataURLToFaviconUsage(url, favicon, favicons); + + if (template_urls) { + // If there is a SHORTCUT attribute for this bookmark, we + // add it as our keywords. + TemplateURL* t_url = CreateTemplateURL(title, shortcut, url); + if (t_url) + template_urls->push_back(t_url); + } continue; } @@ -221,14 +225,42 @@ void Firefox2Importer::ImportBookmarks() { } } + bookmarks->insert(bookmarks->begin(), toolbar_bookmarks.begin(), + toolbar_bookmarks.end()); +} + +void Firefox2Importer::ImportBookmarks() { + // Load the default bookmarks. + std::set<GURL> default_urls; + if (!parsing_bookmarks_html_file_) + LoadDefaultBookmarks(app_path_, &default_urls); + + // Parse the bookmarks.html file. + std::vector<ProfileWriter::BookmarkEntry> bookmarks, toolbar_bookmarks; + std::vector<TemplateURL*> template_urls; + std::vector<history::ImportedFavIconUsage> favicons; + std::wstring file = source_path_; + if (!parsing_bookmarks_html_file_) + file_util::AppendToPath(&file, L"bookmarks.html"); + std::wstring first_folder_name; + if (parsing_bookmarks_html_file_) + first_folder_name = l10n_util::GetString(IDS_BOOKMARK_GROUP); + else + first_folder_name = l10n_util::GetString(IDS_BOOKMARK_GROUP_FROM_FIREFOX); + + ImportBookmarksFile(file, default_urls, first_run(), + first_folder_name, this, &bookmarks, &template_urls, + &favicons); + // Write data into profile. - bookmarks.insert(bookmarks.begin(), toolbar_bookmarks.begin(), - toolbar_bookmarks.end()); if (!bookmarks.empty() && !cancelled()) { main_loop_->PostTask(FROM_HERE, NewRunnableMethod(writer_, - &ProfileWriter::AddBookmarkEntry, bookmarks, false)); + &ProfileWriter::AddBookmarkEntry, bookmarks, + first_folder_name, + first_run() ? ProfileWriter::FIRST_RUN : 0)); } - if (!template_urls.empty() && !cancelled()) { + if (!parsing_bookmarks_html_file_ && !template_urls.empty() && + !cancelled()) { main_loop_->PostTask(FROM_HERE, NewRunnableMethod(writer_, &ProfileWriter::AddKeywords, template_urls, -1, false)); } else { diff --git a/chrome/browser/importer/firefox2_importer.h b/chrome/browser/importer/firefox2_importer.h index f14438d..eb02cb7 100644 --- a/chrome/browser/importer/firefox2_importer.h +++ b/chrome/browser/importer/firefox2_importer.h @@ -33,6 +33,19 @@ class Firefox2Importer : public Importer { const std::wstring& keyword, const GURL& url); + // Imports the bookmarks from the specified file. |template_urls| and + // |favicons| may be null, in which case TemplateURLs and favicons are + // not parsed. Any bookmarks in |default_urls| are ignored. + static void ImportBookmarksFile( + const std::wstring& file_path, + const std::set<GURL>& default_urls, + bool first_run, + const std::wstring& first_folder_name, + Importer* importer, + std::vector<ProfileWriter::BookmarkEntry>* bookmarks, + std::vector<TemplateURL*>* template_urls, + std::vector<history::ImportedFavIconUsage>* favicons); + private: FRIEND_TEST(FirefoxImporterTest, Firefox2BookmarkParse); FRIEND_TEST(FirefoxImporterTest, Firefox2CookesParse); @@ -105,9 +118,10 @@ class Firefox2Importer : public Importer { ProfileWriter* writer_; std::wstring source_path_; std::wstring app_path_; + // If true, we only parse the bookmarks.html file specified as source_path_. + bool parsing_bookmarks_html_file_; DISALLOW_EVIL_CONSTRUCTORS(Firefox2Importer); }; #endif // CHROME_BROWSER_IMPORTER_FIREFOX2_IMPORTER_H_ - diff --git a/chrome/browser/importer/firefox3_importer.cc b/chrome/browser/importer/firefox3_importer.cc index 965ba15..ea470d5 100644 --- a/chrome/browser/importer/firefox3_importer.cc +++ b/chrome/browser/importer/firefox3_importer.cc @@ -247,7 +247,9 @@ void Firefox3Importer::ImportBookmarks() { // Write into profile. if (!bookmarks.empty() && !cancelled()) { main_loop_->PostTask(FROM_HERE, NewRunnableMethod(writer_, - &ProfileWriter::AddBookmarkEntry, bookmarks, false)); + &ProfileWriter::AddBookmarkEntry, bookmarks, + l10n_util::GetString(IDS_BOOKMARK_GROUP_FROM_FIREFOX), + first_run() ? ProfileWriter::FIRST_RUN : 0)); } if (!template_urls.empty() && !cancelled()) { main_loop_->PostTask(FROM_HERE, NewRunnableMethod(writer_, diff --git a/chrome/browser/importer/ie_importer.cc b/chrome/browser/importer/ie_importer.cc index c952603..2e5d919 100644 --- a/chrome/browser/importer/ie_importer.cc +++ b/chrome/browser/importer/ie_importer.cc @@ -109,7 +109,9 @@ void IEImporter::ImportFavorites() { if (!bookmarks.empty() && !cancelled()) { main_loop_->PostTask(FROM_HERE, NewRunnableMethod(writer_, - &ProfileWriter::AddBookmarkEntry, bookmarks, false)); + &ProfileWriter::AddBookmarkEntry, bookmarks, + l10n_util::GetString(IDS_BOOKMARK_GROUP_FROM_IE), + first_run() ? ProfileWriter::FIRST_RUN : 0)); } } @@ -552,4 +554,3 @@ int IEImporter::CurrentIEVersion() const { } return version; } - diff --git a/chrome/browser/importer/importer.cc b/chrome/browser/importer/importer.cc index 16552e3..3ce8996 100644 --- a/chrome/browser/importer/importer.cc +++ b/chrome/browser/importer/importer.cc @@ -5,6 +5,7 @@ #include "chrome/browser/importer/importer.h" #include <map> +#include <set> #include "base/file_util.h" #include "base/gfx/image_operations.h" @@ -81,10 +82,15 @@ void ProfileWriter::AddHomepage(const GURL& home_page) { void ProfileWriter::AddBookmarkEntry( const std::vector<BookmarkEntry>& bookmark, - bool check_uniqueness) { + const std::wstring& first_folder_name, + int options) { BookmarkModel* model = profile_->GetBookmarkModel(); DCHECK(model->IsLoaded()); + bool first_run = (options & FIRST_RUN) != 0; + std::wstring real_first_folder = first_run ? first_folder_name : + GenerateUniqueFolderName(model, first_folder_name); + bool show_bookmark_toolbar = false; std::set<BookmarkNode*> groups_added_to; for (std::vector<BookmarkEntry>::const_iterator it = bookmark.begin(); @@ -92,49 +98,13 @@ void ProfileWriter::AddBookmarkEntry( // Don't insert this url if it isn't valid. if (!it->url.is_valid()) continue; - + // We suppose that bookmarks are unique by Title, URL, and Folder. Since // checking for uniqueness may not be always the user's intention we have // this as an option. - if (check_uniqueness) { - std::vector<BookmarkModel::TitleMatch> matches; - model->GetBookmarksMatchingText((*it).title, 32, &matches); // 32 enough? - if (!matches.empty()) { - bool found_match = false; - for (std::vector<BookmarkModel::TitleMatch>::iterator match_it = - matches.begin(); - match_it != matches.end() && !found_match; - ++match_it) { - if ((*it).title != (*match_it).node->GetTitle()) - continue; - if ((*it).url != (*match_it).node->GetURL()) - continue; - - // Check the folder path for uniqueness as well - found_match = true; - BookmarkNode* node = (*match_it).node->GetParent(); - for(std::vector<std::wstring>::const_reverse_iterator path_it = - (*it).path.rbegin(); - (path_it != (*it).path.rend()) && found_match; - ++path_it) { - if (NULL == node || (*path_it != node->GetTitle())) - found_match = false; - if (found_match) - node = node->GetParent(); - } - - // We need a post test to differentiate checks such as - // /home/hello and /hello. Note that here the current parent - // should be the "Other bookmarks" node, its parent should be the - // root with title "", and it's parent is finally NULL. - if (NULL == node->GetParent() || - NULL != node->GetParent()->GetParent()) - found_match = false; - } - - if (found_match) - continue; - } + if (options & ADD_IF_UNIQUE && + DoesBookmarkExist(model, *it, real_first_folder, first_run)) { + continue; } // Set up groups in BookmarkModel in such a way that path[i] is @@ -146,17 +116,21 @@ void ProfileWriter::AddBookmarkEntry( for (std::vector<std::wstring>::const_iterator i = it->path.begin(); i != it->path.end(); ++i) { BookmarkNode* child = NULL; + const std::wstring& folder_name = + (!first_run && !it->in_toolbar && (i == it->path.begin())) ? + real_first_folder : *i; + for (int index = 0; index < parent->GetChildCount(); ++index) { BookmarkNode* node = parent->GetChild(index); if ((node->GetType() == history::StarredEntry::BOOKMARK_BAR || node->GetType() == history::StarredEntry::USER_GROUP) && - node->GetTitle() == *i) { + node->GetTitle() == folder_name) { child = node; break; } } if (child == NULL) - child = model->AddGroup(parent, parent->GetChildCount(), *i); + child = model->AddGroup(parent, parent->GetChildCount(), folder_name); parent = child; } groups_added_to.insert(parent); @@ -321,6 +295,79 @@ void ProfileWriter::ShowBookmarkBar() { } } +std::wstring ProfileWriter::GenerateUniqueFolderName( + BookmarkModel* model, + const std::wstring& folder_name) { + // Build a set containing the folder names of the other folder. + std::set<std::wstring> other_folder_names; + BookmarkNode* other = model->other_node(); + for (int i = 0; i < other->GetChildCount(); ++i) { + BookmarkNode* node = other->GetChild(i); + if (node->is_folder()) + other_folder_names.insert(node->GetTitle()); + } + + if (other_folder_names.find(folder_name) == other_folder_names.end()) + return folder_name; // Name is unique, use it. + + // Otherwise iterate until we find a unique name. + for (int i = 1; i < 100; ++i) { + std::wstring name = folder_name + StringPrintf(L" (%d)", i); + if (other_folder_names.find(name) == other_folder_names.end()) + return name; + } + + return folder_name; +} + +bool ProfileWriter::DoesBookmarkExist( + BookmarkModel* model, + const BookmarkEntry& entry, + const std::wstring& first_folder_name, + bool first_run) { + std::vector<BookmarkNode*> nodes_with_same_url; + model->GetNodesByURL(entry.url, &nodes_with_same_url); + if (nodes_with_same_url.empty()) + return false; + + for (size_t i = 0; i < nodes_with_same_url.size(); ++i) { + BookmarkNode* node = nodes_with_same_url[i]; + if (entry.title != node->GetTitle()) + continue; + + // Does the path match? + bool found_match = true; + BookmarkNode* parent = node->GetParent(); + for (std::vector<std::wstring>::const_reverse_iterator path_it = + entry.path.rbegin(); + (path_it != entry.path.rend()) && found_match; ++path_it) { + const std::wstring& folder_name = + (!first_run && path_it + 1 == entry.path.rend()) ? + first_folder_name : *path_it; + if (NULL == parent || *path_it != folder_name) + found_match = false; + else + parent = parent->GetParent(); + } + + // We need a post test to differentiate checks such as + // /home/hello and /hello. The parent should either by the other folder + // node, or the bookmarks bar, depending upon first_run and + // entry.in_toolbar. + if (found_match && + ((first_run && entry.in_toolbar && parent != + model->GetBookmarkBarNode()) || + ((!first_run || !entry.in_toolbar) && + parent != model->other_node()))) { + found_match = false; + } + + if (found_match) + return true; // Found a match with the same url path and title. + } + return false; +} + // Importer. // static @@ -539,6 +586,7 @@ Importer* ImporterHost::CreateImporterByType(ProfileType type) { switch (type) { case MS_IE: return new IEImporter(); + case BOOKMARKS_HTML: case FIREFOX2: return new Firefox2Importer(); case FIREFOX3: diff --git a/chrome/browser/importer/importer.h b/chrome/browser/importer/importer.h index a597879..4e57b64 100644 --- a/chrome/browser/importer/importer.h +++ b/chrome/browser/importer/importer.h @@ -27,7 +27,9 @@ enum ProfileType { MS_IE = 0, FIREFOX2, FIREFOX3, - GOOGLE_TOOLBAR5 + GOOGLE_TOOLBAR5, + // Identifies a 'bookmarks.html' file. + BOOKMARKS_HTML }; // An enumeration of the type of data we want to import. @@ -57,6 +59,18 @@ class Importer; // This object must be invoked on UI thread. class ProfileWriter : public base::RefCounted<ProfileWriter> { public: + // Used to identify how the bookmarks are added. + enum BookmarkOptions { + // Indicates the bookmark should only be added if unique. Uniqueness + // is done by title, url and path. That is, if this is passed to + // AddBookmarkEntry the bookmark is added only if there is no other + // URL with the same url, path and title. + ADD_IF_UNIQUE = 1 << 0, + + // Indicates the bookmarks are being added during first run. + FIRST_RUN = 1 << 1 + }; + explicit ProfileWriter(Profile* profile) : profile_(profile) { } virtual ~ProfileWriter() { } @@ -86,9 +100,21 @@ class ProfileWriter : public base::RefCounted<ProfileWriter> { virtual void AddIE7PasswordInfo(const IE7PasswordInfo& info); virtual void AddHistoryPage(const std::vector<history::URLRow>& page); virtual void AddHomepage(const GURL& homepage); - virtual void AddBookmarkEntry( - const std::vector<BookmarkEntry>& bookmark, - bool check_uniqueness); + // Adds the bookmarks to the BookmarkModel. + // |options| is a bitmask of BookmarkOptions and dictates how and + // which bookmarks are added. If the bitmask contains FIRST_RUN, + // then any entries with a value of true for in_toolbar are added to + // the bookmark bar. If the bitmask does not contain FIRST_RUN then + // the folder name the bookmarks are added to is uniqued based on + // |first_folder_name|. For example, if |first_folder_name| is 'foo' + // and a folder with the name 'foo' already exists in the other + // bookmarks folder, then the folder name 'foo 2' is used. + // If |options| contains ADD_IF_UNIQUE, then the bookmark is added only + // if another bookmarks does not exist with the same title, path and + // url. + virtual void AddBookmarkEntry(const std::vector<BookmarkEntry>& bookmark, + const std::wstring& first_folder_name, + int options); virtual void AddFavicons( const std::vector<history::ImportedFavIconUsage>& favicons); // Add the TemplateURLs in |template_urls| to the local store and make the @@ -111,6 +137,19 @@ class ProfileWriter : public base::RefCounted<ProfileWriter> { Profile* GetProfile() const { return profile_; } private: + // Generates a unique folder name. If folder_name is not unique, then this + // repeatedly tests for '|folder_name| + (i)' until a unique name is found. + std::wstring GenerateUniqueFolderName(BookmarkModel* model, + const std::wstring& folder_name); + + // Returns true if a bookmark exists with the same url, title and path + // as |entry|. |first_folder_name| is the name to use for the first + // path entry if |first_run| is true. + bool DoesBookmarkExist(BookmarkModel* model, + const BookmarkEntry& entry, + const std::wstring& first_folder_name, + bool first_run); + Profile* profile_; DISALLOW_EVIL_CONSTRUCTORS(ProfileWriter); @@ -290,12 +329,15 @@ class Importer : public base::RefCounted<Importer> { void set_first_run(bool first_run) { first_run_ = first_run; } + bool cancelled() const { return cancelled_; } + protected: Importer() : main_loop_(MessageLoop::current()), delagate_loop_(NULL), importer_host_(NULL), - cancelled_(false) {} + cancelled_(false), + first_run_(false) {} // Notifies the coordinator that the collection of data for the specified // item has begun. @@ -329,8 +371,6 @@ class Importer : public base::RefCounted<Importer> { static bool ReencodeFavicon(const unsigned char* src_data, size_t src_len, std::vector<unsigned char>* png_data); - bool cancelled() const { return cancelled_; } - bool first_run() const { return first_run_; } // The importer should know the main thread so that ProfileWriter @@ -359,6 +399,7 @@ class ImportObserver { public: virtual ~ImportObserver() {} // The import operation was canceled by the user. + // TODO (4164): this is never invoked, either rip it out or invoke it. virtual void ImportCanceled() = 0; // The import operation was completed successfully. @@ -380,4 +421,3 @@ void StartImportingWithUI(HWND parent_window, bool first_run); #endif // CHROME_BROWSER_IMPORTER_IMPORTER_H_ - diff --git a/chrome/browser/importer/importer_unittest.cc b/chrome/browser/importer/importer_unittest.cc index f75aa4f..a6094fc 100644 --- a/chrome/browser/importer/importer_unittest.cc +++ b/chrome/browser/importer/importer_unittest.cc @@ -189,9 +189,9 @@ class TestObserver : public ProfileWriter, ++history_count_; } - virtual void AddBookmarkEntry( - const std::vector<BookmarkEntry>& bookmark, - bool check_duplicates) { + virtual void AddBookmarkEntry(const std::vector<BookmarkEntry>& bookmark, + const std::wstring& first_folder_name, + int options) { // Importer should import the IE Favorites folder the same as the list. for (size_t i = 0; i < bookmark.size(); ++i) { if (FindBookmarkEntry(bookmark[i], kIEBookmarks, @@ -545,7 +545,8 @@ class FirefoxObserver : public ProfileWriter, } virtual void AddBookmarkEntry(const std::vector<BookmarkEntry>& bookmark, - bool check_duplicates) { + const std::wstring& first_folder_name, + int options) { for (size_t i = 0; i < bookmark.size(); ++i) { if (FindBookmarkEntry(bookmark[i], kFirefox2Bookmarks, arraysize(kFirefox2Bookmarks))) @@ -745,7 +746,8 @@ class Firefox3Observer : public ProfileWriter, } virtual void AddBookmarkEntry(const std::vector<BookmarkEntry>& bookmark, - bool check_duplicates) { + const std::wstring& first_folder_name, + int options) { for (size_t i = 0; i < bookmark.size(); ++i) { if (FindBookmarkEntry(bookmark[i], kFirefox3Bookmarks, arraysize(kFirefox3Bookmarks))) @@ -829,4 +831,3 @@ TEST_F(ImporterTest, Firefox3Importer) { HISTORY | PASSWORDS | FAVORITES | SEARCH_ENGINES, observer, true)); loop->Run(); } - diff --git a/chrome/browser/importer/toolbar_importer.cc b/chrome/browser/importer/toolbar_importer.cc index 8813807..e96435f 100644 --- a/chrome/browser/importer/toolbar_importer.cc +++ b/chrome/browser/importer/toolbar_importer.cc @@ -568,7 +568,11 @@ bool Toolbar5Importer::ExtractFoldersFromXmlReader( void Toolbar5Importer::AddBookMarksToChrome( const std::vector<ProfileWriter::BookmarkEntry>& bookmarks) { if (!bookmarks.empty() && !cancelled()) { - main_loop_->PostTask(FROM_HERE, NewRunnableMethod(writer_, - &ProfileWriter::AddBookmarkEntry, bookmarks, true)); + int options = ProfileWriter::ADD_IF_UNIQUE | + (first_run() ? ProfileWriter::FIRST_RUN : 0); + main_loop_->PostTask(FROM_HERE, NewRunnableMethod(writer_, + &ProfileWriter::AddBookmarkEntry, bookmarks, + l10n_util::GetString(IDS_BOOKMARK_GROUP_FROM_GOOGLE_TOOLBAR), + options)); } } diff --git a/chrome/browser/shell_dialogs.h b/chrome/browser/shell_dialogs.h index 7e57c7b..74cf66c 100644 --- a/chrome/browser/shell_dialogs.h +++ b/chrome/browser/shell_dialogs.h @@ -73,17 +73,30 @@ class SelectFileDialog // the dialog. This only works for SELECT_SAVEAS_FILE and SELECT_OPEN_FILE. // Can be an empty string to indicate Windows should choose the default to // show. + // |filter| is a null (\0) separated list of alternating filter description + // and filters and terminated with two nulls. // |owning_hwnd| is the window the dialog is modal to, or NULL for a modeless // dialog. // |params| is data from the calling context which will be passed through to // the listener. Can be NULL. // NOTE: only one instance of any shell dialog can be shown per owning_hwnd // at a time (for obvious reasons). + // TODO: convert all callers to this and rip out the old. virtual void SelectFile(Type type, const std::wstring& title, const std::wstring& default_path, + const std::wstring& filter, HWND owning_hwnd, void* params) = 0; + + void SelectFile(Type type, + const std::wstring& title, + const std::wstring& default_path, + HWND owning_hwnd, + void* params) { + SelectFile(type, title, default_path, std::wstring(), + owning_hwnd, params); + } }; // Shows a dialog box for selecting a font. @@ -137,4 +150,3 @@ class SelectFontDialog }; #endif // #ifndef CHROME_BROWSER_SHELL_DIALOGS_H_ - diff --git a/chrome/browser/views/bookmark_manager_view.cc b/chrome/browser/views/bookmark_manager_view.cc index 9650e92..76ac3d7 100644 --- a/chrome/browser/views/bookmark_manager_view.cc +++ b/chrome/browser/views/bookmark_manager_view.cc @@ -9,12 +9,15 @@ #include "base/gfx/skia_utils.h" #include "chrome/app/locales/locale_settings.h" #include "chrome/browser/bookmarks/bookmark_folder_tree_model.h" +#include "chrome/browser/bookmarks/bookmark_html_writer.h" #include "chrome/browser/bookmarks/bookmark_model.h" #include "chrome/browser/bookmarks/bookmark_table_model.h" #include "chrome/browser/bookmarks/bookmark_utils.h" #include "chrome/browser/browser_list.h" #include "chrome/browser/browser_process.h" +#include "chrome/browser/importer/importer.h" #include "chrome/browser/profile.h" +#include "chrome/browser/user_metrics.h" #include "chrome/browser/views/bookmark_editor_view.h" #include "chrome/browser/views/bookmark_folder_tree_view.h" #include "chrome/browser/views/bookmark_table_view.h" @@ -40,6 +43,53 @@ static BookmarkManagerView* manager = NULL; // Delay, in ms, between when the user types and when we run the search. static const int kSearchDelayMS = 200; +static const int kOrganizeMenuButtonID = 1; +static const int kToolsMenuButtonID = 2; + +namespace { + +// Observer installed on the importer. When done importing the newly created +// folder is selected in the bookmark manager. +class ImportObserverImpl : public ImportObserver { + public: + explicit ImportObserverImpl(Profile* profile) : profile_(profile) { + BookmarkModel* model = profile->GetBookmarkModel(); + initial_other_count_ = model->other_node()->GetChildCount(); + } + + virtual void ImportCanceled() { + delete this; + } + + virtual void ImportComplete() { + // We aren't needed anymore. + MessageLoop::current()->DeleteSoon(FROM_HERE, this); + + BookmarkManagerView* manager = BookmarkManagerView::current(); + if (!manager || manager->profile() != profile_) + return; + + BookmarkModel* model = profile_->GetBookmarkModel(); + int other_count = model->other_node()->GetChildCount(); + if (other_count == initial_other_count_ + 1) { + BookmarkNode* imported_node = + model->other_node()->GetChild(initial_other_count_); + manager->SelectInTree(imported_node); + manager->ExpandAll(imported_node); + } + } + + private: + Profile* profile_; + // Number of children in the other bookmarks folder at the time we were + // created. + int initial_other_count_; + + DISALLOW_COPY_AND_ASSIGN(ImportObserverImpl); +}; + +} // namespace + BookmarkManagerView::BookmarkManagerView(Profile* profile) : profile_(profile->GetOriginalProfile()), table_view_(NULL), @@ -59,6 +109,12 @@ BookmarkManagerView::BookmarkManagerView(Profile* profile) views::MenuButton* organize_menu_button = new views::MenuButton( l10n_util::GetString(IDS_BOOKMARK_MANAGER_ORGANIZE_MENU), this, true); + organize_menu_button->SetID(kOrganizeMenuButtonID); + + views::MenuButton* tools_menu_button = new views::MenuButton( + l10n_util::GetString(IDS_BOOKMARK_MANAGER_TOOLS_MENU), + this, true); + tools_menu_button->SetID(kToolsMenuButtonID); split_view_ = new views::SingleSplitView(tree_view_, table_view_); @@ -66,10 +122,13 @@ BookmarkManagerView::BookmarkManagerView(Profile* profile) SetLayoutManager(layout); const int top_id = 1; const int split_cs_id = 2; - layout->SetInsets(kPanelVertMargin, 0, 0, 0); + layout->SetInsets(2, 0, 0, 0); views::ColumnSet* column_set = layout->AddColumnSet(top_id); column_set->AddColumn(views::GridLayout::LEADING, views::GridLayout::CENTER, - 1, views::GridLayout::USE_PREF, 0, 0); + 0, views::GridLayout::USE_PREF, 0, 0); + column_set->AddPaddingColumn(0, kRelatedControlHorizontalSpacing); + column_set->AddColumn(views::GridLayout::LEADING, views::GridLayout::CENTER, + 0, views::GridLayout::USE_PREF, 0, 0); column_set->AddPaddingColumn(1, kUnrelatedControlHorizontalSpacing); column_set->AddColumn(views::GridLayout::TRAILING, views::GridLayout::CENTER, 1, views::GridLayout::USE_PREF, 0, 0); @@ -81,6 +140,7 @@ BookmarkManagerView::BookmarkManagerView(Profile* profile) layout->StartRow(0, top_id); layout->AddView(organize_menu_button); + layout->AddView(tools_menu_button); layout->AddView(search_tf_); layout->AddPaddingRow(0, kRelatedControlVerticalSpacing); @@ -94,6 +154,9 @@ BookmarkManagerView::BookmarkManagerView(Profile* profile) } BookmarkManagerView::~BookmarkManagerView() { + if (select_file_dialog_.get()) + select_file_dialog_->ListenerDestroyed(); + if (!GetBookmarkModel()->IsLoaded()) { GetBookmarkModel()->RemoveObserver(this); } else { @@ -164,6 +227,16 @@ void BookmarkManagerView::SelectInTree(BookmarkNode* node) { } } +void BookmarkManagerView::ExpandAll(BookmarkNode* node) { + BookmarkNode* parent = node->is_url() ? node->GetParent() : node; + FolderNode* folder_node = tree_model_->GetFolderNodeForBookmarkNode(parent); + if (!folder_node) { + NOTREACHED(); + return; + } + tree_view_->ExpandAll(folder_node); +} + BookmarkNode* BookmarkManagerView::GetSelectedFolder() { return tree_view_->GetSelectedBookmarkNode(); } @@ -339,8 +412,62 @@ void BookmarkManagerView::RunMenu(views::View* source, // TODO(glen): when you change the buttons around and what not, futz with // this to make it look good. If you end up keeping padding numbers make them // constants. - ShowMenu(hwnd, pt.x - source->width() + 5, pt.y + 2, - BookmarkContextMenu::BOOKMARK_MANAGER_ORGANIZE_MENU); + if (!GetBookmarkModel()->IsLoaded()) + return; + + if (source->GetID() == kOrganizeMenuButtonID) { + ShowMenu(hwnd, pt.x - source->width() + 5, pt.y + 2, + BookmarkContextMenu::BOOKMARK_MANAGER_ORGANIZE_MENU); + } else if (source->GetID() == kToolsMenuButtonID) { + ShowToolsMenu(hwnd, pt.x - source->width() + 5, pt.y + 2); + } else { + NOTREACHED(); + } +} + +void BookmarkManagerView::ExecuteCommand(int id) { + switch (id) { + case IDS_BOOKMARK_MANAGER_IMPORT_MENU: + UserMetrics::RecordAction(L"BookmarkManager_Import", profile_); + ShowImportBookmarksFileChooser(); + break; + + case IDS_BOOKMARK_MANAGER_EXPORT_MENU: + UserMetrics::RecordAction(L"BookmarkManager_Export", profile_); + ShowExportBookmarksFileChooser(); + break; + + default: + NOTREACHED(); + break; + } +} + +void BookmarkManagerView::FileSelected(const std::wstring& path, + void* params) { + int id = reinterpret_cast<int>(params); + if (id == IDS_BOOKMARK_MANAGER_IMPORT_MENU) { + // ImporterHost is ref counted and will delete itself when done. + ImporterHost* host = new ImporterHost(); + ProfileInfo profile_info; + profile_info.browser_type = BOOKMARKS_HTML; + profile_info.source_path = path; + StartImportingWithUI(GetContainer()->GetHWND(), FAVORITES, host, + profile_info, profile_, + new ImportObserverImpl(profile()), false); + } else if (id == IDS_BOOKMARK_MANAGER_EXPORT_MENU) { + if (g_browser_process->io_thread()) { + bookmark_html_writer::WriteBookmarks( + g_browser_process->io_thread()->message_loop(), GetBookmarkModel(), + path); + } + } else { + NOTREACHED(); + } +} + +void BookmarkManagerView::FileSelectionCanceled(void* params) { + select_file_dialog_ = NULL; } BookmarkTableModel* BookmarkManagerView::CreateSearchTableModel() { @@ -456,3 +583,40 @@ void BookmarkManagerView::ShowMenu( menu.RunMenuAt(x, y); } } + +void BookmarkManagerView::ShowToolsMenu(HWND host, int x, int y) { + views::MenuItemView menu(this); + menu.AppendMenuItemWithLabel( + IDS_BOOKMARK_MANAGER_IMPORT_MENU, + l10n_util::GetString(IDS_BOOKMARK_MANAGER_IMPORT_MENU)); + menu.AppendMenuItemWithLabel( + IDS_BOOKMARK_MANAGER_EXPORT_MENU, + l10n_util::GetString(IDS_BOOKMARK_MANAGER_EXPORT_MENU)); + menu.RunMenuAt(GetContainer()->GetHWND(), gfx::Rect(x, y, 0, 0), + views::MenuItemView::TOPLEFT, true); +} + +void BookmarkManagerView::ShowImportBookmarksFileChooser() { + if (select_file_dialog_.get()) + select_file_dialog_->ListenerDestroyed(); + + // TODO(sky): need a textual description here once we can add new + // strings. + std::wstring filter_string(L"*.html\0*.html\0\0"); + select_file_dialog_ = SelectFileDialog::Create(this); + select_file_dialog_->SelectFile( + SelectFileDialog::SELECT_OPEN_FILE, std::wstring(), std::wstring(), + filter_string, GetContainer()->GetHWND(), + reinterpret_cast<void*>(IDS_BOOKMARK_MANAGER_IMPORT_MENU)); +} + +void BookmarkManagerView::ShowExportBookmarksFileChooser() { + if (select_file_dialog_.get()) + select_file_dialog_->ListenerDestroyed(); + + select_file_dialog_ = SelectFileDialog::Create(this); + select_file_dialog_->SelectFile( + SelectFileDialog::SELECT_SAVEAS_FILE, std::wstring(), std::wstring(), + std::wstring(), GetContainer()->GetHWND(), + reinterpret_cast<void*>(IDS_BOOKMARK_MANAGER_EXPORT_MENU)); +} diff --git a/chrome/browser/views/bookmark_manager_view.h b/chrome/browser/views/bookmark_manager_view.h index 8a28eac..0aa83dd 100644 --- a/chrome/browser/views/bookmark_manager_view.h +++ b/chrome/browser/views/bookmark_manager_view.h @@ -5,9 +5,11 @@ #ifndef CHROME_BROWSER_VIEWS_BOOKMARK_MANAGER_VIEW_H_ #define CHROME_BROWSER_VIEWS_BOOKMARK_MANAGER_VIEW_H_ +#include "base/ref_counted.h" #include "base/task.h" #include "chrome/browser/bookmarks/bookmark_context_menu.h" #include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/shell_dialogs.h" #include "chrome/views/table_view.h" #include "chrome/views/text_field.h" #include "chrome/views/tree_view.h" @@ -39,7 +41,9 @@ class BookmarkManagerView : public views::View, public views::TextField::Controller, public BookmarkModelObserver, public views::ContextMenuController, - public views::ViewMenuDelegate { + public views::ViewMenuDelegate, + public views::MenuDelegate, + public SelectFileDialog::Listener { public: explicit BookmarkManagerView(Profile* profile); virtual ~BookmarkManagerView(); @@ -56,6 +60,9 @@ class BookmarkManagerView : public views::View, // selected and node is selected in the table. void SelectInTree(BookmarkNode* node); + // Expands all the children of the selected folder. + void ExpandAll(BookmarkNode* node); + // Returns the selected folder, which may be null. BookmarkNode* GetSelectedFolder(); @@ -137,6 +144,13 @@ class BookmarkManagerView : public views::View, // ViewMenuDelegate. virtual void RunMenu(views::View* source, const CPoint& pt, HWND hwnd); + // MenuDelegate. + virtual void ExecuteCommand(int id); + + // SelectFileDialog::Listener. + virtual void FileSelected(const std::wstring& path, void* params); + virtual void FileSelectionCanceled(void* params); + // Creates the table model to use when searching. This returns NULL if there // is no search text. BookmarkTableModel* CreateSearchTableModel(); @@ -167,6 +181,14 @@ class BookmarkManagerView : public views::View, int y, BookmarkContextMenu::ConfigurationType config); + // Shows the tools menu. + void ShowToolsMenu(HWND host, int x, int y); + + // Shows the import/export file chooser. These invoke + // FileSelected/FileSelectionCanceled when done. + void ShowImportBookmarksFileChooser(); + void ShowExportBookmarksFileChooser(); + Profile* profile_; BookmarkTableView* table_view_; BookmarkFolderTreeView* tree_view_; @@ -175,6 +197,9 @@ class BookmarkManagerView : public views::View, views::TextField* search_tf_; views::SingleSplitView* split_view_; + // Import/export file dialog. + scoped_refptr<SelectFileDialog> select_file_dialog_; + // Factory used for delaying search. ScopedRunnableMethodFactory<BookmarkManagerView> search_factory_; diff --git a/chrome/browser/views/importing_progress_view.cc b/chrome/browser/views/importing_progress_view.cc index 3d759ef..783a83c 100644 --- a/chrome/browser/views/importing_progress_view.cc +++ b/chrome/browser/views/importing_progress_view.cc @@ -22,14 +22,13 @@ ImportingProgressView::ImportingProgressView(const std::wstring& source_name, int16 items, ImporterHost* coordinator, ImportObserver* observer, - HWND parent_window) + HWND parent_window, + bool bookmarks_import) : state_bookmarks_(new views::CheckmarkThrobber), state_searches_(new views::CheckmarkThrobber), state_passwords_(new views::CheckmarkThrobber), state_history_(new views::CheckmarkThrobber), state_cookies_(new views::CheckmarkThrobber), - label_info_(new views::Label(l10n_util::GetStringF( - IDS_IMPORT_PROGRESS_INFO, source_name))), label_bookmarks_(new views::Label( l10n_util::GetString(IDS_IMPORT_PROGRESS_STATUS_BOOKMARKS))), label_searches_(new views::Label( @@ -44,7 +43,12 @@ ImportingProgressView::ImportingProgressView(const std::wstring& source_name, coordinator_(coordinator), import_observer_(observer), items_(items), - importing_(true) { + importing_(true), + bookmarks_import_(bookmarks_import) { + std::wstring info_text = bookmarks_import ? + l10n_util::GetString(IDS_IMPORT_BOOKMARKS) : + l10n_util::GetStringF(IDS_IMPORT_PROGRESS_INFO, source_name); + label_info_ = new views::Label(info_text); coordinator_->SetObserver(this); label_info_->SetMultiLine(true); label_info_->SetHorizontalAlignment(views::Label::ALIGN_LEFT); @@ -78,6 +82,17 @@ ImportingProgressView::~ImportingProgressView() { RemoveChildView(label_passwords_.get()); RemoveChildView(label_history_.get()); RemoveChildView(label_cookies_.get()); + + if (importing_) { + // We're being deleted while importing, clean up state so that the importer + // doesn't have a reference to us and cancel the import. We can get here + // if our parent window is closed, which closes our window and deletes us. + importing_ = false; + coordinator_->SetObserver(NULL); + coordinator_->Cancel(); + if (import_observer_) + import_observer_->ImportComplete(); + } } //////////////////////////////////////////////////////////////////////////////// @@ -217,6 +232,11 @@ void ImportingProgressView::InitControlLayout() { const int single_column_view_set_id = 0; ColumnSet* column_set = layout->AddColumnSet(single_column_view_set_id); + if (bookmarks_import_) { + column_set->AddColumn(GridLayout::CENTER, GridLayout::CENTER, 0, + GridLayout::FIXED, ps.width(), 0); + column_set->AddPaddingColumn(0, kRelatedControlHorizontalSpacing); + } column_set->AddColumn(GridLayout::FILL, GridLayout::FILL, 1, GridLayout::USE_PREF, 0, 0); const int double_column_view_set_id = 1; @@ -230,10 +250,12 @@ void ImportingProgressView::InitControlLayout() { column_set->AddPaddingColumn(0, kUnrelatedControlLargeHorizontalSpacing); layout->StartRow(0, single_column_view_set_id); + if (bookmarks_import_) + layout->AddView(state_bookmarks_.get()); layout->AddView(label_info_); layout->AddPaddingRow(0, kUnrelatedControlVerticalSpacing); - if (items_ & FAVORITES) { + if (items_ & FAVORITES && !bookmarks_import_) { layout->StartRow(0, double_column_view_set_id); layout->AddView(state_bookmarks_.get()); layout->AddView(label_bookmarks_.get()); @@ -277,7 +299,8 @@ void StartImportingWithUI(HWND parent_window, bool first_run) { DCHECK(items != 0); ImportingProgressView* v = new ImportingProgressView( - source_profile.description, items, coordinator, observer, parent_window); + source_profile.description, items, coordinator, observer, parent_window, + source_profile.browser_type == BOOKMARKS_HTML); views::Window* window = views::Window::CreateChromeWindow(parent_window, gfx::Rect(), v); diff --git a/chrome/browser/views/importing_progress_view.h b/chrome/browser/views/importing_progress_view.h index c7f97c0..61ec12f 100644 --- a/chrome/browser/views/importing_progress_view.h +++ b/chrome/browser/views/importing_progress_view.h @@ -19,11 +19,15 @@ class ImportingProgressView : public views::View, public views::DialogDelegate, public ImporterHost::Observer { public: + // |items| is a bitmask of ImportItems being imported. + // |bookmark_import| is true if we're importing bookmarks from a + // bookmarks.html file. ImportingProgressView(const std::wstring& source_name, int16 items, ImporterHost* coordinator, ImportObserver* observer, - HWND parent_window); + HWND parent_window, + bool bookmarks_import); virtual ~ImportingProgressView(); protected: @@ -79,6 +83,9 @@ class ImportingProgressView : public views::View, // True if the import operation is in progress. bool importing_; + // Are we importing a bookmarks.html file? + bool bookmarks_import_; + DISALLOW_EVIL_CONSTRUCTORS(ImportingProgressView); }; diff --git a/chrome/browser/views/shell_dialogs.cc b/chrome/browser/views/shell_dialogs.cc index b2dbf1c..feadbed 100644 --- a/chrome/browser/views/shell_dialogs.cc +++ b/chrome/browser/views/shell_dialogs.cc @@ -192,7 +192,9 @@ class SelectFileDialogImpl : public SelectFileDialog, // SelectFileDialog implementation: virtual void SelectFile(Type type, const std::wstring& title, - const std::wstring& default_path, HWND owning_hwnd, + const std::wstring& default_path, + const std::wstring& filter, + HWND owning_hwnd, void* params); virtual bool IsRunning(HWND owning_hwnd) const; virtual void ListenerDestroyed(); @@ -203,6 +205,7 @@ class SelectFileDialogImpl : public SelectFileDialog, void ExecuteSelectFile(Type type, const std::wstring& title, const std::wstring& default_path, + const std::wstring& filter, RunState run_state, void* params); @@ -225,6 +228,7 @@ class SelectFileDialogImpl : public SelectFileDialog, // Runs an Open file dialog box, with similar semantics for input paramaters // as RunSelectFolderDialog. bool RunOpenFileDialog(const std::wstring& title, + const std::wstring& filters, HWND owner, std::wstring* path); @@ -245,12 +249,13 @@ SelectFileDialogImpl::~SelectFileDialogImpl() { void SelectFileDialogImpl::SelectFile(Type type, const std::wstring& title, const std::wstring& default_path, + const std::wstring& filter, HWND owner, void* params) { RunState run_state = BeginRun(owner); run_state.dialog_thread->message_loop()->PostTask(FROM_HERE, NewRunnableMethod(this, &SelectFileDialogImpl::ExecuteSelectFile, type, - title, default_path, run_state, params)); + title, default_path, filter, run_state, params)); } bool SelectFileDialogImpl::IsRunning(HWND owning_hwnd) const { @@ -263,11 +268,13 @@ void SelectFileDialogImpl::ListenerDestroyed() { listener_ = NULL; } -void SelectFileDialogImpl::ExecuteSelectFile(Type type, - const std::wstring& title, - const std::wstring& default_path, - RunState run_state, - void* params) { +void SelectFileDialogImpl::ExecuteSelectFile( + Type type, + const std::wstring& title, + const std::wstring& default_path, + const std::wstring& filter, + RunState run_state, + void* params) { std::wstring path = default_path; bool success = false; if (type == SELECT_FOLDER) { @@ -276,7 +283,7 @@ void SelectFileDialogImpl::ExecuteSelectFile(Type type, success = win_util::SaveFileAs(run_state.owner, default_path, &path); DisableOwner(run_state.owner); } else if (type == SELECT_OPEN_FILE) { - success = RunOpenFileDialog(title, run_state.owner, &path); + success = RunOpenFileDialog(title, filter, run_state.owner, &path); } if (success) { @@ -331,9 +338,11 @@ bool SelectFileDialogImpl::RunSelectFolderDialog(const std::wstring& title, return false; } -bool SelectFileDialogImpl::RunOpenFileDialog(const std::wstring& title, - HWND owner, - std::wstring* path) { +bool SelectFileDialogImpl::RunOpenFileDialog( + const std::wstring& title, + const std::wstring& filter, + HWND owner, + std::wstring* path) { OPENFILENAME ofn; // We must do this otherwise the ofn's FlagsEx may be initialized to random // junk in release builds which can cause the Places Bar not to show up! @@ -350,8 +359,9 @@ bool SelectFileDialogImpl::RunOpenFileDialog(const std::wstring& title, // without having to close Chrome first. ofn.Flags = OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; - // TODO(beng): (http://b/issue?id=1126563) edit the filter options in the - // dropdown list. + if (!filter.empty()) { + ofn.lpstrFilter = filter.c_str(); + } bool success = !!GetOpenFileName(&ofn); DisableOwner(owner); if (success) @@ -527,4 +537,3 @@ void SelectFontDialogImpl::FontNotSelected(void* params, RunState run_state) { SelectFontDialog* SelectFontDialog::Create(Listener* listener) { return new SelectFontDialogImpl(listener); } - diff --git a/chrome/test/unit/unit_tests.scons b/chrome/test/unit/unit_tests.scons index df137a2..3129c16 100644 --- a/chrome/test/unit/unit_tests.scons +++ b/chrome/test/unit/unit_tests.scons @@ -146,6 +146,7 @@ if env['PLATFORM'] == 'win32': '$CHROME_DIR/browser/bookmarks/bookmark_context_menu_test.cc', '$CHROME_DIR/browser/bookmarks/bookmark_drag_data_unittest.cc', '$CHROME_DIR/browser/bookmarks/bookmark_folder_tree_model_unittest.cc', + '$CHROME_DIR/browser/bookmarks/bookmark_html_writer_unittest.cc', '$CHROME_DIR/browser/bookmarks/bookmark_model_unittest.cc', '$CHROME_DIR/browser/bookmarks/bookmark_table_model_unittest.cc', '$CHROME_DIR/browser/cache_manager_host_unittest.cc', diff --git a/chrome/test/unit/unittests.vcproj b/chrome/test/unit/unittests.vcproj index 850fc8a..d39295b 100644 --- a/chrome/test/unit/unittests.vcproj +++ b/chrome/test/unit/unittests.vcproj @@ -231,6 +231,14 @@ </File> </Filter> <Filter + Name="TestBookmarkHTMLWriter" + > + <File + RelativePath="..\..\browser\bookmarks\bookmark_html_writer_unittest.cc" + > + </File> + </Filter> + <Filter Name="TestGoogleURLTracker" > <File diff --git a/chrome/views/tree_view.cc b/chrome/views/tree_view.cc index 8987042..913b845 100644 --- a/chrome/views/tree_view.cc +++ b/chrome/views/tree_view.cc @@ -157,6 +157,18 @@ void TreeView::ExpandAll() { ExpandAll(model_->GetRoot()); } +void TreeView::ExpandAll(TreeModelNode* node) { + DCHECK(node); + // Expand the node. + if (node != model_->GetRoot() || root_shown_) + TreeView_Expand(tree_view_, GetNodeDetails(node)->tree_item, TVE_EXPAND); + // And recursively expand all the children. + for (int i = model_->GetChildCount(node) - 1; i >= 0; --i) { + TreeModelNode* child = model_->GetChild(node, i); + ExpandAll(child); + } +} + bool TreeView::IsExpanded(TreeModelNode* node) { TreeModelNode* parent = model_->GetParent(node); if (!parent) @@ -470,18 +482,6 @@ HTREEITEM TreeView::GetTreeItemForNode(TreeModelNode* node) { return details ? details->tree_item : NULL; } -void TreeView::ExpandAll(TreeModelNode* node) { - DCHECK(node); - // Expand the node. - if (node != model_->GetRoot() || root_shown_) - TreeView_Expand(tree_view_, GetNodeDetails(node)->tree_item, TVE_EXPAND); - // And recursively expand all the children. - for (int i = model_->GetChildCount(node) - 1; i >= 0; --i) { - TreeModelNode* child = model_->GetChild(node, i); - ExpandAll(child); - } -} - void TreeView::DeleteRootItems() { HTREEITEM root = TreeView_GetRoot(tree_view_); if (root) { diff --git a/chrome/views/tree_view.h b/chrome/views/tree_view.h index fb3f50e..d0d083e 100644 --- a/chrome/views/tree_view.h +++ b/chrome/views/tree_view.h @@ -147,6 +147,10 @@ class TreeView : public NativeControl, TreeModelObserver { // Convenience to expand ALL nodes in the tree. void ExpandAll(); + // Invoked from ExpandAll(). Expands the supplied node and recursively + // invokes itself with all children. + void ExpandAll(TreeModelNode* node); + // Returns true if the specified node is expanded. bool IsExpanded(TreeModelNode* node); @@ -253,10 +257,6 @@ class TreeView : public NativeControl, TreeModelObserver { bool loaded_children; }; - // Invoked from ExpandAll(). Expands the supplied node and recursively - // invokes itself with all children. - void ExpandAll(TreeModelNode* node); - // Deletes the root items from the treeview. This is used when the model // changes. void DeleteRootItems(); diff --git a/net/base/escape.h b/net/base/escape.h index 90614f5..14f46c6 100644 --- a/net/base/escape.h +++ b/net/base/escape.h @@ -30,6 +30,7 @@ void AppendEscapedCharForHTML(char c, std::string* output); // Escape chars that might cause this text to be interpretted as HTML tags. std::string EscapeForHTML(const std::string& text); +std::wstring EscapeForHTML(const std::wstring& text); // Unescaping ------------------------------------------------------------------ |