1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/bookmarks/bookmark_html_writer.h"
6
7 #include "base/base64.h"
8 #include "base/callback.h"
9 #include "base/file_path.h"
10 #include "base/memory/scoped_ptr.h"
11 #include "base/message_loop.h"
12 #include "base/platform_file.h"
13 #include "base/string_number_conversions.h"
14 #include "base/time.h"
15 #include "base/values.h"
16 #include "chrome/browser/bookmarks/bookmark_codec.h"
17 #include "chrome/browser/bookmarks/bookmark_model.h"
18 #include "chrome/browser/history/history_types.h"
19 #include "chrome/browser/profiles/profile.h"
20 #include "content/browser/browser_thread.h"
21 #include "content/common/notification_source.h"
22 #include "grit/generated_resources.h"
23 #include "net/base/escape.h"
24 #include "net/base/file_stream.h"
25 #include "net/base/net_errors.h"
26 #include "ui/base/l10n/l10n_util.h"
27
28 namespace {
29
30 static BookmarkFaviconFetcher* fetcher = NULL;
31
32 // File header.
33 const char kHeader[] =
34 "<!DOCTYPE NETSCAPE-Bookmark-file-1>\r\n"
35 "<!-- This is an automatically generated file.\r\n"
36 " It will be read and overwritten.\r\n"
37 " DO NOT EDIT! -->\r\n"
38 "<META HTTP-EQUIV=\"Content-Type\""
39 " CONTENT=\"text/html; charset=UTF-8\">\r\n"
40 "<TITLE>Bookmarks</TITLE>\r\n"
41 "<H1>Bookmarks</H1>\r\n"
42 "<DL><p>\r\n";
43
44 // Newline separator.
45 const char kNewline[] = "\r\n";
46
47 // The following are used for bookmarks.
48
49 // Start of a bookmark.
50 const char kBookmarkStart[] = "<DT><A HREF=\"";
51 // After kBookmarkStart.
52 const char kAddDate[] = "\" ADD_DATE=\"";
53 // After kAddDate.
54 const char kIcon[] = "\" ICON=\"";
55 // After kIcon.
56 const char kBookmarkAttributeEnd[] = "\">";
57 // End of a bookmark.
58 const char kBookmarkEnd[] = "</A>";
59
60 // The following are used when writing folders.
61
62 // Start of a folder.
63 const char kFolderStart[] = "<DT><H3 ADD_DATE=\"";
64 // After kFolderStart.
65 const char kLastModified[] = "\" LAST_MODIFIED=\"";
66 // After kLastModified when writing the bookmark bar.
67 const char kBookmarkBar[] = "\" PERSONAL_TOOLBAR_FOLDER=\"true\">";
68 // After kLastModified when writing a user created folder.
69 const char kFolderAttributeEnd[] = "\">";
70 // End of the folder.
71 const char kFolderEnd[] = "</H3>";
72 // Start of the children of a folder.
73 const char kFolderChildren[] = "<DL><p>";
74 // End of the children for a folder.
75 const char kFolderChildrenEnd[] = "</DL><p>";
76
77 // Number of characters to indent by.
78 const size_t kIndentSize = 4;
79
80 // Class responsible for the actual writing. Takes ownership of favicons_map.
81 class Writer : public Task {
82 public:
Writer(Value * bookmarks,const FilePath & path,BookmarkFaviconFetcher::URLFaviconMap * favicons_map,BookmarksExportObserver * observer)83 Writer(Value* bookmarks,
84 const FilePath& path,
85 BookmarkFaviconFetcher::URLFaviconMap* favicons_map,
86 BookmarksExportObserver* observer)
87 : bookmarks_(bookmarks),
88 path_(path),
89 favicons_map_(favicons_map),
90 observer_(observer) {
91 }
92
Run()93 virtual void Run() {
94 RunImpl();
95 NotifyOnFinish();
96 }
97
98 // Writing bookmarks and favicons data to file.
RunImpl()99 virtual void RunImpl() {
100 if (!OpenFile()) {
101 return;
102 }
103
104 Value* roots;
105 if (!Write(kHeader) ||
106 bookmarks_->GetType() != Value::TYPE_DICTIONARY ||
107 !static_cast<DictionaryValue*>(bookmarks_.get())->Get(
108 BookmarkCodec::kRootsKey, &roots) ||
109 roots->GetType() != Value::TYPE_DICTIONARY) {
110 NOTREACHED();
111 return;
112 }
113
114 DictionaryValue* roots_d_value = static_cast<DictionaryValue*>(roots);
115 Value* root_folder_value;
116 Value* other_folder_value;
117 if (!roots_d_value->Get(BookmarkCodec::kRootFolderNameKey,
118 &root_folder_value) ||
119 root_folder_value->GetType() != Value::TYPE_DICTIONARY ||
120 !roots_d_value->Get(BookmarkCodec::kOtherBookmarkFolderNameKey,
121 &other_folder_value) ||
122 other_folder_value->GetType() != Value::TYPE_DICTIONARY) {
123 NOTREACHED();
124 return; // Invalid type for root folder and/or other folder.
125 }
126
127 IncrementIndent();
128
129 if (!WriteNode(*static_cast<DictionaryValue*>(root_folder_value),
130 BookmarkNode::BOOKMARK_BAR) ||
131 !WriteNode(*static_cast<DictionaryValue*>(other_folder_value),
132 BookmarkNode::OTHER_NODE)) {
133 return;
134 }
135
136 DecrementIndent();
137
138 Write(kFolderChildrenEnd);
139 Write(kNewline);
140 // File stream close is forced so that unit test could read it.
141 file_stream_.Close();
142 }
143
144 private:
145 // Types of text being written out. The type dictates how the text is
146 // escaped.
147 enum TextType {
148 // The text is the value of an html attribute, eg foo in
149 // <a href="foo">.
150 ATTRIBUTE_VALUE,
151
152 // Actual content, eg foo in <h1>foo</h2>.
153 CONTENT
154 };
155
156 // Opens the file, returning true on success.
OpenFile()157 bool OpenFile() {
158 int flags = base::PLATFORM_FILE_CREATE_ALWAYS | base::PLATFORM_FILE_WRITE;
159 return (file_stream_.Open(path_, flags) == net::OK);
160 }
161
162 // Increments the indent.
IncrementIndent()163 void IncrementIndent() {
164 indent_.resize(indent_.size() + kIndentSize, ' ');
165 }
166
167 // Decrements the indent.
DecrementIndent()168 void DecrementIndent() {
169 DCHECK(!indent_.empty());
170 indent_.resize(indent_.size() - kIndentSize, ' ');
171 }
172
173 // Called at the end of the export process.
NotifyOnFinish()174 void NotifyOnFinish() {
175 if (observer_ != NULL) {
176 observer_->OnExportFinished();
177 }
178 }
179
180 // Writes raw text out returning true on success. This does not escape
181 // the text in anyway.
Write(const std::string & text)182 bool Write(const std::string& text) {
183 size_t wrote = file_stream_.Write(text.c_str(), text.length(), NULL);
184 bool result = (wrote == text.length());
185 DCHECK(result);
186 return result;
187 }
188
189 // Writes out the text string (as UTF8). The text is escaped based on
190 // type.
Write(const std::string & text,TextType type)191 bool Write(const std::string& text, TextType type) {
192 DCHECK(IsStringUTF8(text));
193 std::string utf8_string;
194
195 switch (type) {
196 case ATTRIBUTE_VALUE:
197 // Convert " to "
198 utf8_string = text;
199 ReplaceSubstringsAfterOffset(&utf8_string, 0, "\"", """);
200 break;
201
202 case CONTENT:
203 utf8_string = EscapeForHTML(text);
204 break;
205
206 default:
207 NOTREACHED();
208 }
209
210 return Write(utf8_string);
211 }
212
213 // Indents the current line.
WriteIndent()214 bool WriteIndent() {
215 return Write(indent_);
216 }
217
218 // Converts a time string written to the JSON codec into a time_t string
219 // (used by bookmarks.html) and writes it.
WriteTime(const std::string & time_string)220 bool WriteTime(const std::string& time_string) {
221 int64 internal_value;
222 base::StringToInt64(time_string, &internal_value);
223 return Write(base::Int64ToString(
224 base::Time::FromInternalValue(internal_value).ToTimeT()));
225 }
226
227 // Writes the node and all its children, returning true on success.
WriteNode(const DictionaryValue & value,BookmarkNode::Type folder_type)228 bool WriteNode(const DictionaryValue& value,
229 BookmarkNode::Type folder_type) {
230 std::string title, date_added_string, type_string;
231 if (!value.GetString(BookmarkCodec::kNameKey, &title) ||
232 !value.GetString(BookmarkCodec::kDateAddedKey, &date_added_string) ||
233 !value.GetString(BookmarkCodec::kTypeKey, &type_string) ||
234 (type_string != BookmarkCodec::kTypeURL &&
235 type_string != BookmarkCodec::kTypeFolder)) {
236 NOTREACHED();
237 return false;
238 }
239
240 if (type_string == BookmarkCodec::kTypeURL) {
241 std::string url_string;
242 if (!value.GetString(BookmarkCodec::kURLKey, &url_string)) {
243 NOTREACHED();
244 return false;
245 }
246
247 std::string favicon_string;
248 BookmarkFaviconFetcher::URLFaviconMap::iterator itr =
249 favicons_map_->find(url_string);
250 if (itr != favicons_map_->end()) {
251 scoped_refptr<RefCountedMemory> data(itr->second.get());
252 std::string favicon_data;
253 favicon_data.assign(reinterpret_cast<const char*>(data->front()),
254 data->size());
255 std::string favicon_base64_encoded;
256 if (base::Base64Encode(favicon_data, &favicon_base64_encoded)) {
257 GURL favicon_url("data:image/png;base64," + favicon_base64_encoded);
258 favicon_string = favicon_url.spec();
259 }
260 }
261
262 if (!WriteIndent() ||
263 !Write(kBookmarkStart) ||
264 !Write(url_string, ATTRIBUTE_VALUE) ||
265 !Write(kAddDate) ||
266 !WriteTime(date_added_string) ||
267 (!favicon_string.empty() &&
268 (!Write(kIcon) ||
269 !Write(favicon_string, ATTRIBUTE_VALUE))) ||
270 !Write(kBookmarkAttributeEnd) ||
271 !Write(title, CONTENT) ||
272 !Write(kBookmarkEnd) ||
273 !Write(kNewline)) {
274 return false;
275 }
276 return true;
277 }
278
279 // Folder.
280 std::string last_modified_date;
281 Value* child_values;
282 if (!value.GetString(BookmarkCodec::kDateModifiedKey,
283 &last_modified_date) ||
284 !value.Get(BookmarkCodec::kChildrenKey, &child_values) ||
285 child_values->GetType() != Value::TYPE_LIST) {
286 NOTREACHED();
287 return false;
288 }
289 if (folder_type != BookmarkNode::OTHER_NODE) {
290 // The other folder name is not written out. This gives the effect of
291 // making the contents of the 'other folder' be a sibling to the bookmark
292 // bar folder.
293 if (!WriteIndent() ||
294 !Write(kFolderStart) ||
295 !WriteTime(date_added_string) ||
296 !Write(kLastModified) ||
297 !WriteTime(last_modified_date)) {
298 return false;
299 }
300 if (folder_type == BookmarkNode::BOOKMARK_BAR) {
301 if (!Write(kBookmarkBar))
302 return false;
303 title = l10n_util::GetStringUTF8(IDS_BOOMARK_BAR_FOLDER_NAME);
304 } else if (!Write(kFolderAttributeEnd)) {
305 return false;
306 }
307 if (!Write(title, CONTENT) ||
308 !Write(kFolderEnd) ||
309 !Write(kNewline) ||
310 !WriteIndent() ||
311 !Write(kFolderChildren) ||
312 !Write(kNewline)) {
313 return false;
314 }
315 IncrementIndent();
316 }
317
318 // Write the children.
319 ListValue* children = static_cast<ListValue*>(child_values);
320 for (size_t i = 0; i < children->GetSize(); ++i) {
321 Value* child_value;
322 if (!children->Get(i, &child_value) ||
323 child_value->GetType() != Value::TYPE_DICTIONARY) {
324 NOTREACHED();
325 return false;
326 }
327 if (!WriteNode(*static_cast<DictionaryValue*>(child_value),
328 BookmarkNode::FOLDER)) {
329 return false;
330 }
331 }
332 if (folder_type != BookmarkNode::OTHER_NODE) {
333 // Close out the folder.
334 DecrementIndent();
335 if (!WriteIndent() ||
336 !Write(kFolderChildrenEnd) ||
337 !Write(kNewline)) {
338 return false;
339 }
340 }
341 return true;
342 }
343
344 // The BookmarkModel as a Value. This value was generated from the
345 // BookmarkCodec.
346 scoped_ptr<Value> bookmarks_;
347
348 // Path we're writing to.
349 FilePath path_;
350
351 // Map that stores favicon per URL.
352 scoped_ptr<BookmarkFaviconFetcher::URLFaviconMap> favicons_map_;
353
354 // Observer to be notified on finish.
355 BookmarksExportObserver* observer_;
356
357 // File we're writing to.
358 net::FileStream file_stream_;
359
360 // How much we indent when writing a bookmark/folder. This is modified
361 // via IncrementIndent and DecrementIndent.
362 std::string indent_;
363 };
364
365 } // namespace
366
BookmarkFaviconFetcher(Profile * profile,const FilePath & path,BookmarksExportObserver * observer)367 BookmarkFaviconFetcher::BookmarkFaviconFetcher(
368 Profile* profile,
369 const FilePath& path,
370 BookmarksExportObserver* observer)
371 : profile_(profile),
372 path_(path),
373 observer_(observer) {
374 favicons_map_.reset(new URLFaviconMap());
375 registrar_.Add(this,
376 NotificationType::PROFILE_DESTROYED,
377 Source<Profile>(profile_));
378 }
379
~BookmarkFaviconFetcher()380 BookmarkFaviconFetcher::~BookmarkFaviconFetcher() {
381 }
382
ExportBookmarks()383 void BookmarkFaviconFetcher::ExportBookmarks() {
384 ExtractUrls(profile_->GetBookmarkModel()->GetBookmarkBarNode());
385 ExtractUrls(profile_->GetBookmarkModel()->other_node());
386 if (!bookmark_urls_.empty()) {
387 FetchNextFavicon();
388 } else {
389 ExecuteWriter();
390 }
391 }
392
Observe(NotificationType type,const NotificationSource & source,const NotificationDetails & details)393 void BookmarkFaviconFetcher::Observe(NotificationType type,
394 const NotificationSource& source,
395 const NotificationDetails& details) {
396 if (NotificationType::PROFILE_DESTROYED == type && fetcher != NULL) {
397 MessageLoop::current()->DeleteSoon(FROM_HERE, fetcher);
398 fetcher = NULL;
399 }
400 }
401
ExtractUrls(const BookmarkNode * node)402 void BookmarkFaviconFetcher::ExtractUrls(const BookmarkNode* node) {
403 if (BookmarkNode::URL == node->type()) {
404 std::string url = node->GetURL().spec();
405 if (!url.empty()) {
406 bookmark_urls_.push_back(url);
407 }
408 } else {
409 for (int i = 0; i < node->child_count(); ++i) {
410 ExtractUrls(node->GetChild(i));
411 }
412 }
413 }
414
ExecuteWriter()415 void BookmarkFaviconFetcher::ExecuteWriter() {
416 // BookmarkModel isn't thread safe (nor would we want to lock it down
417 // for the duration of the write), as such we make a copy of the
418 // BookmarkModel using BookmarkCodec then write from that.
419 BookmarkCodec codec;
420 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
421 new Writer(codec.Encode(profile_->GetBookmarkModel()),
422 path_,
423 favicons_map_.release(),
424 observer_));
425 if (fetcher != NULL) {
426 MessageLoop::current()->DeleteSoon(FROM_HERE, fetcher);
427 fetcher = NULL;
428 }
429 }
430
FetchNextFavicon()431 bool BookmarkFaviconFetcher::FetchNextFavicon() {
432 if (bookmark_urls_.empty()) {
433 return false;
434 }
435 do {
436 std::string url = bookmark_urls_.front();
437 // Filter out urls that we've already got favicon for.
438 URLFaviconMap::const_iterator iter = favicons_map_->find(url);
439 if (favicons_map_->end() == iter) {
440 FaviconService* favicon_service =
441 profile_->GetFaviconService(Profile::EXPLICIT_ACCESS);
442 favicon_service->GetFaviconForURL(GURL(url), history::FAVICON,
443 &favicon_consumer_,
444 NewCallback(this, &BookmarkFaviconFetcher::OnFaviconDataAvailable));
445 return true;
446 } else {
447 bookmark_urls_.pop_front();
448 }
449 } while (!bookmark_urls_.empty());
450 return false;
451 }
452
OnFaviconDataAvailable(FaviconService::Handle handle,history::FaviconData favicon)453 void BookmarkFaviconFetcher::OnFaviconDataAvailable(
454 FaviconService::Handle handle,
455 history::FaviconData favicon) {
456 GURL url;
457 if (!bookmark_urls_.empty()) {
458 url = GURL(bookmark_urls_.front());
459 bookmark_urls_.pop_front();
460 }
461 if (favicon.is_valid() && !url.is_empty()) {
462 favicons_map_->insert(make_pair(url.spec(), favicon.image_data));
463 }
464
465 if (FetchNextFavicon()) {
466 return;
467 }
468 ExecuteWriter();
469 }
470
471 namespace bookmark_html_writer {
472
WriteBookmarks(Profile * profile,const FilePath & path,BookmarksExportObserver * observer)473 void WriteBookmarks(Profile* profile,
474 const FilePath& path,
475 BookmarksExportObserver* observer) {
476 // BookmarkModel isn't thread safe (nor would we want to lock it down
477 // for the duration of the write), as such we make a copy of the
478 // BookmarkModel using BookmarkCodec then write from that.
479 if (fetcher == NULL) {
480 fetcher = new BookmarkFaviconFetcher(profile, path, observer);
481 fetcher->ExportBookmarks();
482 }
483 }
484
485 } // namespace bookmark_html_writer
486