| // Copyright 2014 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 "components/enhanced_bookmarks/enhanced_bookmark_model.h" |
| |
| #include <iomanip> |
| #include <sstream> |
| |
| #include "base/base64.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/rand_util.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/thread_task_runner_handle.h" |
| #include "components/bookmarks/browser/bookmark_model.h" |
| #include "components/bookmarks/browser/bookmark_node.h" |
| #include "components/enhanced_bookmarks/enhanced_bookmark_model_observer.h" |
| #include "components/enhanced_bookmarks/proto/metadata.pb.h" |
| #include "ui/base/models/tree_node_iterator.h" |
| #include "url/gurl.h" |
| |
| using bookmarks::BookmarkModel; |
| using bookmarks::BookmarkNode; |
| |
| namespace { |
| const char* kBookmarkBarId = "f_bookmarks_bar"; |
| |
| const char* kIdKey = "stars.id"; |
| const char* kImageDataKey = "stars.imageData"; |
| const char* kNoteKey = "stars.note"; |
| const char* kOldIdKey = "stars.oldId"; |
| const char* kPageDataKey = "stars.pageData"; |
| const char* kVersionKey = "stars.version"; |
| |
| const char* kBookmarkPrefix = "ebc_"; |
| |
| // Helper method for working with bookmark metainfo. |
| std::string DataForMetaInfoField(const BookmarkNode* node, |
| const std::string& field) { |
| std::string value; |
| if (!node->GetMetaInfo(field, &value)) |
| return std::string(); |
| |
| std::string decoded; |
| if (!base::Base64Decode(value, &decoded)) |
| return std::string(); |
| |
| return decoded; |
| } |
| |
| // Helper method for working with ImageData_ImageInfo. |
| bool PopulateImageData(const image::collections::ImageData_ImageInfo& info, |
| GURL* out_url, |
| int* width, |
| int* height) { |
| if (!info.has_url() || !info.has_width() || !info.has_height()) |
| return false; |
| |
| GURL url(info.url()); |
| if (!url.is_valid()) |
| return false; |
| |
| *out_url = url; |
| *width = info.width(); |
| *height = info.height(); |
| return true; |
| } |
| |
| // Generate a random remote id, with a prefix that depends on whether the node |
| // is a folder or a bookmark. |
| std::string GenerateRemoteId() { |
| std::stringstream random_id; |
| random_id << kBookmarkPrefix; |
| |
| // Generate 32 digit hex string random suffix. |
| random_id << std::hex << std::setfill('0') << std::setw(16); |
| random_id << base::RandUint64() << base::RandUint64(); |
| return random_id.str(); |
| } |
| } // namespace |
| |
| namespace enhanced_bookmarks { |
| |
| EnhancedBookmarkModel::EnhancedBookmarkModel(BookmarkModel* bookmark_model, |
| const std::string& version) |
| : bookmark_model_(bookmark_model), |
| loaded_(false), |
| version_(version), |
| weak_ptr_factory_(this) { |
| bookmark_model_->AddObserver(this); |
| bookmark_model_->AddNonClonedKey(kIdKey); |
| if (bookmark_model_->loaded()) { |
| InitializeIdMap(); |
| loaded_ = true; |
| } |
| } |
| |
| EnhancedBookmarkModel::~EnhancedBookmarkModel() { |
| Shutdown(); |
| } |
| |
| void EnhancedBookmarkModel::Shutdown() { |
| if (bookmark_model_) { |
| FOR_EACH_OBSERVER(EnhancedBookmarkModelObserver, |
| observers_, |
| EnhancedBookmarkModelShuttingDown()); |
| weak_ptr_factory_.InvalidateWeakPtrs(); |
| bookmark_model_->RemoveObserver(this); |
| bookmark_model_ = NULL; |
| } |
| } |
| |
| void EnhancedBookmarkModel::AddObserver( |
| EnhancedBookmarkModelObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void EnhancedBookmarkModel::RemoveObserver( |
| EnhancedBookmarkModelObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| // Moves |node| to |new_parent| and inserts it at the given |index|. |
| void EnhancedBookmarkModel::Move(const BookmarkNode* node, |
| const BookmarkNode* new_parent, |
| int index) { |
| bookmark_model_->Move(node, new_parent, index); |
| } |
| |
| // Adds a new folder node at the specified position. |
| const BookmarkNode* EnhancedBookmarkModel::AddFolder( |
| const BookmarkNode* parent, |
| int index, |
| const base::string16& title) { |
| BookmarkNode::MetaInfoMap meta_info; |
| meta_info[kVersionKey] = GetVersionString(); |
| return bookmark_model_->AddFolderWithMetaInfo(parent, index, title, |
| &meta_info); |
| } |
| |
| // Adds a url at the specified position. |
| const BookmarkNode* EnhancedBookmarkModel::AddURL( |
| const BookmarkNode* parent, |
| int index, |
| const base::string16& title, |
| const GURL& url, |
| const base::Time& creation_time) { |
| BookmarkNode::MetaInfoMap meta_info; |
| meta_info[kIdKey] = GenerateRemoteId(); |
| meta_info[kVersionKey] = GetVersionString(); |
| return bookmark_model_->AddURLWithCreationTimeAndMetaInfo( |
| parent, index, title, url, creation_time, &meta_info); |
| } |
| |
| std::string EnhancedBookmarkModel::GetRemoteId(const BookmarkNode* node) { |
| if (node == bookmark_model_->bookmark_bar_node()) |
| return kBookmarkBarId; |
| |
| std::string id; |
| if (!node->GetMetaInfo(kIdKey, &id)) |
| return std::string(); |
| return id; |
| } |
| |
| const BookmarkNode* EnhancedBookmarkModel::BookmarkForRemoteId( |
| const std::string& remote_id) { |
| IdToNodeMap::iterator it = id_map_.find(remote_id); |
| if (it != id_map_.end()) |
| return it->second; |
| return NULL; |
| } |
| |
| void EnhancedBookmarkModel::SetDescription(const BookmarkNode* node, |
| const std::string& description) { |
| SetMetaInfo(node, kNoteKey, description); |
| } |
| |
| std::string EnhancedBookmarkModel::GetDescription(const BookmarkNode* node) { |
| // First, look for a custom note set by the user. |
| std::string description; |
| if (node->GetMetaInfo(kNoteKey, &description) && !description.empty()) |
| return description; |
| |
| // If none are present, return the snippet. |
| return GetSnippet(node); |
| } |
| |
| bool EnhancedBookmarkModel::SetOriginalImage(const BookmarkNode* node, |
| const GURL& url, |
| int width, |
| int height) { |
| DCHECK(node->is_url()); |
| DCHECK(url.is_valid()); |
| |
| std::string decoded(DataForMetaInfoField(node, kImageDataKey)); |
| image::collections::ImageData data; |
| |
| // Try to populate the imageData with the existing data. |
| if (!decoded.empty()) { |
| // If the parsing fails, something is wrong. Immediately fail. |
| bool result = data.ParseFromString(decoded); |
| if (!result) |
| return false; |
| } |
| |
| scoped_ptr<image::collections::ImageData_ImageInfo> info( |
| new image::collections::ImageData_ImageInfo); |
| info->set_url(url.spec()); |
| info->set_width(width); |
| info->set_height(height); |
| data.set_allocated_original_info(info.release()); |
| |
| std::string output; |
| bool result = data.SerializePartialToString(&output); |
| if (!result) |
| return false; |
| |
| std::string encoded; |
| base::Base64Encode(output, &encoded); |
| SetMetaInfo(node, kImageDataKey, encoded); |
| return true; |
| } |
| |
| void EnhancedBookmarkModel::RemoveImageData(const BookmarkNode* node) { |
| DCHECK(node->is_url()); |
| image::collections::ImageData data; |
| data.set_user_removed_image(true); |
| |
| std::string encoded_data; |
| base::Base64Encode(data.SerializeAsString(), &encoded_data); |
| SetMetaInfo(node, kImageDataKey, encoded_data); |
| } |
| |
| bool EnhancedBookmarkModel::GetOriginalImage(const BookmarkNode* node, |
| GURL* url, |
| int* width, |
| int* height) { |
| std::string decoded(DataForMetaInfoField(node, kImageDataKey)); |
| if (decoded.empty()) |
| return false; |
| |
| image::collections::ImageData data; |
| bool result = data.ParseFromString(decoded); |
| if (!result) |
| return false; |
| |
| if (!data.has_original_info()) |
| return false; |
| |
| return PopulateImageData(data.original_info(), url, width, height); |
| } |
| |
| bool EnhancedBookmarkModel::GetThumbnailImage(const BookmarkNode* node, |
| GURL* url, |
| int* width, |
| int* height) { |
| std::string decoded(DataForMetaInfoField(node, kImageDataKey)); |
| if (decoded.empty()) |
| return false; |
| |
| image::collections::ImageData data; |
| bool result = data.ParseFromString(decoded); |
| if (!result) |
| return false; |
| |
| if (!data.has_thumbnail_info()) |
| return false; |
| |
| return PopulateImageData(data.thumbnail_info(), url, width, height); |
| } |
| |
| std::string EnhancedBookmarkModel::GetSnippet(const BookmarkNode* node) { |
| std::string decoded(DataForMetaInfoField(node, kPageDataKey)); |
| if (decoded.empty()) |
| return decoded; |
| |
| image::collections::PageData data; |
| bool result = data.ParseFromString(decoded); |
| if (!result) |
| return std::string(); |
| |
| return data.snippet(); |
| } |
| |
| void EnhancedBookmarkModel::SetVersionSuffix( |
| const std::string& version_suffix) { |
| version_suffix_ = version_suffix; |
| } |
| |
| void EnhancedBookmarkModel::BookmarkModelChanged() { |
| } |
| |
| void EnhancedBookmarkModel::BookmarkModelLoaded(BookmarkModel* model, |
| bool ids_reassigned) { |
| InitializeIdMap(); |
| loaded_ = true; |
| FOR_EACH_OBSERVER( |
| EnhancedBookmarkModelObserver, observers_, EnhancedBookmarkModelLoaded()); |
| } |
| |
| void EnhancedBookmarkModel::BookmarkNodeAdded(BookmarkModel* model, |
| const BookmarkNode* parent, |
| int index) { |
| const BookmarkNode* node = parent->GetChild(index); |
| std::string remote_id; |
| if (node->GetMetaInfo(kIdKey, &remote_id)) { |
| AddToIdMap(node); |
| ScheduleResetDuplicateRemoteIds(); |
| } |
| FOR_EACH_OBSERVER( |
| EnhancedBookmarkModelObserver, observers_, EnhancedBookmarkAdded(node)); |
| } |
| |
| void EnhancedBookmarkModel::BookmarkNodeRemoved( |
| BookmarkModel* model, |
| const BookmarkNode* parent, |
| int old_index, |
| const BookmarkNode* node, |
| const std::set<GURL>& removed_urls) { |
| RemoveNodeFromMaps(node); |
| FOR_EACH_OBSERVER( |
| EnhancedBookmarkModelObserver, observers_, EnhancedBookmarkRemoved(node)); |
| } |
| |
| void EnhancedBookmarkModel::BookmarkNodeChanged(BookmarkModel* model, |
| const BookmarkNode* node) { |
| FOR_EACH_OBSERVER( |
| EnhancedBookmarkModelObserver, observers_, |
| EnhancedBookmarkNodeChanged(node)); |
| } |
| |
| void EnhancedBookmarkModel::OnWillChangeBookmarkMetaInfo( |
| BookmarkModel* model, |
| const BookmarkNode* node) { |
| prev_remote_id_ = GetRemoteId(node); |
| } |
| |
| void EnhancedBookmarkModel::BookmarkMetaInfoChanged(BookmarkModel* model, |
| const BookmarkNode* node) { |
| std::string remote_id = GetRemoteId(node); |
| if (remote_id != prev_remote_id_) { |
| id_map_.erase(prev_remote_id_); |
| if (!remote_id.empty()) { |
| AddToIdMap(node); |
| ScheduleResetDuplicateRemoteIds(); |
| } |
| FOR_EACH_OBSERVER( |
| EnhancedBookmarkModelObserver, |
| observers_, |
| EnhancedBookmarkRemoteIdChanged(node, prev_remote_id_, remote_id)); |
| } |
| } |
| |
| void EnhancedBookmarkModel::BookmarkAllUserNodesRemoved( |
| BookmarkModel* model, |
| const std::set<GURL>& removed_urls) { |
| id_map_.clear(); |
| // Re-initialize so non-user nodes with remote ids are present in the map. |
| InitializeIdMap(); |
| FOR_EACH_OBSERVER(EnhancedBookmarkModelObserver, |
| observers_, |
| EnhancedBookmarkAllUserNodesRemoved()); |
| } |
| |
| void EnhancedBookmarkModel::InitializeIdMap() { |
| ui::TreeNodeIterator<const BookmarkNode> iterator( |
| bookmark_model_->root_node()); |
| while (iterator.has_next()) { |
| AddToIdMap(iterator.Next()); |
| } |
| ScheduleResetDuplicateRemoteIds(); |
| } |
| |
| void EnhancedBookmarkModel::AddToIdMap(const BookmarkNode* node) { |
| std::string remote_id = GetRemoteId(node); |
| if (remote_id.empty()) |
| return; |
| |
| // Try to insert the node. |
| std::pair<IdToNodeMap::iterator, bool> result = |
| id_map_.insert(make_pair(remote_id, node)); |
| if (!result.second) { |
| // Some node already had the same remote id, so add both nodes to the |
| // to-be-reset set. |
| nodes_to_reset_[result.first->second] = remote_id; |
| nodes_to_reset_[node] = remote_id; |
| } |
| } |
| |
| void EnhancedBookmarkModel::RemoveNodeFromMaps(const BookmarkNode* node) { |
| for (int i = 0; i < node->child_count(); i++) { |
| RemoveNodeFromMaps(node->GetChild(i)); |
| } |
| std::string remote_id = GetRemoteId(node); |
| id_map_.erase(remote_id); |
| nodes_to_reset_.erase(node); |
| } |
| |
| void EnhancedBookmarkModel::ScheduleResetDuplicateRemoteIds() { |
| if (!nodes_to_reset_.empty()) { |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::Bind(&EnhancedBookmarkModel::ResetDuplicateRemoteIds, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void EnhancedBookmarkModel::ResetDuplicateRemoteIds() { |
| for (NodeToIdMap::iterator it = nodes_to_reset_.begin(); |
| it != nodes_to_reset_.end(); |
| ++it) { |
| BookmarkNode::MetaInfoMap meta_info; |
| meta_info[kIdKey] = ""; |
| meta_info[kOldIdKey] = it->second; |
| SetMultipleMetaInfo(it->first, meta_info); |
| } |
| nodes_to_reset_.clear(); |
| } |
| |
| void EnhancedBookmarkModel::SetMetaInfo(const BookmarkNode* node, |
| const std::string& field, |
| const std::string& value) { |
| DCHECK(!bookmark_model_->is_permanent_node(node)); |
| |
| BookmarkNode::MetaInfoMap meta_info; |
| const BookmarkNode::MetaInfoMap* old_meta_info = node->GetMetaInfoMap(); |
| if (old_meta_info) |
| meta_info.insert(old_meta_info->begin(), old_meta_info->end()); |
| |
| // Don't update anything if the value to set is already there. |
| BookmarkNode::MetaInfoMap::iterator it = meta_info.find(field); |
| if (it != meta_info.end() && it->second == value) |
| return; |
| |
| meta_info[field] = value; |
| meta_info[kVersionKey] = GetVersionString(); |
| bookmark_model_->SetNodeMetaInfoMap(node, meta_info); |
| } |
| |
| std::string EnhancedBookmarkModel::GetVersionString() { |
| if (version_suffix_.empty()) |
| return version_; |
| return version_ + '/' + version_suffix_; |
| } |
| |
| void EnhancedBookmarkModel::SetMultipleMetaInfo( |
| const BookmarkNode* node, |
| BookmarkNode::MetaInfoMap meta_info) { |
| DCHECK(!bookmark_model_->is_permanent_node(node)); |
| |
| // Don't update anything if every value is already set correctly. |
| if (node->GetMetaInfoMap()) { |
| bool changed = false; |
| const BookmarkNode::MetaInfoMap* old_meta_info = node->GetMetaInfoMap(); |
| for (BookmarkNode::MetaInfoMap::iterator it = meta_info.begin(); |
| it != meta_info.end(); |
| ++it) { |
| BookmarkNode::MetaInfoMap::const_iterator old_field = |
| old_meta_info->find(it->first); |
| if (old_field == old_meta_info->end() || |
| old_field->second != it->second) { |
| changed = true; |
| break; |
| } |
| } |
| if (!changed) |
| return; |
| |
| // Fill in the values that aren't changing |
| meta_info.insert(old_meta_info->begin(), old_meta_info->end()); |
| } |
| |
| meta_info[kVersionKey] = GetVersionString(); |
| bookmark_model_->SetNodeMetaInfoMap(node, meta_info); |
| } |
| |
| bool EnhancedBookmarkModel::SetAllImages(const BookmarkNode* node, |
| const GURL& image_url, |
| int image_width, |
| int image_height, |
| const GURL& thumbnail_url, |
| int thumbnail_width, |
| int thumbnail_height) { |
| DCHECK(node->is_url()); |
| DCHECK(image_url.is_valid() || image_url.is_empty()); |
| DCHECK(thumbnail_url.is_valid() || thumbnail_url.is_empty()); |
| std::string decoded(DataForMetaInfoField(node, kImageDataKey)); |
| image::collections::ImageData data; |
| |
| // Try to populate the imageData with the existing data. |
| if (!decoded.empty()) { |
| // If the parsing fails, something is wrong. Immediately fail. |
| bool result = data.ParseFromString(decoded); |
| if (!result) |
| return false; |
| } |
| |
| if (image_url.is_empty()) { |
| data.release_original_info(); |
| } else { |
| // Regardless of whether an image info exists, we make a new one. |
| // Intentially make a raw pointer. |
| image::collections::ImageData_ImageInfo* info = |
| new image::collections::ImageData_ImageInfo; |
| info->set_url(image_url.spec()); |
| info->set_width(image_width); |
| info->set_height(image_height); |
| // This method consumes the raw pointer. |
| data.set_allocated_original_info(info); |
| } |
| |
| if (thumbnail_url.is_empty()) { |
| data.release_thumbnail_info(); |
| } else { |
| // Regardless of whether an image info exists, we make a new one. |
| // Intentially make a raw pointer. |
| image::collections::ImageData_ImageInfo* info = |
| new image::collections::ImageData_ImageInfo; |
| info->set_url(thumbnail_url.spec()); |
| info->set_width(thumbnail_width); |
| info->set_height(thumbnail_height); |
| // This method consumes the raw pointer. |
| data.set_allocated_thumbnail_info(info); |
| } |
| std::string output; |
| bool result = data.SerializePartialToString(&output); |
| if (!result) |
| return false; |
| |
| std::string encoded; |
| base::Base64Encode(output, &encoded); |
| bookmark_model_->SetNodeMetaInfo(node, kImageDataKey, encoded); |
| return true; |
| } |
| |
| } // namespace enhanced_bookmarks |