diff options
author | Allan Sandfeld Jensen <allan.jensen@theqtcompany.com> | 2016-05-09 14:22:11 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2016-05-09 15:11:45 +0000 |
commit | 2ddb2d3e14eef3de7dbd0cef553d669b9ac2361c (patch) | |
tree | e75f511546c5fd1a173e87c1f9fb11d7ac8d1af3 /chromium/components/bookmarks | |
parent | a4f3d46271c57e8155ba912df46a05559d14726e (diff) | |
download | qtwebengine-chromium-2ddb2d3e14eef3de7dbd0cef553d669b9ac2361c.tar.gz |
BASELINE: Update Chromium to 51.0.2704.41
Also adds in all smaller components by reversing logic for exclusion.
Change-Id: Ibf90b506e7da088ea2f65dcf23f2b0992c504422
Reviewed-by: Joerg Bornemann <joerg.bornemann@theqtcompany.com>
Diffstat (limited to 'chromium/components/bookmarks')
62 files changed, 11065 insertions, 0 deletions
diff --git a/chromium/components/bookmarks/DEPS b/chromium/components/bookmarks/DEPS new file mode 100644 index 00000000000..40b213ec092 --- /dev/null +++ b/chromium/components/bookmarks/DEPS @@ -0,0 +1,18 @@ +include_rules = [ + "+components/favicon_base", + "+components/keyed_service", + "+components/pref_registry", + "+components/prefs", + "+components/query_parser", + "+components/url_formatter", + "+grit/components_strings.h", + "+jni", + "+net/base", + "+ui", +] + +specific_include_rules = { + "bookmark_model_unittest.cc": [ + "+third_party/skia", + ] +} diff --git a/chromium/components/bookmarks/OWNERS b/chromium/components/bookmarks/OWNERS new file mode 100644 index 00000000000..90b3e809ffe --- /dev/null +++ b/chromium/components/bookmarks/OWNERS @@ -0,0 +1 @@ +sky@chromium.org diff --git a/chromium/components/bookmarks/browser/BUILD.gn b/chromium/components/bookmarks/browser/BUILD.gn new file mode 100644 index 00000000000..66ffe44fb02 --- /dev/null +++ b/chromium/components/bookmarks/browser/BUILD.gn @@ -0,0 +1,99 @@ +# 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. + +import("//build/config/ui.gni") + +source_set("browser") { + sources = [ + "base_bookmark_model_observer.cc", + "base_bookmark_model_observer.h", + "bookmark_client.cc", + "bookmark_client.h", + "bookmark_codec.cc", + "bookmark_codec.h", + "bookmark_expanded_state_tracker.cc", + "bookmark_expanded_state_tracker.h", + "bookmark_index.cc", + "bookmark_index.h", + "bookmark_match.cc", + "bookmark_match.h", + "bookmark_model.cc", + "bookmark_model.h", + "bookmark_model_observer.h", + "bookmark_node.cc", + "bookmark_node.h", + "bookmark_node_data.cc", + "bookmark_node_data.h", + "bookmark_node_data_ios.cc", + "bookmark_node_data_mac.cc", + "bookmark_pasteboard_helper_mac.h", + "bookmark_pasteboard_helper_mac.mm", + "bookmark_storage.cc", + "bookmark_storage.h", + "bookmark_undo_delegate.h", + "bookmark_undo_provider.h", + "bookmark_utils.cc", + "bookmark_utils.h", + "scoped_group_bookmark_actions.cc", + "scoped_group_bookmark_actions.h", + "startup_task_runner_service.cc", + "startup_task_runner_service.h", + ] + + public_deps = [ + "//components/bookmarks/common", + ] + + deps = [ + "//base", + "//base:i18n", + "//components/favicon_base", + "//components/keyed_service/core", + "//components/pref_registry", + "//components/prefs", + "//components/query_parser", + "//components/strings", + "//components/url_formatter", + "//net", + "//third_party/icu", + "//ui/base", + "//ui/gfx", + "//url", + ] + + if (toolkit_views) { + sources += [ "bookmark_node_data_views.cc" ] + deps += [ "//ui/views" ] + } +} + +source_set("unit_tests") { + testonly = true + sources = [ + "bookmark_codec_unittest.cc", + "bookmark_expanded_state_tracker_unittest.cc", + "bookmark_index_unittest.cc", + "bookmark_model_unittest.cc", + "bookmark_utils_unittest.cc", + ] + + if (toolkit_views) { + sources += [ "bookmark_node_data_unittest.cc" ] + } + + configs += [ "//build/config/compiler:no_size_t_to_int_warning" ] + + deps = [ + ":browser", + "//components/bookmarks/common", + "//components/bookmarks/test", + "//components/favicon_base", + "//components/pref_registry", + "//components/prefs", + "//components/prefs:test_support", + "//testing/gtest", + "//ui/base", + "//url", + ] +} diff --git a/chromium/components/bookmarks/browser/OWNERS b/chromium/components/bookmarks/browser/OWNERS new file mode 100644 index 00000000000..3a970a95432 --- /dev/null +++ b/chromium/components/bookmarks/browser/OWNERS @@ -0,0 +1,2 @@ +per-file bookmark_pasteboard_helper_mac.*=avi@chromium.org +per-file bookmark_node_data_mac.*=avi@chromium.org diff --git a/chromium/components/bookmarks/browser/base_bookmark_model_observer.cc b/chromium/components/bookmarks/browser/base_bookmark_model_observer.cc new file mode 100644 index 00000000000..7d40d0f9eb5 --- /dev/null +++ b/chromium/components/bookmarks/browser/base_bookmark_model_observer.cc @@ -0,0 +1,63 @@ +// 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/bookmarks/browser/base_bookmark_model_observer.h" + +namespace bookmarks { + +void BaseBookmarkModelObserver::BookmarkModelLoaded(BookmarkModel* model, + bool ids_reassigned) {} + +void BaseBookmarkModelObserver::BookmarkModelBeingDeleted( + BookmarkModel* model) { + BookmarkModelChanged(); +} + +void BaseBookmarkModelObserver::BookmarkNodeMoved( + BookmarkModel* model, + const BookmarkNode* old_parent, + int old_index, + const BookmarkNode* new_parent, + int new_index) { + BookmarkModelChanged(); +} + +void BaseBookmarkModelObserver::BookmarkNodeAdded(BookmarkModel* model, + const BookmarkNode* parent, + int index) { + BookmarkModelChanged(); +} + +void BaseBookmarkModelObserver::BookmarkNodeRemoved( + BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node, + const std::set<GURL>& removed_urls) { + BookmarkModelChanged(); +} + +void BaseBookmarkModelObserver::BookmarkAllUserNodesRemoved( + BookmarkModel* model, + const std::set<GURL>& removed_urls) { + BookmarkModelChanged(); +} + +void BaseBookmarkModelObserver::BookmarkNodeChanged(BookmarkModel* model, + const BookmarkNode* node) { + BookmarkModelChanged(); +} + +void BaseBookmarkModelObserver::BookmarkNodeFaviconChanged( + BookmarkModel* model, + const BookmarkNode* node) { +} + +void BaseBookmarkModelObserver::BookmarkNodeChildrenReordered( + BookmarkModel* model, + const BookmarkNode* node) { + BookmarkModelChanged(); +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/base_bookmark_model_observer.h b/chromium/components/bookmarks/browser/base_bookmark_model_observer.h new file mode 100644 index 00000000000..14a184ef777 --- /dev/null +++ b/chromium/components/bookmarks/browser/base_bookmark_model_observer.h @@ -0,0 +1,55 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_BROWSER_BASE_BOOKMARK_MODEL_OBSERVER_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BASE_BOOKMARK_MODEL_OBSERVER_H_ + +#include "base/macros.h" +#include "components/bookmarks/browser/bookmark_model_observer.h" + +namespace bookmarks { + +// Base class for a BookmarkModelObserver implementation. All mutations of the +// model funnel into the method BookmarkModelChanged. +class BaseBookmarkModelObserver : public BookmarkModelObserver { + public: + BaseBookmarkModelObserver() {} + + virtual void BookmarkModelChanged() = 0; + + // BookmarkModelObserver: + void BookmarkModelLoaded(BookmarkModel* model, bool ids_reassigned) override; + void BookmarkModelBeingDeleted(BookmarkModel* model) override; + void BookmarkNodeMoved(BookmarkModel* model, + const BookmarkNode* old_parent, + int old_index, + const BookmarkNode* new_parent, + int new_index) override; + void BookmarkNodeAdded(BookmarkModel* model, + const BookmarkNode* parent, + int index) override; + void BookmarkNodeRemoved(BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node, + const std::set<GURL>& removed_urls) override; + void BookmarkAllUserNodesRemoved(BookmarkModel* model, + const std::set<GURL>& removed_urls) override; + void BookmarkNodeChanged(BookmarkModel* model, + const BookmarkNode* node) override; + void BookmarkNodeFaviconChanged(BookmarkModel* model, + const BookmarkNode* node) override; + void BookmarkNodeChildrenReordered(BookmarkModel* model, + const BookmarkNode* node) override; + + protected: + ~BaseBookmarkModelObserver() override {} + + private: + DISALLOW_COPY_AND_ASSIGN(BaseBookmarkModelObserver); +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BASE_BOOKMARK_MODEL_OBSERVER_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_client.cc b/chromium/components/bookmarks/browser/bookmark_client.cc new file mode 100644 index 00000000000..93250153eaf --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_client.cc @@ -0,0 +1,35 @@ +// 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/bookmarks/browser/bookmark_client.h" + +#include "base/logging.h" + +namespace bookmarks { + +void BookmarkClient::Init(BookmarkModel* model) {} + +bool BookmarkClient::PreferTouchIcon() { + return false; +} + +base::CancelableTaskTracker::TaskId BookmarkClient::GetFaviconImageForPageURL( + const GURL& page_url, + favicon_base::IconType type, + const favicon_base::FaviconImageCallback& callback, + base::CancelableTaskTracker* tracker) { + return base::CancelableTaskTracker::kBadTaskId; +} + +bool BookmarkClient::SupportsTypedCountForNodes() { + return false; +} + +void BookmarkClient::GetTypedCountForNodes( + const NodeSet& nodes, + NodeTypedCountPairs* node_typed_count_pairs) { + NOTREACHED(); +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_client.h b/chromium/components/bookmarks/browser/bookmark_client.h new file mode 100644 index 00000000000..5eb625fa88f --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_client.h @@ -0,0 +1,100 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_CLIENT_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_CLIENT_H_ + +#include <set> +#include <utility> +#include <vector> + +#include "base/callback_forward.h" +#include "base/task/cancelable_task_tracker.h" +#include "components/bookmarks/browser/bookmark_storage.h" +#include "components/favicon_base/favicon_callback.h" +#include "components/favicon_base/favicon_types.h" +#include "components/keyed_service/core/keyed_service.h" + +class GURL; + +namespace base { +struct UserMetricsAction; +} + +namespace bookmarks { + +class BookmarkModel; +class BookmarkNode; +class BookmarkPermanentNode; + +// This class abstracts operations that depends on the embedder's environment, +// e.g. Chrome. +class BookmarkClient { + public: + // Types representing a set of BookmarkNode and a mapping from BookmarkNode + // to the number of time the corresponding URL has been typed by the user in + // the Omnibox. + typedef std::set<const BookmarkNode*> NodeSet; + typedef std::pair<const BookmarkNode*, int> NodeTypedCountPair; + typedef std::vector<NodeTypedCountPair> NodeTypedCountPairs; + + virtual ~BookmarkClient() {} + + // Called during initialization of BookmarkModel. + virtual void Init(BookmarkModel* model); + + // Returns true if the embedder favors touch icons over favicons. + virtual bool PreferTouchIcon(); + + // Requests a favicon from the history cache for the web page at |page_url|. + // |callback| is run when the favicon has been fetched. If |type| is: + // - favicon_base::FAVICON, the returned gfx::Image is a multi-resolution + // image of gfx::kFaviconSize DIP width and height. The data from the + // history cache is resized if need be. + // - not favicon_base::FAVICON, the returned gfx::Image is a single-resolution + // image with the largest bitmap in the history cache for |page_url| and + // |type|. + virtual base::CancelableTaskTracker::TaskId GetFaviconImageForPageURL( + const GURL& page_url, + favicon_base::IconType type, + const favicon_base::FaviconImageCallback& callback, + base::CancelableTaskTracker* tracker); + + // Returns true if the embedder supports typed count for URL. + virtual bool SupportsTypedCountForNodes(); + + // Retrieves the number of time each BookmarkNode URL has been typed in + // the Omnibox by the user. + virtual void GetTypedCountForNodes( + const NodeSet& nodes, + NodeTypedCountPairs* node_typed_count_pairs); + + // Returns whether the embedder wants permanent node |node| + // to always be visible or to only show them when not empty. + virtual bool IsPermanentNodeVisible(const BookmarkPermanentNode* node) = 0; + + // Wrapper around RecordAction defined in base/metrics/user_metrics.h + // that ensure that the action is posted from the correct thread. + virtual void RecordAction(const base::UserMetricsAction& action) = 0; + + // Returns a task that will be used to load any additional root nodes. This + // task will be invoked in the Profile's IO task runner. + virtual LoadExtraCallback GetLoadExtraNodesCallback() = 0; + + // Returns true if the |permanent_node| can have its title updated. + virtual bool CanSetPermanentNodeTitle(const BookmarkNode* permanent_node) = 0; + + // Returns true if |node| should sync. + virtual bool CanSyncNode(const BookmarkNode* node) = 0; + + // Returns true if this node can be edited by the user. + // TODO(joaodasilva): the model should check this more aggressively, and + // should give the client a means to temporarily disable those checks. + // http://crbug.com/49598 + virtual bool CanBeEditedByUser(const BookmarkNode* node) = 0; +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_CLIENT_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_codec.cc b/chromium/components/bookmarks/browser/bookmark_codec.cc new file mode 100644 index 00000000000..ddda44b7fd3 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_codec.cc @@ -0,0 +1,492 @@ +// 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/bookmarks/browser/bookmark_codec.h" + +#include <stddef.h> + +#include <algorithm> + +#include "base/json/json_string_value_serializer.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/values.h" +#include "components/bookmarks/browser/bookmark_model.h" +#include "grit/components_strings.h" +#include "ui/base/l10n/l10n_util.h" +#include "url/gurl.h" + +using base::Time; + +namespace bookmarks { + +const char BookmarkCodec::kRootsKey[] = "roots"; +const char BookmarkCodec::kRootFolderNameKey[] = "bookmark_bar"; +const char BookmarkCodec::kOtherBookmarkFolderNameKey[] = "other"; +// The value is left as 'synced' for historical reasons. +const char BookmarkCodec::kMobileBookmarkFolderNameKey[] = "synced"; +const char BookmarkCodec::kVersionKey[] = "version"; +const char BookmarkCodec::kChecksumKey[] = "checksum"; +const char BookmarkCodec::kIdKey[] = "id"; +const char BookmarkCodec::kTypeKey[] = "type"; +const char BookmarkCodec::kNameKey[] = "name"; +const char BookmarkCodec::kDateAddedKey[] = "date_added"; +const char BookmarkCodec::kURLKey[] = "url"; +const char BookmarkCodec::kDateModifiedKey[] = "date_modified"; +const char BookmarkCodec::kChildrenKey[] = "children"; +const char BookmarkCodec::kMetaInfo[] = "meta_info"; +const char BookmarkCodec::kSyncTransactionVersion[] = + "sync_transaction_version"; +const char BookmarkCodec::kTypeURL[] = "url"; +const char BookmarkCodec::kTypeFolder[] = "folder"; + +// Current version of the file. +static const int kCurrentVersion = 1; + +BookmarkCodec::BookmarkCodec() + : ids_reassigned_(false), + ids_valid_(true), + maximum_id_(0), + model_sync_transaction_version_( + BookmarkNode::kInvalidSyncTransactionVersion) { +} + +BookmarkCodec::~BookmarkCodec() {} + +base::Value* BookmarkCodec::Encode(BookmarkModel* model) { + return Encode(model->bookmark_bar_node(), + model->other_node(), + model->mobile_node(), + model->root_node()->GetMetaInfoMap(), + model->root_node()->sync_transaction_version()); +} + +base::Value* BookmarkCodec::Encode( + const BookmarkNode* bookmark_bar_node, + const BookmarkNode* other_folder_node, + const BookmarkNode* mobile_folder_node, + const BookmarkNode::MetaInfoMap* model_meta_info_map, + int64_t sync_transaction_version) { + ids_reassigned_ = false; + InitializeChecksum(); + base::DictionaryValue* roots = new base::DictionaryValue(); + roots->Set(kRootFolderNameKey, EncodeNode(bookmark_bar_node)); + roots->Set(kOtherBookmarkFolderNameKey, EncodeNode(other_folder_node)); + roots->Set(kMobileBookmarkFolderNameKey, EncodeNode(mobile_folder_node)); + if (model_meta_info_map) + roots->Set(kMetaInfo, EncodeMetaInfo(*model_meta_info_map)); + if (sync_transaction_version != + BookmarkNode::kInvalidSyncTransactionVersion) { + roots->SetString(kSyncTransactionVersion, + base::Int64ToString(sync_transaction_version)); + } + base::DictionaryValue* main = new base::DictionaryValue(); + main->SetInteger(kVersionKey, kCurrentVersion); + FinalizeChecksum(); + // We are going to store the computed checksum. So set stored checksum to be + // the same as computed checksum. + stored_checksum_ = computed_checksum_; + main->Set(kChecksumKey, new base::StringValue(computed_checksum_)); + main->Set(kRootsKey, roots); + return main; +} + +bool BookmarkCodec::Decode(BookmarkNode* bb_node, + BookmarkNode* other_folder_node, + BookmarkNode* mobile_folder_node, + int64_t* max_id, + const base::Value& value) { + ids_.clear(); + ids_reassigned_ = false; + ids_valid_ = true; + maximum_id_ = 0; + stored_checksum_.clear(); + InitializeChecksum(); + bool success = DecodeHelper(bb_node, other_folder_node, mobile_folder_node, + value); + FinalizeChecksum(); + // If either the checksums differ or some IDs were missing/not unique, + // reassign IDs. + if (!ids_valid_ || computed_checksum() != stored_checksum()) + ReassignIDs(bb_node, other_folder_node, mobile_folder_node); + *max_id = maximum_id_ + 1; + return success; +} + +base::Value* BookmarkCodec::EncodeNode(const BookmarkNode* node) { + base::DictionaryValue* value = new base::DictionaryValue(); + std::string id = base::Int64ToString(node->id()); + value->SetString(kIdKey, id); + const base::string16& title = node->GetTitle(); + value->SetString(kNameKey, title); + value->SetString(kDateAddedKey, + base::Int64ToString(node->date_added().ToInternalValue())); + if (node->is_url()) { + value->SetString(kTypeKey, kTypeURL); + std::string url = node->url().possibly_invalid_spec(); + value->SetString(kURLKey, url); + UpdateChecksumWithUrlNode(id, title, url); + } else { + value->SetString(kTypeKey, kTypeFolder); + value->SetString( + kDateModifiedKey, + base::Int64ToString(node->date_folder_modified().ToInternalValue())); + UpdateChecksumWithFolderNode(id, title); + + base::ListValue* child_values = new base::ListValue(); + value->Set(kChildrenKey, child_values); + for (int i = 0; i < node->child_count(); ++i) + child_values->Append(EncodeNode(node->GetChild(i))); + } + const BookmarkNode::MetaInfoMap* meta_info_map = node->GetMetaInfoMap(); + if (meta_info_map) + value->Set(kMetaInfo, EncodeMetaInfo(*meta_info_map)); + if (node->sync_transaction_version() != + BookmarkNode::kInvalidSyncTransactionVersion) { + value->SetString(kSyncTransactionVersion, + base::Int64ToString(node->sync_transaction_version())); + } + return value; +} + +base::Value* BookmarkCodec::EncodeMetaInfo( + const BookmarkNode::MetaInfoMap& meta_info_map) { + base::DictionaryValue* meta_info = new base::DictionaryValue; + for (BookmarkNode::MetaInfoMap::const_iterator it = meta_info_map.begin(); + it != meta_info_map.end(); ++it) { + meta_info->SetStringWithoutPathExpansion(it->first, it->second); + } + return meta_info; +} + +bool BookmarkCodec::DecodeHelper(BookmarkNode* bb_node, + BookmarkNode* other_folder_node, + BookmarkNode* mobile_folder_node, + const base::Value& value) { + const base::DictionaryValue* d_value = nullptr; + if (!value.GetAsDictionary(&d_value)) + return false; // Unexpected type. + + int version; + if (!d_value->GetInteger(kVersionKey, &version) || version != kCurrentVersion) + return false; // Unknown version. + + const base::Value* checksum_value; + if (d_value->Get(kChecksumKey, &checksum_value)) { + if (checksum_value->GetType() != base::Value::TYPE_STRING) + return false; + if (!checksum_value->GetAsString(&stored_checksum_)) + return false; + } + + const base::Value* roots; + if (!d_value->Get(kRootsKey, &roots)) + return false; // No roots. + + const base::DictionaryValue* roots_d_value = nullptr; + if (!roots->GetAsDictionary(&roots_d_value)) + return false; // Invalid type for roots. + const base::Value* root_folder_value; + const base::Value* other_folder_value = nullptr; + const base::DictionaryValue* root_folder_d_value = nullptr; + const base::DictionaryValue* other_folder_d_value = nullptr; + if (!roots_d_value->Get(kRootFolderNameKey, &root_folder_value) || + !root_folder_value->GetAsDictionary(&root_folder_d_value) || + !roots_d_value->Get(kOtherBookmarkFolderNameKey, &other_folder_value) || + !other_folder_value->GetAsDictionary(&other_folder_d_value)) { + return false; // Invalid type for root folder and/or other + // folder. + } + DecodeNode(*root_folder_d_value, nullptr, bb_node); + DecodeNode(*other_folder_d_value, nullptr, other_folder_node); + + // Fail silently if we can't deserialize mobile bookmarks. We can't require + // them to exist in order to be backwards-compatible with older versions of + // chrome. + const base::Value* mobile_folder_value; + const base::DictionaryValue* mobile_folder_d_value = nullptr; + if (roots_d_value->Get(kMobileBookmarkFolderNameKey, &mobile_folder_value) && + mobile_folder_value->GetAsDictionary(&mobile_folder_d_value)) { + DecodeNode(*mobile_folder_d_value, nullptr, mobile_folder_node); + } else { + // If we didn't find the mobile folder, we're almost guaranteed to have a + // duplicate id when we add the mobile folder. Consequently, if we don't + // intend to reassign ids in the future (ids_valid_ is still true), then at + // least reassign the mobile bookmarks to avoid it colliding with anything + // else. + if (ids_valid_) + ReassignIDsHelper(mobile_folder_node); + } + + if (!DecodeMetaInfo(*roots_d_value, &model_meta_info_map_, + &model_sync_transaction_version_)) + return false; + + std::string sync_transaction_version_str; + if (roots_d_value->GetString(kSyncTransactionVersion, + &sync_transaction_version_str) && + !base::StringToInt64(sync_transaction_version_str, + &model_sync_transaction_version_)) + return false; + + // Need to reset the type as decoding resets the type to FOLDER. Similarly + // we need to reset the title as the title is persisted and restored from + // the file. + bb_node->set_type(BookmarkNode::BOOKMARK_BAR); + other_folder_node->set_type(BookmarkNode::OTHER_NODE); + mobile_folder_node->set_type(BookmarkNode::MOBILE); + bb_node->SetTitle(l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_FOLDER_NAME)); + other_folder_node->SetTitle( + l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_OTHER_FOLDER_NAME)); + mobile_folder_node->SetTitle( + l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_MOBILE_FOLDER_NAME)); + + return true; +} + +bool BookmarkCodec::DecodeChildren(const base::ListValue& child_value_list, + BookmarkNode* parent) { + for (size_t i = 0; i < child_value_list.GetSize(); ++i) { + const base::Value* child_value; + if (!child_value_list.Get(i, &child_value)) + return false; + + const base::DictionaryValue* child_d_value = nullptr; + if (!child_value->GetAsDictionary(&child_d_value)) + return false; + DecodeNode(*child_d_value, parent, nullptr); + } + return true; +} + +bool BookmarkCodec::DecodeNode(const base::DictionaryValue& value, + BookmarkNode* parent, + BookmarkNode* node) { + // If no |node| is specified, we'll create one and add it to the |parent|. + // Therefore, in that case, |parent| must be non-NULL. + if (!node && !parent) { + NOTREACHED(); + return false; + } + + std::string id_string; + int64_t id = 0; + if (ids_valid_) { + if (!value.GetString(kIdKey, &id_string) || + !base::StringToInt64(id_string, &id) || + ids_.count(id) != 0) { + ids_valid_ = false; + } else { + ids_.insert(id); + } + } + + maximum_id_ = std::max(maximum_id_, id); + + base::string16 title; + value.GetString(kNameKey, &title); + + std::string date_added_string; + if (!value.GetString(kDateAddedKey, &date_added_string)) + date_added_string = base::Int64ToString(Time::Now().ToInternalValue()); + int64_t internal_time; + base::StringToInt64(date_added_string, &internal_time); + + std::string type_string; + if (!value.GetString(kTypeKey, &type_string)) + return false; + + if (type_string != kTypeURL && type_string != kTypeFolder) + return false; // Unknown type. + + if (type_string == kTypeURL) { + std::string url_string; + if (!value.GetString(kURLKey, &url_string)) + return false; + + GURL url = GURL(url_string); + if (!node && url.is_valid()) + node = new BookmarkNode(id, url); + else + return false; // Node invalid. + + if (parent) + parent->Add(node, parent->child_count()); + node->set_type(BookmarkNode::URL); + UpdateChecksumWithUrlNode(id_string, title, url_string); + } else { + std::string last_modified_date; + if (!value.GetString(kDateModifiedKey, &last_modified_date)) + last_modified_date = base::Int64ToString(Time::Now().ToInternalValue()); + + const base::Value* child_values; + if (!value.Get(kChildrenKey, &child_values)) + return false; + + if (child_values->GetType() != base::Value::TYPE_LIST) + return false; + + if (!node) { + node = new BookmarkNode(id, GURL()); + } else { + // If a new node is not created, explicitly assign ID to the existing one. + node->set_id(id); + } + + node->set_type(BookmarkNode::FOLDER); + int64_t internal_time; + base::StringToInt64(last_modified_date, &internal_time); + node->set_date_folder_modified(Time::FromInternalValue(internal_time)); + + if (parent) + parent->Add(node, parent->child_count()); + + UpdateChecksumWithFolderNode(id_string, title); + + const base::ListValue* child_l_values = nullptr; + if (!child_values->GetAsList(&child_l_values)) + return false; + if (!DecodeChildren(*child_l_values, node)) + return false; + } + + node->SetTitle(title); + node->set_date_added(Time::FromInternalValue(internal_time)); + + int64_t sync_transaction_version = node->sync_transaction_version(); + BookmarkNode::MetaInfoMap meta_info_map; + if (!DecodeMetaInfo(value, &meta_info_map, &sync_transaction_version)) + return false; + node->SetMetaInfoMap(meta_info_map); + + std::string sync_transaction_version_str; + if (value.GetString(kSyncTransactionVersion, &sync_transaction_version_str) && + !base::StringToInt64(sync_transaction_version_str, + &sync_transaction_version)) + return false; + + node->set_sync_transaction_version(sync_transaction_version); + + return true; +} + +bool BookmarkCodec::DecodeMetaInfo(const base::DictionaryValue& value, + BookmarkNode::MetaInfoMap* meta_info_map, + int64_t* sync_transaction_version) { + DCHECK(meta_info_map); + DCHECK(sync_transaction_version); + meta_info_map->clear(); + + const base::Value* meta_info; + if (!value.Get(kMetaInfo, &meta_info)) + return true; + + scoped_ptr<base::Value> deserialized_holder; + + // Meta info used to be stored as a serialized dictionary, so attempt to + // parse the value as one. + if (meta_info->IsType(base::Value::TYPE_STRING)) { + std::string meta_info_str; + meta_info->GetAsString(&meta_info_str); + JSONStringValueDeserializer deserializer(meta_info_str); + deserialized_holder = deserializer.Deserialize(nullptr, nullptr); + if (!deserialized_holder) + return false; + meta_info = deserialized_holder.get(); + } + // meta_info is now either the kMetaInfo node, or the deserialized node if it + // was stored as a string. Either way it should now be a (possibly nested) + // dictionary of meta info values. + const base::DictionaryValue* meta_info_dict; + if (!meta_info->GetAsDictionary(&meta_info_dict)) + return false; + DecodeMetaInfoHelper(*meta_info_dict, std::string(), meta_info_map); + + // Previously sync transaction version was stored in the meta info field + // using this key. If the key is present when decoding, set the sync + // transaction version to its value, then delete the field. + if (deserialized_holder) { + const char kBookmarkTransactionVersionKey[] = "sync.transaction_version"; + BookmarkNode::MetaInfoMap::iterator it = + meta_info_map->find(kBookmarkTransactionVersionKey); + if (it != meta_info_map->end()) { + base::StringToInt64(it->second, sync_transaction_version); + meta_info_map->erase(it); + } + } + + return true; +} + +void BookmarkCodec::DecodeMetaInfoHelper( + const base::DictionaryValue& dict, + const std::string& prefix, + BookmarkNode::MetaInfoMap* meta_info_map) { + for (base::DictionaryValue::Iterator it(dict); !it.IsAtEnd(); it.Advance()) { + if (it.value().IsType(base::Value::TYPE_DICTIONARY)) { + const base::DictionaryValue* subdict; + it.value().GetAsDictionary(&subdict); + DecodeMetaInfoHelper(*subdict, prefix + it.key() + ".", meta_info_map); + } else if (it.value().IsType(base::Value::TYPE_STRING)) { + it.value().GetAsString(&(*meta_info_map)[prefix + it.key()]); + } + } +} + +void BookmarkCodec::ReassignIDs(BookmarkNode* bb_node, + BookmarkNode* other_node, + BookmarkNode* mobile_node) { + maximum_id_ = 0; + ReassignIDsHelper(bb_node); + ReassignIDsHelper(other_node); + ReassignIDsHelper(mobile_node); + ids_reassigned_ = true; +} + +void BookmarkCodec::ReassignIDsHelper(BookmarkNode* node) { + DCHECK(node); + node->set_id(++maximum_id_); + for (int i = 0; i < node->child_count(); ++i) + ReassignIDsHelper(node->GetChild(i)); +} + +void BookmarkCodec::UpdateChecksum(const std::string& str) { + base::MD5Update(&md5_context_, str); +} + +void BookmarkCodec::UpdateChecksum(const base::string16& str) { + base::MD5Update(&md5_context_, + base::StringPiece( + reinterpret_cast<const char*>(str.data()), + str.length() * sizeof(str[0]))); +} + +void BookmarkCodec::UpdateChecksumWithUrlNode(const std::string& id, + const base::string16& title, + const std::string& url) { + DCHECK(base::IsStringUTF8(url)); + UpdateChecksum(id); + UpdateChecksum(title); + UpdateChecksum(kTypeURL); + UpdateChecksum(url); +} + +void BookmarkCodec::UpdateChecksumWithFolderNode(const std::string& id, + const base::string16& title) { + UpdateChecksum(id); + UpdateChecksum(title); + UpdateChecksum(kTypeFolder); +} + +void BookmarkCodec::InitializeChecksum() { + base::MD5Init(&md5_context_); +} + +void BookmarkCodec::FinalizeChecksum() { + base::MD5Digest digest; + base::MD5Final(&digest, &md5_context_); + computed_checksum_ = base::MD5DigestToBase16(digest); +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_codec.h b/chromium/components/bookmarks/browser/bookmark_codec.h new file mode 100644 index 00000000000..8b42d3940bb --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_codec.h @@ -0,0 +1,212 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_CODEC_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_CODEC_H_ + +#include <stdint.h> +#include <set> +#include <string> + +#include "base/macros.h" +#include "base/md5.h" +#include "base/strings/string16.h" +#include "components/bookmarks/browser/bookmark_node.h" + +namespace base { +class DictionaryValue; +class ListValue; +class Value; +} + +namespace bookmarks { + +class BookmarkModel; + +// BookmarkCodec is responsible for encoding and decoding the BookmarkModel +// into JSON values. The encoded values are written to disk via the +// BookmarkStorage. +class BookmarkCodec { + public: + // Creates an instance of the codec. During decoding, if the IDs in the file + // are not unique, we will reassign IDs to make them unique. There are no + // guarantees on how the IDs are reassigned or about doing minimal + // reassignments to achieve uniqueness. + BookmarkCodec(); + ~BookmarkCodec(); + + // Encodes the model to a JSON value. It's up to the caller to delete the + // returned object. This is invoked to encode the contents of the bookmark bar + // model and is currently a convenience to invoking Encode that takes the + // bookmark bar node and other folder node. + base::Value* Encode(BookmarkModel* model); + + // Encodes the bookmark bar and other folders returning the JSON value. It's + // up to the caller to delete the returned object. + base::Value* Encode(const BookmarkNode* bookmark_bar_node, + const BookmarkNode* other_folder_node, + const BookmarkNode* mobile_folder_node, + const BookmarkNode::MetaInfoMap* model_meta_info_map, + int64_t sync_transaction_version); + + // Decodes the previously encoded value to the specified nodes as well as + // setting |max_node_id| to the greatest node id. Returns true on success, + // false otherwise. If there is an error (such as unexpected version) all + // children are removed from the bookmark bar and other folder nodes. On exit + // |max_node_id| is set to the max id of the nodes. + bool Decode(BookmarkNode* bb_node, + BookmarkNode* other_folder_node, + BookmarkNode* mobile_folder_node, + int64_t* max_node_id, + const base::Value& value); + + // Returns the checksum computed during last encoding/decoding call. + const std::string& computed_checksum() const { return computed_checksum_; } + + // Returns the checksum that's stored in the file. After a call to Encode, + // the computed and stored checksums are the same since the computed checksum + // is stored to the file. After a call to decode, the computed checksum can + // differ from the stored checksum if the file contents were changed by the + // user. + const std::string& stored_checksum() const { return stored_checksum_; } + + // Return meta info of bookmark model root. + const BookmarkNode::MetaInfoMap& model_meta_info_map() const { + return model_meta_info_map_; + } + + // Return the sync transaction version of the bookmark model root. + int64_t model_sync_transaction_version() const { + return model_sync_transaction_version_; + } + + // Returns whether the IDs were reassigned during decoding. Always returns + // false after encoding. + bool ids_reassigned() const { return ids_reassigned_; } + + // Names of the various keys written to the Value. + static const char kRootsKey[]; + static const char kRootFolderNameKey[]; + static const char kOtherBookmarkFolderNameKey[]; + static const char kMobileBookmarkFolderNameKey[]; + static const char kVersionKey[]; + static const char kChecksumKey[]; + static const char kIdKey[]; + static const char kTypeKey[]; + static const char kNameKey[]; + static const char kDateAddedKey[]; + static const char kURLKey[]; + static const char kDateModifiedKey[]; + static const char kChildrenKey[]; + static const char kMetaInfo[]; + static const char kSyncTransactionVersion[]; + + // Possible values for kTypeKey. + static const char kTypeURL[]; + static const char kTypeFolder[]; + + private: + // Encodes node and all its children into a Value object and returns it. + // The caller takes ownership of the returned object. + base::Value* EncodeNode(const BookmarkNode* node); + + // Encodes the given meta info into a Value object and returns it. The caller + // takes ownership of the returned object. + base::Value* EncodeMetaInfo(const BookmarkNode::MetaInfoMap& meta_info_map); + + // Helper to perform decoding. + bool DecodeHelper(BookmarkNode* bb_node, + BookmarkNode* other_folder_node, + BookmarkNode* mobile_folder_node, + const base::Value& value); + + // Decodes the children of the specified node. Returns true on success. + bool DecodeChildren(const base::ListValue& child_value_list, + BookmarkNode* parent); + + // Reassigns bookmark IDs for all nodes. + void ReassignIDs(BookmarkNode* bb_node, + BookmarkNode* other_node, + BookmarkNode* mobile_node); + + // Helper to recursively reassign IDs. + void ReassignIDsHelper(BookmarkNode* node); + + // Decodes the supplied node from the supplied value. Child nodes are + // created appropriately by way of DecodeChildren. If node is NULL a new + // node is created and added to parent (parent must then be non-NULL), + // otherwise node is used. + bool DecodeNode(const base::DictionaryValue& value, + BookmarkNode* parent, + BookmarkNode* node); + + // Decodes the meta info from the supplied value. If the meta info contains + // a "sync.transaction_version" key, the value of that field will be stored + // in the sync_transaction_version variable, then deleted. This is for + // backward-compatibility reasons. + // meta_info_map and sync_transaction_version must not be NULL. + bool DecodeMetaInfo(const base::DictionaryValue& value, + BookmarkNode::MetaInfoMap* meta_info_map, + int64_t* sync_transaction_version); + + // Decodes the meta info from the supplied sub-node dictionary. The values + // found will be inserted in meta_info_map with the given prefix added to the + // start of their keys. + void DecodeMetaInfoHelper(const base::DictionaryValue& dict, + const std::string& prefix, + BookmarkNode::MetaInfoMap* meta_info_map); + + // Updates the check-sum with the given string. + void UpdateChecksum(const std::string& str); + void UpdateChecksum(const base::string16& str); + + // Updates the check-sum with the given contents of URL/folder bookmark node. + // NOTE: These functions take in individual properties of a bookmark node + // instead of taking in a BookmarkNode for efficiency so that we don't convert + // various data-types to UTF16 strings multiple times - once for serializing + // and once for computing the check-sum. + // The url parameter should be a valid UTF8 string. + void UpdateChecksumWithUrlNode(const std::string& id, + const base::string16& title, + const std::string& url); + void UpdateChecksumWithFolderNode(const std::string& id, + const base::string16& title); + + // Initializes/Finalizes the checksum. + void InitializeChecksum(); + void FinalizeChecksum(); + + // Whether or not IDs were reassigned by the codec. + bool ids_reassigned_; + + // Whether or not IDs are valid. This is initially true, but set to false + // if an id is missing or not unique. + bool ids_valid_; + + // Contains the id of each of the nodes found in the file. Used to determine + // if we have duplicates. + std::set<int64_t> ids_; + + // MD5 context used to compute MD5 hash of all bookmark data. + base::MD5Context md5_context_; + + // Checksums. + std::string computed_checksum_; + std::string stored_checksum_; + + // Maximum ID assigned when decoding data. + int64_t maximum_id_; + + // Meta info set on bookmark model root. + BookmarkNode::MetaInfoMap model_meta_info_map_; + + // Sync transaction version set on bookmark model root. + int64_t model_sync_transaction_version_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkCodec); +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_CODEC_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_codec_unittest.cc b/chromium/components/bookmarks/browser/bookmark_codec_unittest.cc new file mode 100644 index 00000000000..51e4c188678 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_codec_unittest.cc @@ -0,0 +1,478 @@ +// 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/bookmarks/browser/bookmark_codec.h" + +#include <stddef.h> +#include <stdint.h> + +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/json/json_file_value_serializer.h" +#include "base/json/json_string_value_serializer.h" +#include "base/memory/scoped_ptr.h" +#include "base/path_service.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/values.h" +#include "components/bookmarks/browser/bookmark_model.h" +#include "components/bookmarks/test/test_bookmark_client.h" +#include "testing/gtest/include/gtest/gtest.h" + +using base::ASCIIToUTF16; + +namespace bookmarks { +namespace { + +const char kUrl1Title[] = "url1"; +const char kUrl1Url[] = "http://www.url1.com"; +const char kUrl2Title[] = "url2"; +const char kUrl2Url[] = "http://www.url2.com"; +const char kUrl3Title[] = "url3"; +const char kUrl3Url[] = "http://www.url3.com"; +const char kUrl4Title[] = "url4"; +const char kUrl4Url[] = "http://www.url4.com"; +const char kFolder1Title[] = "folder1"; +const char kFolder2Title[] = "folder2"; + +const base::FilePath& GetTestDataDir() { + CR_DEFINE_STATIC_LOCAL(base::FilePath, dir, ()); + if (dir.empty()) { + PathService::Get(base::DIR_SOURCE_ROOT, &dir); + dir = dir.AppendASCII("components"); + dir = dir.AppendASCII("test"); + dir = dir.AppendASCII("data"); + } + return dir; +} + +// Helper to get a mutable bookmark node. +BookmarkNode* AsMutable(const BookmarkNode* node) { + return const_cast<BookmarkNode*>(node); +} + +// Helper to verify the two given bookmark nodes. +void AssertNodesEqual(const BookmarkNode* expected, + const BookmarkNode* actual) { + ASSERT_TRUE(expected); + ASSERT_TRUE(actual); + EXPECT_EQ(expected->id(), actual->id()); + EXPECT_EQ(expected->GetTitle(), actual->GetTitle()); + EXPECT_EQ(expected->type(), actual->type()); + EXPECT_TRUE(expected->date_added() == actual->date_added()); + if (expected->is_url()) { + EXPECT_EQ(expected->url(), actual->url()); + } else { + EXPECT_TRUE(expected->date_folder_modified() == + actual->date_folder_modified()); + ASSERT_EQ(expected->child_count(), actual->child_count()); + for (int i = 0; i < expected->child_count(); ++i) + AssertNodesEqual(expected->GetChild(i), actual->GetChild(i)); + } +} + +// Verifies that the two given bookmark models are the same. +void AssertModelsEqual(BookmarkModel* expected, BookmarkModel* actual) { + ASSERT_NO_FATAL_FAILURE(AssertNodesEqual(expected->bookmark_bar_node(), + actual->bookmark_bar_node())); + ASSERT_NO_FATAL_FAILURE( + AssertNodesEqual(expected->other_node(), actual->other_node())); + ASSERT_NO_FATAL_FAILURE( + AssertNodesEqual(expected->mobile_node(), actual->mobile_node())); +} + +} // namespace + +class BookmarkCodecTest : public testing::Test { + protected: + // Helpers to create bookmark models with different data. + BookmarkModel* CreateTestModel1() { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + const BookmarkNode* bookmark_bar = model->bookmark_bar_node(); + model->AddURL(bookmark_bar, 0, ASCIIToUTF16(kUrl1Title), GURL(kUrl1Url)); + return model.release(); + } + BookmarkModel* CreateTestModel2() { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + const BookmarkNode* bookmark_bar = model->bookmark_bar_node(); + model->AddURL(bookmark_bar, 0, ASCIIToUTF16(kUrl1Title), GURL(kUrl1Url)); + model->AddURL(bookmark_bar, 1, ASCIIToUTF16(kUrl2Title), GURL(kUrl2Url)); + return model.release(); + } + BookmarkModel* CreateTestModel3() { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + const BookmarkNode* bookmark_bar = model->bookmark_bar_node(); + model->AddURL(bookmark_bar, 0, ASCIIToUTF16(kUrl1Title), GURL(kUrl1Url)); + const BookmarkNode* folder1 = + model->AddFolder(bookmark_bar, 1, ASCIIToUTF16(kFolder1Title)); + model->AddURL(folder1, 0, ASCIIToUTF16(kUrl2Title), GURL(kUrl2Url)); + return model.release(); + } + + void GetBookmarksBarChildValue(base::Value* value, + size_t index, + base::DictionaryValue** result_value) { + ASSERT_EQ(base::Value::TYPE_DICTIONARY, value->GetType()); + + base::DictionaryValue* d_value = nullptr; + value->GetAsDictionary(&d_value); + base::Value* roots; + ASSERT_TRUE(d_value->Get(BookmarkCodec::kRootsKey, &roots)); + ASSERT_EQ(base::Value::TYPE_DICTIONARY, roots->GetType()); + + base::DictionaryValue* roots_d_value = nullptr; + roots->GetAsDictionary(&roots_d_value); + base::Value* bb_value; + ASSERT_TRUE( + roots_d_value->Get(BookmarkCodec::kRootFolderNameKey, &bb_value)); + ASSERT_EQ(base::Value::TYPE_DICTIONARY, bb_value->GetType()); + + base::DictionaryValue* bb_d_value = nullptr; + bb_value->GetAsDictionary(&bb_d_value); + base::Value* bb_children_value; + ASSERT_TRUE( + bb_d_value->Get(BookmarkCodec::kChildrenKey, &bb_children_value)); + ASSERT_EQ(base::Value::TYPE_LIST, bb_children_value->GetType()); + + base::ListValue* bb_children_l_value = nullptr; + bb_children_value->GetAsList(&bb_children_l_value); + base::Value* child_value; + ASSERT_TRUE(bb_children_l_value->Get(index, &child_value)); + ASSERT_EQ(base::Value::TYPE_DICTIONARY, child_value->GetType()); + + child_value->GetAsDictionary(result_value); + } + + base::Value* EncodeHelper(BookmarkModel* model, std::string* checksum) { + BookmarkCodec encoder; + // Computed and stored checksums should be empty. + EXPECT_EQ("", encoder.computed_checksum()); + EXPECT_EQ("", encoder.stored_checksum()); + + scoped_ptr<base::Value> value(encoder.Encode(model)); + const std::string& computed_checksum = encoder.computed_checksum(); + const std::string& stored_checksum = encoder.stored_checksum(); + + // Computed and stored checksums should not be empty and should be equal. + EXPECT_FALSE(computed_checksum.empty()); + EXPECT_FALSE(stored_checksum.empty()); + EXPECT_EQ(computed_checksum, stored_checksum); + + *checksum = computed_checksum; + return value.release(); + } + + bool Decode(BookmarkCodec* codec, + BookmarkModel* model, + const base::Value& value) { + int64_t max_id; + bool result = codec->Decode(AsMutable(model->bookmark_bar_node()), + AsMutable(model->other_node()), + AsMutable(model->mobile_node()), + &max_id, + value); + model->set_next_node_id(max_id); + AsMutable(model->root_node())->SetMetaInfoMap(codec->model_meta_info_map()); + AsMutable(model->root_node()) + ->set_sync_transaction_version(codec->model_sync_transaction_version()); + + return result; + } + + BookmarkModel* DecodeHelper(const base::Value& value, + const std::string& expected_stored_checksum, + std::string* computed_checksum, + bool expected_changes) { + BookmarkCodec decoder; + // Computed and stored checksums should be empty. + EXPECT_EQ("", decoder.computed_checksum()); + EXPECT_EQ("", decoder.stored_checksum()); + + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + EXPECT_TRUE(Decode(&decoder, model.get(), value)); + + *computed_checksum = decoder.computed_checksum(); + const std::string& stored_checksum = decoder.stored_checksum(); + + // Computed and stored checksums should not be empty. + EXPECT_FALSE(computed_checksum->empty()); + EXPECT_FALSE(stored_checksum.empty()); + + // Stored checksum should be as expected. + EXPECT_EQ(expected_stored_checksum, stored_checksum); + + // The two checksums should be equal if expected_changes is true; otherwise + // they should be different. + if (expected_changes) + EXPECT_NE(*computed_checksum, stored_checksum); + else + EXPECT_EQ(*computed_checksum, stored_checksum); + + return model.release(); + } + + void CheckIDs(const BookmarkNode* node, std::set<int64_t>* assigned_ids) { + DCHECK(node); + int64_t node_id = node->id(); + EXPECT_TRUE(assigned_ids->find(node_id) == assigned_ids->end()); + assigned_ids->insert(node_id); + for (int i = 0; i < node->child_count(); ++i) + CheckIDs(node->GetChild(i), assigned_ids); + } + + void ExpectIDsUnique(BookmarkModel* model) { + std::set<int64_t> assigned_ids; + CheckIDs(model->bookmark_bar_node(), &assigned_ids); + CheckIDs(model->other_node(), &assigned_ids); + CheckIDs(model->mobile_node(), &assigned_ids); + } +}; + +TEST_F(BookmarkCodecTest, ChecksumEncodeDecodeTest) { + scoped_ptr<BookmarkModel> model_to_encode(CreateTestModel1()); + std::string enc_checksum; + scoped_ptr<base::Value> value( + EncodeHelper(model_to_encode.get(), &enc_checksum)); + + EXPECT_TRUE(value.get() != NULL); + + std::string dec_checksum; + scoped_ptr<BookmarkModel> decoded_model( + DecodeHelper(*value.get(), enc_checksum, &dec_checksum, false)); +} + +TEST_F(BookmarkCodecTest, ChecksumEncodeIdenticalModelsTest) { + // Encode two identical models and make sure the check-sums are same as long + // as the data is the same. + scoped_ptr<BookmarkModel> model1(CreateTestModel1()); + std::string enc_checksum1; + scoped_ptr<base::Value> value1(EncodeHelper(model1.get(), &enc_checksum1)); + EXPECT_TRUE(value1.get() != NULL); + + scoped_ptr<BookmarkModel> model2(CreateTestModel1()); + std::string enc_checksum2; + scoped_ptr<base::Value> value2(EncodeHelper(model2.get(), &enc_checksum2)); + EXPECT_TRUE(value2.get() != NULL); + + ASSERT_EQ(enc_checksum1, enc_checksum2); +} + +TEST_F(BookmarkCodecTest, ChecksumManualEditTest) { + scoped_ptr<BookmarkModel> model_to_encode(CreateTestModel1()); + std::string enc_checksum; + scoped_ptr<base::Value> value( + EncodeHelper(model_to_encode.get(), &enc_checksum)); + + EXPECT_TRUE(value.get() != NULL); + + // Change something in the encoded value before decoding it. + base::DictionaryValue* child1_value; + GetBookmarksBarChildValue(value.get(), 0, &child1_value); + std::string title; + ASSERT_TRUE(child1_value->GetString(BookmarkCodec::kNameKey, &title)); + child1_value->SetString(BookmarkCodec::kNameKey, title + "1"); + + std::string dec_checksum; + scoped_ptr<BookmarkModel> decoded_model1( + DecodeHelper(*value.get(), enc_checksum, &dec_checksum, true)); + + // Undo the change and make sure the checksum is same as original. + child1_value->SetString(BookmarkCodec::kNameKey, title); + scoped_ptr<BookmarkModel> decoded_model2( + DecodeHelper(*value.get(), enc_checksum, &dec_checksum, false)); +} + +TEST_F(BookmarkCodecTest, ChecksumManualEditIDsTest) { + scoped_ptr<BookmarkModel> model_to_encode(CreateTestModel3()); + + // The test depends on existence of multiple children under bookmark bar, so + // make sure that's the case. + int bb_child_count = model_to_encode->bookmark_bar_node()->child_count(); + ASSERT_GT(bb_child_count, 1); + + std::string enc_checksum; + scoped_ptr<base::Value> value( + EncodeHelper(model_to_encode.get(), &enc_checksum)); + + EXPECT_TRUE(value.get() != NULL); + + // Change IDs for all children of bookmark bar to be 1. + base::DictionaryValue* child_value; + for (int i = 0; i < bb_child_count; ++i) { + GetBookmarksBarChildValue(value.get(), i, &child_value); + std::string id; + ASSERT_TRUE(child_value->GetString(BookmarkCodec::kIdKey, &id)); + child_value->SetString(BookmarkCodec::kIdKey, "1"); + } + + std::string dec_checksum; + scoped_ptr<BookmarkModel> decoded_model( + DecodeHelper(*value.get(), enc_checksum, &dec_checksum, true)); + + ExpectIDsUnique(decoded_model.get()); + + // add a few extra nodes to bookmark model and make sure IDs are still uniuqe. + const BookmarkNode* bb_node = decoded_model->bookmark_bar_node(); + decoded_model->AddURL( + bb_node, 0, ASCIIToUTF16("new url1"), GURL("http://newurl1.com")); + decoded_model->AddURL( + bb_node, 0, ASCIIToUTF16("new url2"), GURL("http://newurl2.com")); + + ExpectIDsUnique(decoded_model.get()); +} + +TEST_F(BookmarkCodecTest, PersistIDsTest) { + scoped_ptr<BookmarkModel> model_to_encode(CreateTestModel3()); + BookmarkCodec encoder; + scoped_ptr<base::Value> model_value(encoder.Encode(model_to_encode.get())); + + scoped_ptr<BookmarkModel> decoded_model(TestBookmarkClient::CreateModel()); + BookmarkCodec decoder; + ASSERT_TRUE(Decode(&decoder, decoded_model.get(), *model_value.get())); + ASSERT_NO_FATAL_FAILURE( + AssertModelsEqual(model_to_encode.get(), decoded_model.get())); + + // Add a couple of more items to the decoded bookmark model and make sure + // ID persistence is working properly. + const BookmarkNode* bookmark_bar = decoded_model->bookmark_bar_node(); + decoded_model->AddURL(bookmark_bar, + bookmark_bar->child_count(), + ASCIIToUTF16(kUrl3Title), + GURL(kUrl3Url)); + const BookmarkNode* folder2_node = decoded_model->AddFolder( + bookmark_bar, bookmark_bar->child_count(), ASCIIToUTF16(kFolder2Title)); + decoded_model->AddURL( + folder2_node, 0, ASCIIToUTF16(kUrl4Title), GURL(kUrl4Url)); + + BookmarkCodec encoder2; + scoped_ptr<base::Value> model_value2(encoder2.Encode(decoded_model.get())); + + scoped_ptr<BookmarkModel> decoded_model2(TestBookmarkClient::CreateModel()); + BookmarkCodec decoder2; + ASSERT_TRUE(Decode(&decoder2, decoded_model2.get(), *model_value2.get())); + ASSERT_NO_FATAL_FAILURE( + AssertModelsEqual(decoded_model.get(), decoded_model2.get())); +} + +TEST_F(BookmarkCodecTest, CanDecodeModelWithoutMobileBookmarks) { + base::FilePath test_file = + GetTestDataDir().AppendASCII("bookmarks/model_without_sync.json"); + ASSERT_TRUE(base::PathExists(test_file)); + + JSONFileValueDeserializer deserializer(test_file); + scoped_ptr<base::Value> root = deserializer.Deserialize(NULL, NULL); + + scoped_ptr<BookmarkModel> decoded_model(TestBookmarkClient::CreateModel()); + BookmarkCodec decoder; + ASSERT_TRUE(Decode(&decoder, decoded_model.get(), *root.get())); + ExpectIDsUnique(decoded_model.get()); + + const BookmarkNode* bbn = decoded_model->bookmark_bar_node(); + ASSERT_EQ(1, bbn->child_count()); + + const BookmarkNode* child = bbn->GetChild(0); + EXPECT_EQ(BookmarkNode::FOLDER, child->type()); + EXPECT_EQ(ASCIIToUTF16("Folder A"), child->GetTitle()); + ASSERT_EQ(1, child->child_count()); + + child = child->GetChild(0); + EXPECT_EQ(BookmarkNode::URL, child->type()); + EXPECT_EQ(ASCIIToUTF16("Bookmark Manager"), child->GetTitle()); + + const BookmarkNode* other = decoded_model->other_node(); + ASSERT_EQ(1, other->child_count()); + + child = other->GetChild(0); + EXPECT_EQ(BookmarkNode::FOLDER, child->type()); + EXPECT_EQ(ASCIIToUTF16("Folder B"), child->GetTitle()); + ASSERT_EQ(1, child->child_count()); + + child = child->GetChild(0); + EXPECT_EQ(BookmarkNode::URL, child->type()); + EXPECT_EQ(ASCIIToUTF16("Get started with Google Chrome"), child->GetTitle()); + + ASSERT_TRUE(decoded_model->mobile_node() != NULL); +} + +TEST_F(BookmarkCodecTest, EncodeAndDecodeMetaInfo) { + // Add meta info and encode. + scoped_ptr<BookmarkModel> model(CreateTestModel1()); + model->SetNodeMetaInfo(model->root_node(), "model_info", "value1"); + model->SetNodeMetaInfo( + model->bookmark_bar_node()->GetChild(0), "node_info", "value2"); + std::string checksum; + scoped_ptr<base::Value> value(EncodeHelper(model.get(), &checksum)); + ASSERT_TRUE(value.get() != NULL); + + // Decode and check for meta info. + model.reset(DecodeHelper(*value, checksum, &checksum, false)); + std::string meta_value; + EXPECT_TRUE(model->root_node()->GetMetaInfo("model_info", &meta_value)); + EXPECT_EQ("value1", meta_value); + EXPECT_FALSE(model->root_node()->GetMetaInfo("other_key", &meta_value)); + const BookmarkNode* bbn = model->bookmark_bar_node(); + ASSERT_EQ(1, bbn->child_count()); + const BookmarkNode* child = bbn->GetChild(0); + EXPECT_TRUE(child->GetMetaInfo("node_info", &meta_value)); + EXPECT_EQ("value2", meta_value); + EXPECT_FALSE(child->GetMetaInfo("other_key", &meta_value)); +} + +TEST_F(BookmarkCodecTest, EncodeAndDecodeSyncTransactionVersion) { + // Add sync transaction version and encode. + scoped_ptr<BookmarkModel> model(CreateTestModel2()); + model->SetNodeSyncTransactionVersion(model->root_node(), 1); + const BookmarkNode* bbn = model->bookmark_bar_node(); + model->SetNodeSyncTransactionVersion(bbn->GetChild(1), 42); + + std::string checksum; + scoped_ptr<base::Value> value(EncodeHelper(model.get(), &checksum)); + ASSERT_TRUE(value.get() != NULL); + + // Decode and verify. + model.reset(DecodeHelper(*value, checksum, &checksum, false)); + EXPECT_EQ(1, model->root_node()->sync_transaction_version()); + bbn = model->bookmark_bar_node(); + EXPECT_EQ(42, bbn->GetChild(1)->sync_transaction_version()); + EXPECT_EQ(BookmarkNode::kInvalidSyncTransactionVersion, + bbn->GetChild(0)->sync_transaction_version()); +} + +// Verifies that we can still decode the old codec format after changing the +// way meta info is stored. +TEST_F(BookmarkCodecTest, CanDecodeMetaInfoAsString) { + base::FilePath test_file = + GetTestDataDir().AppendASCII("bookmarks/meta_info_as_string.json"); + ASSERT_TRUE(base::PathExists(test_file)); + + JSONFileValueDeserializer deserializer(test_file); + scoped_ptr<base::Value> root = deserializer.Deserialize(NULL, NULL); + + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + BookmarkCodec decoder; + ASSERT_TRUE(Decode(&decoder, model.get(), *root.get())); + + EXPECT_EQ(1, model->root_node()->sync_transaction_version()); + const BookmarkNode* bbn = model->bookmark_bar_node(); + EXPECT_EQ(BookmarkNode::kInvalidSyncTransactionVersion, + bbn->GetChild(0)->sync_transaction_version()); + EXPECT_EQ(42, bbn->GetChild(1)->sync_transaction_version()); + + const char kSyncTransactionVersionKey[] = "sync.transaction_version"; + const char kNormalKey[] = "key"; + const char kNestedKey[] = "nested.key"; + std::string meta_value; + EXPECT_FALSE( + model->root_node()->GetMetaInfo(kSyncTransactionVersionKey, &meta_value)); + EXPECT_FALSE( + bbn->GetChild(1)->GetMetaInfo(kSyncTransactionVersionKey, &meta_value)); + EXPECT_TRUE(bbn->GetChild(0)->GetMetaInfo(kNormalKey, &meta_value)); + EXPECT_EQ("value", meta_value); + EXPECT_TRUE(bbn->GetChild(1)->GetMetaInfo(kNormalKey, &meta_value)); + EXPECT_EQ("value2", meta_value); + EXPECT_TRUE(bbn->GetChild(0)->GetMetaInfo(kNestedKey, &meta_value)); + EXPECT_EQ("value3", meta_value); +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_expanded_state_tracker.cc b/chromium/components/bookmarks/browser/bookmark_expanded_state_tracker.cc new file mode 100644 index 00000000000..427f2d3c446 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_expanded_state_tracker.cc @@ -0,0 +1,116 @@ +// 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/bookmarks/browser/bookmark_expanded_state_tracker.h" + +#include <stdint.h> + +#include "base/strings/string_number_conversions.h" +#include "base/values.h" +#include "components/bookmarks/browser/bookmark_model.h" +#include "components/bookmarks/browser/bookmark_utils.h" +#include "components/bookmarks/common/bookmark_pref_names.h" +#include "components/prefs/pref_service.h" + +namespace bookmarks { + +BookmarkExpandedStateTracker::BookmarkExpandedStateTracker( + BookmarkModel* bookmark_model, + PrefService* pref_service) + : bookmark_model_(bookmark_model), + pref_service_(pref_service) { + bookmark_model->AddObserver(this); +} + +BookmarkExpandedStateTracker::~BookmarkExpandedStateTracker() { +} + +void BookmarkExpandedStateTracker::SetExpandedNodes(const Nodes& nodes) { + UpdatePrefs(nodes); +} + +BookmarkExpandedStateTracker::Nodes +BookmarkExpandedStateTracker::GetExpandedNodes() { + Nodes nodes; + if (!bookmark_model_->loaded()) + return nodes; + + if (!pref_service_) + return nodes; + + const base::ListValue* value = + pref_service_->GetList(prefs::kBookmarkEditorExpandedNodes); + if (!value) + return nodes; + + bool changed = false; + for (base::ListValue::const_iterator i = value->begin(); + i != value->end(); ++i) { + std::string value; + int64_t node_id; + const BookmarkNode* node; + if ((*i)->GetAsString(&value) && base::StringToInt64(value, &node_id) && + (node = GetBookmarkNodeByID(bookmark_model_, node_id)) != NULL && + node->is_folder()) { + nodes.insert(node); + } else { + changed = true; + } + } + if (changed) + UpdatePrefs(nodes); + return nodes; +} + +void BookmarkExpandedStateTracker::BookmarkModelLoaded(BookmarkModel* model, + bool ids_reassigned) { + if (ids_reassigned) { + // If the ids change we can't trust the value in preferences and need to + // reset it. + SetExpandedNodes(Nodes()); + } +} + +void BookmarkExpandedStateTracker::BookmarkModelChanged() { +} + +void BookmarkExpandedStateTracker::BookmarkModelBeingDeleted( + BookmarkModel* model) { + model->RemoveObserver(this); +} + +void BookmarkExpandedStateTracker::BookmarkNodeRemoved( + BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node, + const std::set<GURL>& removed_urls) { + if (!node->is_folder()) + return; // Only care about folders. + + // Ask for the nodes again, which removes any nodes that were deleted. + GetExpandedNodes(); +} + +void BookmarkExpandedStateTracker::BookmarkAllUserNodesRemoved( + BookmarkModel* model, + const std::set<GURL>& removed_urls) { + // Ask for the nodes again, which removes any nodes that were deleted. + GetExpandedNodes(); +} + +void BookmarkExpandedStateTracker::UpdatePrefs(const Nodes& nodes) { + if (!pref_service_) + return; + + base::ListValue values; + for (Nodes::const_iterator i = nodes.begin(); i != nodes.end(); ++i) { + values.Set(values.GetSize(), + new base::StringValue(base::Int64ToString((*i)->id()))); + } + + pref_service_->Set(prefs::kBookmarkEditorExpandedNodes, values); +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_expanded_state_tracker.h b/chromium/components/bookmarks/browser/bookmark_expanded_state_tracker.h new file mode 100644 index 00000000000..92f8d5f5f3f --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_expanded_state_tracker.h @@ -0,0 +1,60 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_EXPANDED_STATE_TRACKER_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_EXPANDED_STATE_TRACKER_H_ + +#include <set> + +#include "base/macros.h" +#include "components/bookmarks/browser/base_bookmark_model_observer.h" + +class PrefService; + +namespace bookmarks { + +class BookmarkModel; +class BookmarkNode; + +// BookmarkExpandedStateTracker is used to track a set of expanded nodes. The +// nodes are persisted in preferences. If an expanded node is removed from the +// model BookmarkExpandedStateTracker removes the node. +class BookmarkExpandedStateTracker : public BaseBookmarkModelObserver { + public: + typedef std::set<const BookmarkNode*> Nodes; + + BookmarkExpandedStateTracker(BookmarkModel* bookmark_model, + PrefService* pref_service); + ~BookmarkExpandedStateTracker() override; + + // The set of expanded nodes. + void SetExpandedNodes(const Nodes& nodes); + Nodes GetExpandedNodes(); + + private: + // BaseBookmarkModelObserver: + void BookmarkModelLoaded(BookmarkModel* model, bool ids_reassigned) override; + void BookmarkModelChanged() override; + void BookmarkModelBeingDeleted(BookmarkModel* model) override; + void BookmarkNodeRemoved(BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node, + const std::set<GURL>& removed_urls) override; + void BookmarkAllUserNodesRemoved(BookmarkModel* model, + const std::set<GURL>& removed_urls) override; + + // Updates the value for |prefs::kBookmarkEditorExpandedNodes| from + // GetExpandedNodes(). + void UpdatePrefs(const Nodes& nodes); + + BookmarkModel* bookmark_model_; + PrefService* pref_service_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkExpandedStateTracker); +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_EXPANDED_STATE_TRACKER_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_expanded_state_tracker_unittest.cc b/chromium/components/bookmarks/browser/bookmark_expanded_state_tracker_unittest.cc new file mode 100644 index 00000000000..76c0160f3e6 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_expanded_state_tracker_unittest.cc @@ -0,0 +1,102 @@ +// 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/bookmarks/browser/bookmark_expanded_state_tracker.h" + +#include "base/files/file_path.h" +#include "base/macros.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/utf_string_conversions.h" +#include "base/thread_task_runner_handle.h" +#include "components/bookmarks/browser/bookmark_model.h" +#include "components/bookmarks/common/bookmark_pref_names.h" +#include "components/bookmarks/test/bookmark_test_helpers.h" +#include "components/bookmarks/test/test_bookmark_client.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/testing_pref_service.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace bookmarks { + +class BookmarkExpandedStateTrackerTest : public testing::Test { + public: + BookmarkExpandedStateTrackerTest(); + ~BookmarkExpandedStateTrackerTest() override; + + protected: + // testing::Test: + void SetUp() override; + void TearDown() override; + + base::MessageLoop message_loop_; + TestingPrefServiceSimple prefs_; + scoped_ptr<BookmarkModel> model_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkExpandedStateTrackerTest); +}; + +BookmarkExpandedStateTrackerTest::BookmarkExpandedStateTrackerTest() {} + +BookmarkExpandedStateTrackerTest::~BookmarkExpandedStateTrackerTest() {} + +void BookmarkExpandedStateTrackerTest::SetUp() { + prefs_.registry()->RegisterListPref(prefs::kBookmarkEditorExpandedNodes, + new base::ListValue); + prefs_.registry()->RegisterListPref(prefs::kManagedBookmarks); + model_.reset(new BookmarkModel(make_scoped_ptr(new TestBookmarkClient()))); + model_->Load(&prefs_, base::FilePath(), + base::ThreadTaskRunnerHandle::Get(), + base::ThreadTaskRunnerHandle::Get()); + test::WaitForBookmarkModelToLoad(model_.get()); +} + +void BookmarkExpandedStateTrackerTest::TearDown() { + model_.reset(); + message_loop_.RunUntilIdle(); +} + +// Various assertions for SetExpandedNodes. +TEST_F(BookmarkExpandedStateTrackerTest, SetExpandedNodes) { + BookmarkExpandedStateTracker* tracker = model_->expanded_state_tracker(); + + // Should start out initially empty. + EXPECT_TRUE(tracker->GetExpandedNodes().empty()); + + BookmarkExpandedStateTracker::Nodes nodes; + nodes.insert(model_->bookmark_bar_node()); + tracker->SetExpandedNodes(nodes); + EXPECT_EQ(nodes, tracker->GetExpandedNodes()); + + // Add a folder and mark it expanded. + const BookmarkNode* n1 = model_->AddFolder( + model_->bookmark_bar_node(), 0, base::ASCIIToUTF16("x")); + nodes.insert(n1); + tracker->SetExpandedNodes(nodes); + EXPECT_EQ(nodes, tracker->GetExpandedNodes()); + + // Remove the folder, which should remove it from the list of expanded nodes. + model_->Remove(model_->bookmark_bar_node()->GetChild(0)); + nodes.erase(n1); + n1 = NULL; + EXPECT_EQ(nodes, tracker->GetExpandedNodes()); +} + +TEST_F(BookmarkExpandedStateTrackerTest, RemoveAllUserBookmarks) { + BookmarkExpandedStateTracker* tracker = model_->expanded_state_tracker(); + + // Add a folder and mark it expanded. + const BookmarkNode* n1 = model_->AddFolder( + model_->bookmark_bar_node(), 0, base::ASCIIToUTF16("x")); + BookmarkExpandedStateTracker::Nodes nodes; + nodes.insert(n1); + tracker->SetExpandedNodes(nodes); + // Verify that the node is present. + EXPECT_EQ(nodes, tracker->GetExpandedNodes()); + // Call remove all. + model_->RemoveAllUserBookmarks(); + // Verify node is not present. + EXPECT_TRUE(tracker->GetExpandedNodes().empty()); +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_index.cc b/chromium/components/bookmarks/browser/bookmark_index.cc new file mode 100644 index 00000000000..f054022abe4 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_index.cc @@ -0,0 +1,293 @@ +// 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/bookmarks/browser/bookmark_index.h" + +#include <stdint.h> + +#include <algorithm> +#include <functional> +#include <iterator> +#include <list> + +#include "base/i18n/case_conversion.h" +#include "base/logging.h" +#include "base/stl_util.h" +#include "base/strings/utf_offset_string_conversions.h" +#include "build/build_config.h" +#include "components/bookmarks/browser/bookmark_client.h" +#include "components/bookmarks/browser/bookmark_match.h" +#include "components/bookmarks/browser/bookmark_node.h" +#include "components/bookmarks/browser/bookmark_utils.h" +#include "components/query_parser/snippet.h" +#include "third_party/icu/source/common/unicode/normalizer2.h" +#include "third_party/icu/source/common/unicode/utypes.h" + +namespace bookmarks { + +typedef BookmarkClient::NodeTypedCountPair NodeTypedCountPair; +typedef BookmarkClient::NodeTypedCountPairs NodeTypedCountPairs; + +namespace { + +// Returns a normalized version of the UTF16 string |text|. If it fails to +// normalize the string, returns |text| itself as a best-effort. +base::string16 Normalize(const base::string16& text) { + UErrorCode status = U_ZERO_ERROR; + const icu::Normalizer2* normalizer2 = + icu::Normalizer2::getInstance(nullptr, "nfkc", UNORM2_COMPOSE, status); + if (U_FAILURE(status)) { + // Log and crash right away to capture the error code in the crash report. + LOG(FATAL) << "failed to create a normalizer: " << u_errorName(status); + } + icu::UnicodeString unicode_text( + text.data(), static_cast<int32_t>(text.length())); + icu::UnicodeString unicode_normalized_text; + normalizer2->normalize(unicode_text, unicode_normalized_text, status); + if (U_FAILURE(status)) { + // This should not happen. Log the error and fall back. + LOG(ERROR) << "normalization failed: " << u_errorName(status); + return text; + } + return base::string16(unicode_normalized_text.getBuffer(), + unicode_normalized_text.length()); +} + +// Sort functor for NodeTypedCountPairs. We sort in decreasing order of typed +// count so that the best matches will always be added to the results. +struct NodeTypedCountPairSortFunctor { + bool operator()(const NodeTypedCountPair& a, + const NodeTypedCountPair& b) const { + return a.second > b.second; + } +}; + +// Extract the const Node* stored in a BookmarkClient::NodeTypedCountPair. +struct NodeTypedCountPairExtractNodeFunctor { + const BookmarkNode* operator()(const NodeTypedCountPair& pair) const { + return pair.first; + } +}; + +} // namespace + +BookmarkIndex::BookmarkIndex(BookmarkClient* client) + : client_(client) { + DCHECK(client_); +} + +BookmarkIndex::~BookmarkIndex() { +} + +void BookmarkIndex::Add(const BookmarkNode* node) { + if (!node->is_url()) + return; + std::vector<base::string16> terms = + ExtractQueryWords(Normalize(node->GetTitle())); + for (size_t i = 0; i < terms.size(); ++i) + RegisterNode(terms[i], node); + terms = + ExtractQueryWords(CleanUpUrlForMatching(node->url(), nullptr)); + for (size_t i = 0; i < terms.size(); ++i) + RegisterNode(terms[i], node); +} + +void BookmarkIndex::Remove(const BookmarkNode* node) { + if (!node->is_url()) + return; + + std::vector<base::string16> terms = + ExtractQueryWords(Normalize(node->GetTitle())); + for (size_t i = 0; i < terms.size(); ++i) + UnregisterNode(terms[i], node); + terms = + ExtractQueryWords(CleanUpUrlForMatching(node->url(), nullptr)); + for (size_t i = 0; i < terms.size(); ++i) + UnregisterNode(terms[i], node); +} + +void BookmarkIndex::GetBookmarksMatching( + const base::string16& input_query, + size_t max_count, + query_parser::MatchingAlgorithm matching_algorithm, + std::vector<BookmarkMatch>* results) { + const base::string16 query = Normalize(input_query); + std::vector<base::string16> terms = ExtractQueryWords(query); + if (terms.empty()) + return; + + NodeSet matches; + for (size_t i = 0; i < terms.size(); ++i) { + if (!GetBookmarksMatchingTerm( + terms[i], i == 0, matching_algorithm, &matches)) { + return; + } + } + + Nodes sorted_nodes; + SortMatches(matches, &sorted_nodes); + + // We use a QueryParser to fill in match positions for us. It's not the most + // efficient way to go about this, but by the time we get here we know what + // matches and so this shouldn't be performance critical. + query_parser::QueryParser parser; + ScopedVector<query_parser::QueryNode> query_nodes; + parser.ParseQueryNodes(query, matching_algorithm, &query_nodes.get()); + + // The highest typed counts should be at the beginning of the results vector + // so that the best matches will always be included in the results. The loop + // that calculates result relevance in HistoryContentsProvider::ConvertResults + // will run backwards to assure higher relevance will be attributed to the + // best matches. + for (Nodes::const_iterator i = sorted_nodes.begin(); + i != sorted_nodes.end() && results->size() < max_count; + ++i) + AddMatchToResults(*i, &parser, query_nodes.get(), results); +} + +void BookmarkIndex::SortMatches(const NodeSet& matches, + Nodes* sorted_nodes) const { + sorted_nodes->reserve(matches.size()); + if (client_->SupportsTypedCountForNodes()) { + NodeTypedCountPairs node_typed_counts; + client_->GetTypedCountForNodes(matches, &node_typed_counts); + std::sort(node_typed_counts.begin(), + node_typed_counts.end(), + NodeTypedCountPairSortFunctor()); + std::transform(node_typed_counts.begin(), + node_typed_counts.end(), + std::back_inserter(*sorted_nodes), + NodeTypedCountPairExtractNodeFunctor()); + } else { + sorted_nodes->insert(sorted_nodes->end(), matches.begin(), matches.end()); + } +} + +void BookmarkIndex::AddMatchToResults( + const BookmarkNode* node, + query_parser::QueryParser* parser, + const query_parser::QueryNodeStarVector& query_nodes, + std::vector<BookmarkMatch>* results) { + // Check that the result matches the query. The previous search + // was a simple per-word search, while the more complex matching + // of QueryParser may filter it out. For example, the query + // ["thi"] will match the bookmark titled [Thinking], but since + // ["thi"] is quoted we don't want to do a prefix match. + query_parser::QueryWordVector title_words, url_words; + const base::string16 lower_title = + base::i18n::ToLower(Normalize(node->GetTitle())); + parser->ExtractQueryWords(lower_title, &title_words); + base::OffsetAdjuster::Adjustments adjustments; + parser->ExtractQueryWords( + CleanUpUrlForMatching(node->url(), &adjustments), + &url_words); + query_parser::Snippet::MatchPositions title_matches, url_matches; + for (size_t i = 0; i < query_nodes.size(); ++i) { + const bool has_title_matches = + query_nodes[i]->HasMatchIn(title_words, &title_matches); + const bool has_url_matches = + query_nodes[i]->HasMatchIn(url_words, &url_matches); + if (!has_title_matches && !has_url_matches) + return; + query_parser::QueryParser::SortAndCoalesceMatchPositions(&title_matches); + query_parser::QueryParser::SortAndCoalesceMatchPositions(&url_matches); + } + BookmarkMatch match; + if (lower_title.length() == node->GetTitle().length()) { + // Only use title matches if the lowercase string is the same length + // as the original string, otherwise the matches are meaningless. + // TODO(mpearson): revise match positions appropriately. + match.title_match_positions.swap(title_matches); + } + // Now that we're done processing this entry, correct the offsets of the + // matches in |url_matches| so they point to offsets in the original URL + // spec, not the cleaned-up URL string that we used for matching. + std::vector<size_t> offsets = + BookmarkMatch::OffsetsFromMatchPositions(url_matches); + base::OffsetAdjuster::UnadjustOffsets(adjustments, &offsets); + url_matches = + BookmarkMatch::ReplaceOffsetsInMatchPositions(url_matches, offsets); + match.url_match_positions.swap(url_matches); + match.node = node; + results->push_back(match); +} + +bool BookmarkIndex::GetBookmarksMatchingTerm( + const base::string16& term, + bool first_term, + query_parser::MatchingAlgorithm matching_algorithm, + NodeSet* matches) { + Index::const_iterator i = index_.lower_bound(term); + if (i == index_.end()) + return false; + + if (!query_parser::QueryParser::IsWordLongEnoughForPrefixSearch( + term, matching_algorithm)) { + // Term is too short for prefix match, compare using exact match. + if (i->first != term) + return false; // No bookmarks with this term. + + if (first_term) { + (*matches) = i->second; + return true; + } + *matches = base::STLSetIntersection<NodeSet>(i->second, *matches); + } else { + // Loop through index adding all entries that start with term to + // |prefix_matches|. + NodeSet tmp_prefix_matches; + // If this is the first term, then store the result directly in |matches| + // to avoid calling stl intersection (which requires a copy). + NodeSet* prefix_matches = first_term ? matches : &tmp_prefix_matches; + while (i != index_.end() && + i->first.size() >= term.size() && + term.compare(0, term.size(), i->first, 0, term.size()) == 0) { +#if !defined(OS_ANDROID) + prefix_matches->insert(i->second.begin(), i->second.end()); +#else + // Work around a bug in the implementation of std::set::insert in the STL + // used on android (http://crbug.com/367050). + for (NodeSet::const_iterator n = i->second.begin(); n != i->second.end(); + ++n) + prefix_matches->insert(prefix_matches->end(), *n); +#endif + ++i; + } + if (!first_term) + *matches = base::STLSetIntersection<NodeSet>(*prefix_matches, *matches); + } + return !matches->empty(); +} + +std::vector<base::string16> BookmarkIndex::ExtractQueryWords( + const base::string16& query) { + std::vector<base::string16> terms; + if (query.empty()) + return std::vector<base::string16>(); + query_parser::QueryParser parser; + parser.ParseQueryWords(base::i18n::ToLower(query), + query_parser::MatchingAlgorithm::DEFAULT, + &terms); + return terms; +} + +void BookmarkIndex::RegisterNode(const base::string16& term, + const BookmarkNode* node) { + index_[term].insert(node); +} + +void BookmarkIndex::UnregisterNode(const base::string16& term, + const BookmarkNode* node) { + Index::iterator i = index_.find(term); + if (i == index_.end()) { + // We can get here if the node has the same term more than once. For + // example, a bookmark with the title 'foo foo' would end up here. + return; + } + i->second.erase(node); + if (i->second.empty()) + index_.erase(i); +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_index.h b/chromium/components/bookmarks/browser/bookmark_index.h new file mode 100644 index 00000000000..a5f9bbfe519 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_index.h @@ -0,0 +1,94 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_INDEX_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_INDEX_H_ + +#include <stddef.h> + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "base/macros.h" +#include "base/strings/string16.h" +#include "components/query_parser/query_parser.h" + +namespace bookmarks { + +class BookmarkClient; +class BookmarkNode; +struct BookmarkMatch; + +// BookmarkIndex maintains an index of the titles and URLs of bookmarks for +// quick look up. BookmarkIndex is owned and maintained by BookmarkModel, you +// shouldn't need to interact directly with BookmarkIndex. +// +// BookmarkIndex maintains the index (index_) as a map of sets. The map (type +// Index) maps from a lower case string to the set (type NodeSet) of +// BookmarkNodes that contain that string in their title or URL. +class BookmarkIndex { + public: + BookmarkIndex(BookmarkClient* client); + ~BookmarkIndex(); + + // Invoked when a bookmark has been added to the model. + void Add(const BookmarkNode* node); + + // Invoked when a bookmark has been removed from the model. + void Remove(const BookmarkNode* node); + + // Returns up to |max_count| of bookmarks containing each term from the text + // |query| in either the title or the URL. + void GetBookmarksMatching(const base::string16& query, + size_t max_count, + query_parser::MatchingAlgorithm matching_algorithm, + std::vector<BookmarkMatch>* results); + + private: + typedef std::vector<const BookmarkNode*> Nodes; + typedef std::set<const BookmarkNode*> NodeSet; + typedef std::map<base::string16, NodeSet> Index; + + // Constructs |sorted_nodes| by taking the matches in |matches| and sorting + // them in decreasing order of typed count (if supported by the client) and + // deduping them. + void SortMatches(const NodeSet& matches, Nodes* sorted_nodes) const; + + // Add |node| to |results| if the node matches the query. + void AddMatchToResults( + const BookmarkNode* node, + query_parser::QueryParser* parser, + const query_parser::QueryNodeStarVector& query_nodes, + std::vector<BookmarkMatch>* results); + + // Populates |matches| for the specified term. If |first_term| is true, this + // is the first term in the query. Returns true if there is at least one node + // matching the term. + bool GetBookmarksMatchingTerm( + const base::string16& term, + bool first_term, + query_parser::MatchingAlgorithm matching_algorithm, + NodeSet* matches); + + // Returns the set of query words from |query|. + std::vector<base::string16> ExtractQueryWords(const base::string16& query); + + // Adds |node| to |index_|. + void RegisterNode(const base::string16& term, const BookmarkNode* node); + + // Removes |node| from |index_|. + void UnregisterNode(const base::string16& term, const BookmarkNode* node); + + Index index_; + + BookmarkClient* const client_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkIndex); +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_INDEX_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_index_unittest.cc b/chromium/components/bookmarks/browser/bookmark_index_unittest.cc new file mode 100644 index 00000000000..8a365a4d1b1 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_index_unittest.cc @@ -0,0 +1,562 @@ +// 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/bookmarks/browser/bookmark_index.h" + +#include <stddef.h> + +#include <string> +#include <vector> + +#include "base/macros.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "components/bookmarks/browser/bookmark_match.h" +#include "components/bookmarks/browser/bookmark_model.h" +#include "components/bookmarks/test/bookmark_test_helpers.h" +#include "components/bookmarks/test/test_bookmark_client.h" +#include "testing/gtest/include/gtest/gtest.h" + +using base::ASCIIToUTF16; +using base::UTF8ToUTF16; + +namespace bookmarks { +namespace { + +const char kAboutBlankURL[] = "about:blank"; + +class BookmarkClientMock : public TestBookmarkClient { + public: + BookmarkClientMock(const std::map<GURL, int>& typed_count_map) + : typed_count_map_(typed_count_map) {} + + bool SupportsTypedCountForNodes() override { return true; } + + void GetTypedCountForNodes( + const NodeSet& nodes, + NodeTypedCountPairs* node_typed_count_pairs) override { + for (NodeSet::const_iterator it = nodes.begin(); it != nodes.end(); ++it) { + const BookmarkNode* node = *it; + std::map<GURL, int>::const_iterator found = + typed_count_map_.find(node->url()); + if (found == typed_count_map_.end()) + continue; + + node_typed_count_pairs->push_back(std::make_pair(node, found->second)); + } + } + + private: + const std::map<GURL, int> typed_count_map_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkClientMock); +}; + +class BookmarkIndexTest : public testing::Test { + public: + BookmarkIndexTest() : model_(TestBookmarkClient::CreateModel()) {} + + typedef std::pair<std::string, std::string> TitleAndURL; + + void AddBookmarks(const char** titles, const char** urls, size_t count) { + // The pair is (title, url). + std::vector<TitleAndURL> bookmarks; + for (size_t i = 0; i < count; ++i) { + TitleAndURL bookmark(titles[i], urls[i]); + bookmarks.push_back(bookmark); + } + AddBookmarks(bookmarks); + } + + void AddBookmarks(const std::vector<TitleAndURL>& bookmarks) { + for (size_t i = 0; i < bookmarks.size(); ++i) { + model_->AddURL(model_->other_node(), static_cast<int>(i), + ASCIIToUTF16(bookmarks[i].first), + GURL(bookmarks[i].second)); + } + } + + void ExpectMatches(const std::string& query, + const char** expected_titles, + size_t expected_count) { + std::vector<std::string> title_vector; + for (size_t i = 0; i < expected_count; ++i) + title_vector.push_back(expected_titles[i]); + ExpectMatches(query, query_parser::MatchingAlgorithm::DEFAULT, + title_vector); + } + + void ExpectMatches(const std::string& query, + query_parser::MatchingAlgorithm matching_algorithm, + const std::vector<std::string>& expected_titles) { + std::vector<BookmarkMatch> matches; + model_->GetBookmarksMatching(ASCIIToUTF16(query), 1000, matching_algorithm, + &matches); + ASSERT_EQ(expected_titles.size(), matches.size()); + for (size_t i = 0; i < expected_titles.size(); ++i) { + bool found = false; + for (size_t j = 0; j < matches.size(); ++j) { + if (ASCIIToUTF16(expected_titles[i]) == matches[j].node->GetTitle()) { + matches.erase(matches.begin() + j); + found = true; + break; + } + } + ASSERT_TRUE(found); + } + } + + void ExtractMatchPositions(const std::string& string, + BookmarkMatch::MatchPositions* matches) { + for (const base::StringPiece& match : + base::SplitStringPiece(string, ":", + base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)) { + std::vector<base::StringPiece> chunks = base::SplitStringPiece( + match, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + ASSERT_EQ(2U, chunks.size()); + matches->push_back(BookmarkMatch::MatchPosition()); + int chunks0, chunks1; + EXPECT_TRUE(base::StringToInt(chunks[0], &chunks0)); + EXPECT_TRUE(base::StringToInt(chunks[1], &chunks1)); + matches->back().first = chunks0; + matches->back().second = chunks1; + } + } + + void ExpectMatchPositions( + const BookmarkMatch::MatchPositions& actual_positions, + const BookmarkMatch::MatchPositions& expected_positions) { + ASSERT_EQ(expected_positions.size(), actual_positions.size()); + for (size_t i = 0; i < expected_positions.size(); ++i) { + EXPECT_EQ(expected_positions[i].first, actual_positions[i].first); + EXPECT_EQ(expected_positions[i].second, actual_positions[i].second); + } + } + + protected: + scoped_ptr<BookmarkModel> model_; + + private: + DISALLOW_COPY_AND_ASSIGN(BookmarkIndexTest); +}; + +// Various permutations with differing input, queries and output that exercises +// all query paths. +TEST_F(BookmarkIndexTest, GetBookmarksMatching) { + struct TestData { + const std::string titles; + const std::string query; + const std::string expected; + } data[] = { + // Trivial test case of only one term, exact match. + { "a;b", "A", "a" }, + + // Two terms, exact matches. + { "a b;b", "a b", "a b" }, + + // Prefix match, one term. + { "abcd;abc;b", "abc", "abcd;abc" }, + + // Prefix match, multiple terms. + { "abcd cdef;abcd;abcd cdefg", "abc cde", "abcd cdef;abcd cdefg"}, + + // Exact and prefix match. + { "ab cdef;abcd;abcd cdefg", "ab cdef", "ab cdef"}, + + // Exact and prefix match. + { "ab cdef ghij;ab;cde;cdef;ghi;cdef ab;ghij ab", + "ab cde ghi", + "ab cdef ghij"}, + + // Title with term multiple times. + { "ab ab", "ab", "ab ab"}, + + // Make sure quotes don't do a prefix match. + { "think", "\"thi\"", ""}, + + // Prefix matches against multiple candidates. + { "abc1 abc2 abc3 abc4", "abc", "abc1 abc2 abc3 abc4"}, + + // Multiple prefix matches (with a lot of redundancy) against multiple + // candidates. + { "abc1 abc2 abc3 abc4 def1 def2 def3 def4", + "abc def abc def abc def abc def abc def", + "abc1 abc2 abc3 abc4 def1 def2 def3 def4"}, + + // Prefix match on the first term. + { "abc", "a", "" }, + + // Prefix match on subsequent terms. + { "abc def", "abc d", "" }, + }; + for (size_t i = 0; i < arraysize(data); ++i) { + std::vector<TitleAndURL> bookmarks; + for (const std::string& title : base::SplitString( + data[i].titles, ";", + base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)) { + TitleAndURL bookmark(title, kAboutBlankURL); + bookmarks.push_back(bookmark); + } + AddBookmarks(bookmarks); + + std::vector<std::string> expected; + if (!data[i].expected.empty()) { + expected = base::SplitString(data[i].expected, ";", + base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + } + + ExpectMatches(data[i].query, query_parser::MatchingAlgorithm::DEFAULT, + expected); + + model_ = TestBookmarkClient::CreateModel(); + } +} + +TEST_F(BookmarkIndexTest, GetBookmarksMatchingAlwaysPrefixSearch) { + struct TestData { + const std::string titles; + const std::string query; + const std::string expected; + } data[] = { + // Trivial test case of only one term, exact match. + { "z;y", "Z", "z" }, + + // Prefix match, one term. + { "abcd;abc;b", "abc", "abcd;abc" }, + + // Prefix match, multiple terms. + { "abcd cdef;abcd;abcd cdefg", "abc cde", "abcd cdef;abcd cdefg" }, + + // Exact and prefix match. + { "ab cdef ghij;ab;cde;cdef;ghi;cdef ab;ghij ab", + "ab cde ghi", + "ab cdef ghij" }, + + // Title with term multiple times. + { "ab ab", "ab", "ab ab" }, + + // Make sure quotes don't do a prefix match. + { "think", "\"thi\"", "" }, + + // Prefix matches against multiple candidates. + { "abc1 abc2 abc3 abc4", "abc", "abc1 abc2 abc3 abc4" }, + + // Prefix match on the first term. + { "abc", "a", "abc" }, + + // Prefix match on subsequent terms. + { "abc def", "abc d", "abc def" }, + + // Exact and prefix match. + { "ab cdef;abcd;abcd cdefg", "ab cdef", "ab cdef;abcd cdefg" }, + }; + for (size_t i = 0; i < arraysize(data); ++i) { + std::vector<TitleAndURL> bookmarks; + for (const std::string& title : base::SplitString( + data[i].titles, ";", + base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)) { + TitleAndURL bookmark(title, kAboutBlankURL); + bookmarks.push_back(bookmark); + } + AddBookmarks(bookmarks); + + std::vector<std::string> expected; + if (!data[i].expected.empty()) { + expected = base::SplitString(data[i].expected, ";", + base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + } + + ExpectMatches(data[i].query, + query_parser::MatchingAlgorithm::ALWAYS_PREFIX_SEARCH, + expected); + + model_ = TestBookmarkClient::CreateModel(); + } +} + +// Analogous to GetBookmarksMatching, this test tests various permutations +// of title, URL, and input to see if the title/URL matches the input as +// expected. +TEST_F(BookmarkIndexTest, GetBookmarksMatchingWithURLs) { + struct TestData { + const std::string query; + const std::string title; + const std::string url; + const bool should_be_retrieved; + } data[] = { + // Test single-word inputs. Include both exact matches and prefix matches. + { "foo", "Foo", "http://www.bar.com/", true }, + { "foo", "Foodie", "http://www.bar.com/", true }, + { "foo", "Bar", "http://www.foo.com/", true }, + { "foo", "Bar", "http://www.foodie.com/", true }, + { "foo", "Foo", "http://www.foo.com/", true }, + { "foo", "Bar", "http://www.bar.com/", false }, + { "foo", "Bar", "http://www.bar.com/blah/foo/blah-again/ ", true }, + { "foo", "Bar", "http://www.bar.com/blah/foodie/blah-again/ ", true }, + { "foo", "Bar", "http://www.bar.com/blah-foo/blah-again/ ", true }, + { "foo", "Bar", "http://www.bar.com/blah-foodie/blah-again/ ", true }, + { "foo", "Bar", "http://www.bar.com/blahafoo/blah-again/ ", false }, + + // Test multi-word inputs. + { "foo bar", "Foo Bar", "http://baz.com/", true }, + { "foo bar", "Foodie Bar", "http://baz.com/", true }, + { "bar foo", "Foo Bar", "http://baz.com/", true }, + { "bar foo", "Foodie Barly", "http://baz.com/", true }, + { "foo bar", "Foo Baz", "http://baz.com/", false }, + { "foo bar", "Foo Baz", "http://bar.com/", true }, + { "foo bar", "Foo Baz", "http://barly.com/", true }, + { "foo bar", "Foodie Baz", "http://barly.com/", true }, + { "bar foo", "Foo Baz", "http://bar.com/", true }, + { "bar foo", "Foo Baz", "http://barly.com/", true }, + { "foo bar", "Baz Bar", "http://blah.com/foo", true }, + { "foo bar", "Baz Barly", "http://blah.com/foodie", true }, + { "foo bar", "Baz Bur", "http://blah.com/foo/bar", true }, + { "foo bar", "Baz Bur", "http://blah.com/food/barly", true }, + { "foo bar", "Baz Bur", "http://bar.com/blah/foo", true }, + { "foo bar", "Baz Bur", "http://barly.com/blah/food", true }, + { "foo bar", "Baz Bur", "http://bar.com/blah/flub", false }, + { "foo bar", "Baz Bur", "http://foo.com/blah/flub", false } + }; + + for (size_t i = 0; i < arraysize(data); ++i) { + model_ = TestBookmarkClient::CreateModel(); + std::vector<TitleAndURL> bookmarks; + bookmarks.push_back(TitleAndURL(data[i].title, data[i].url)); + AddBookmarks(bookmarks); + + std::vector<std::string> expected; + if (data[i].should_be_retrieved) + expected.push_back(data[i].title); + + ExpectMatches(data[i].query, query_parser::MatchingAlgorithm::DEFAULT, + expected); + } +} + +TEST_F(BookmarkIndexTest, Normalization) { + struct TestData { + const char* const title; + const char* const query; + } data[] = { + { "fooa\xcc\x88-test", "foo\xc3\xa4-test" }, + { "fooa\xcc\x88-test", "fooa\xcc\x88-test" }, + { "fooa\xcc\x88-test", "foo\xc3\xa4" }, + { "fooa\xcc\x88-test", "fooa\xcc\x88" }, + { "fooa\xcc\x88-test", "foo" }, + { "foo\xc3\xa4-test", "foo\xc3\xa4-test" }, + { "foo\xc3\xa4-test", "fooa\xcc\x88-test" }, + { "foo\xc3\xa4-test", "foo\xc3\xa4" }, + { "foo\xc3\xa4-test", "fooa\xcc\x88" }, + { "foo\xc3\xa4-test", "foo" }, + { "foo", "foo" } + }; + + GURL url(kAboutBlankURL); + for (size_t i = 0; i < arraysize(data); ++i) { + model_->AddURL(model_->other_node(), 0, UTF8ToUTF16(data[i].title), url); + std::vector<BookmarkMatch> matches; + model_->GetBookmarksMatching(UTF8ToUTF16(data[i].query), 10, &matches); + EXPECT_EQ(1u, matches.size()); + model_ = TestBookmarkClient::CreateModel(); + } +} + +// Makes sure match positions are updated appropriately for title matches. +TEST_F(BookmarkIndexTest, MatchPositionsTitles) { + struct TestData { + const std::string title; + const std::string query; + const std::string expected_title_match_positions; + } data[] = { + // Trivial test case of only one term, exact match. + { "a", "A", "0,1" }, + { "foo bar", "bar", "4,7" }, + { "fooey bark", "bar foo", "0,3:6,9" }, + // Non-trivial tests. + { "foobar foo", "foobar foo", "0,6:7,10" }, + { "foobar foo", "foo foobar", "0,6:7,10" }, + { "foobar foobar", "foobar foo", "0,6:7,13" }, + { "foobar foobar", "foo foobar", "0,6:7,13" }, + }; + for (size_t i = 0; i < arraysize(data); ++i) { + std::vector<TitleAndURL> bookmarks; + TitleAndURL bookmark(data[i].title, kAboutBlankURL); + bookmarks.push_back(bookmark); + AddBookmarks(bookmarks); + + std::vector<BookmarkMatch> matches; + model_->GetBookmarksMatching(ASCIIToUTF16(data[i].query), 1000, &matches); + ASSERT_EQ(1U, matches.size()); + + BookmarkMatch::MatchPositions expected_title_matches; + ExtractMatchPositions(data[i].expected_title_match_positions, + &expected_title_matches); + ExpectMatchPositions(matches[0].title_match_positions, + expected_title_matches); + + model_ = TestBookmarkClient::CreateModel(); + } +} + +// Makes sure match positions are updated appropriately for URL matches. +TEST_F(BookmarkIndexTest, MatchPositionsURLs) { + // The encoded stuff between /wiki/ and the # is 第二次世界大戦 + const std::string ja_wiki_url = "http://ja.wikipedia.org/wiki/%E7%AC%AC%E4" + "%BA%8C%E6%AC%A1%E4%B8%96%E7%95%8C%E5%A4%A7%E6%88%A6#.E3.83.B4.E3.82.A7" + ".E3.83.AB.E3.82.B5.E3.82.A4.E3.83.A6.E4.BD.93.E5.88.B6"; + struct TestData { + const std::string query; + const std::string url; + const std::string expected_url_match_positions; + } data[] = { + { "foo", "http://www.foo.com/", "11,14" }, + { "foo", "http://www.foodie.com/", "11,14" }, + { "foo", "http://www.foofoo.com/", "11,14" }, + { "www", "http://www.foo.com/", "7,10" }, + { "foo", "http://www.foodie.com/blah/foo/fi", "11,14:27,30" }, + { "foo", "http://www.blah.com/blah/foo/fi", "25,28" }, + { "foo www", "http://www.foodie.com/blah/foo/fi", "7,10:11,14:27,30" }, + { "www foo", "http://www.foodie.com/blah/foo/fi", "7,10:11,14:27,30" }, + { "www bla", "http://www.foodie.com/blah/foo/fi", "7,10:22,25" }, + { "http", "http://www.foo.com/", "0,4" }, + { "http www", "http://www.foo.com/", "0,4:7,10" }, + { "http foo", "http://www.foo.com/", "0,4:11,14" }, + { "http foo", "http://www.bar.com/baz/foodie/hi", "0,4:23,26" }, + { "第二次", ja_wiki_url, "29,56" }, + { "ja 第二次", ja_wiki_url, "7,9:29,56" }, + { "第二次 E3.8", ja_wiki_url, "29,56:94,98:103,107:" + "112,116:121,125:" + "130,134:139,143" } + }; + + for (size_t i = 0; i < arraysize(data); ++i) { + model_ = TestBookmarkClient::CreateModel(); + std::vector<TitleAndURL> bookmarks; + TitleAndURL bookmark("123456", data[i].url); + bookmarks.push_back(bookmark); + AddBookmarks(bookmarks); + + std::vector<BookmarkMatch> matches; + model_->GetBookmarksMatching(UTF8ToUTF16(data[i].query), 1000, &matches); + ASSERT_EQ(1U, matches.size()) << data[i].url << data[i].query; + + BookmarkMatch::MatchPositions expected_url_matches; + ExtractMatchPositions(data[i].expected_url_match_positions, + &expected_url_matches); + ExpectMatchPositions(matches[0].url_match_positions, expected_url_matches); + } +} + +// Makes sure index is updated when a node is removed. +TEST_F(BookmarkIndexTest, Remove) { + const char* titles[] = { "a", "b" }; + const char* urls[] = {kAboutBlankURL, kAboutBlankURL}; + AddBookmarks(titles, urls, arraysize(titles)); + + // Remove the node and make sure we don't get back any results. + model_->Remove(model_->other_node()->GetChild(0)); + ExpectMatches("A", NULL, 0U); +} + +// Makes sure index is updated when a node's title is changed. +TEST_F(BookmarkIndexTest, ChangeTitle) { + const char* titles[] = { "a", "b" }; + const char* urls[] = {kAboutBlankURL, kAboutBlankURL}; + AddBookmarks(titles, urls, arraysize(titles)); + + // Remove the node and make sure we don't get back any results. + const char* expected[] = { "blah" }; + model_->SetTitle(model_->other_node()->GetChild(0), ASCIIToUTF16("blah")); + ExpectMatches("BlAh", expected, arraysize(expected)); +} + +// Makes sure index is updated when a node's URL is changed. +TEST_F(BookmarkIndexTest, ChangeURL) { + const char* titles[] = { "a", "b" }; + const char* urls[] = {"http://fizz", + "http://fuzz"}; + AddBookmarks(titles, urls, arraysize(titles)); + + const char* expected[] = { "a" }; + model_->SetURL(model_->other_node()->GetChild(0), GURL("http://blah")); + ExpectMatches("blah", expected, arraysize(expected)); +} + +// Makes sure no more than max queries is returned. +TEST_F(BookmarkIndexTest, HonorMax) { + const char* titles[] = { "abcd", "abcde" }; + const char* urls[] = {kAboutBlankURL, kAboutBlankURL}; + AddBookmarks(titles, urls, arraysize(titles)); + + std::vector<BookmarkMatch> matches; + model_->GetBookmarksMatching(ASCIIToUTF16("ABc"), 1, &matches); + EXPECT_EQ(1U, matches.size()); +} + +// Makes sure if the lower case string of a bookmark title is more characters +// than the upper case string no match positions are returned. +TEST_F(BookmarkIndexTest, EmptyMatchOnMultiwideLowercaseString) { + const BookmarkNode* n1 = model_->AddURL(model_->other_node(), 0, + base::WideToUTF16(L"\u0130 i"), + GURL("http://www.google.com")); + + std::vector<BookmarkMatch> matches; + model_->GetBookmarksMatching(ASCIIToUTF16("i"), 100, &matches); + ASSERT_EQ(1U, matches.size()); + EXPECT_EQ(n1, matches[0].node); + EXPECT_TRUE(matches[0].title_match_positions.empty()); +} + +TEST_F(BookmarkIndexTest, GetResultsSortedByTypedCount) { + struct TestData { + const GURL url; + const char* title; + const int typed_count; + } data[] = { + { GURL("http://www.google.com/"), "Google", 100 }, + { GURL("http://maps.google.com/"), "Google Maps", 40 }, + { GURL("http://docs.google.com/"), "Google Docs", 50 }, + { GURL("http://reader.google.com/"), "Google Reader", 80 }, + }; + + std::map<GURL, int> typed_count_map; + for (size_t i = 0; i < arraysize(data); ++i) + typed_count_map.insert(std::make_pair(data[i].url, data[i].typed_count)); + + scoped_ptr<BookmarkModel> model = TestBookmarkClient::CreateModelWithClient( + make_scoped_ptr(new BookmarkClientMock(typed_count_map))); + + for (size_t i = 0; i < arraysize(data); ++i) + // Populate the BookmarkIndex. + model->AddURL( + model->other_node(), i, UTF8ToUTF16(data[i].title), data[i].url); + + // Populate match nodes. + std::vector<BookmarkMatch> matches; + model->GetBookmarksMatching(ASCIIToUTF16("google"), 4, &matches); + + // The resulting order should be: + // 1. Google (google.com) 100 + // 2. Google Reader (google.com/reader) 80 + // 3. Google Docs (docs.google.com) 50 + // 4. Google Maps (maps.google.com) 40 + ASSERT_EQ(4U, matches.size()); + EXPECT_EQ(data[0].url, matches[0].node->url()); + EXPECT_EQ(data[3].url, matches[1].node->url()); + EXPECT_EQ(data[2].url, matches[2].node->url()); + EXPECT_EQ(data[1].url, matches[3].node->url()); + + matches.clear(); + // Select top two matches. + model->GetBookmarksMatching(ASCIIToUTF16("google"), 2, &matches); + + ASSERT_EQ(2U, matches.size()); + EXPECT_EQ(data[0].url, matches[0].node->url()); + EXPECT_EQ(data[3].url, matches[1].node->url()); +} + +} // namespace +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_match.cc b/chromium/components/bookmarks/browser/bookmark_match.cc new file mode 100644 index 00000000000..f813cbbbb7f --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_match.cc @@ -0,0 +1,50 @@ +// 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/bookmarks/browser/bookmark_match.h" + +#include "base/logging.h" +#include "base/strings/string16.h" + +namespace bookmarks { + +BookmarkMatch::BookmarkMatch() : node(NULL) {} + +BookmarkMatch::BookmarkMatch(const BookmarkMatch& other) = default; + +BookmarkMatch::~BookmarkMatch() {} + +// static +std::vector<size_t> BookmarkMatch::OffsetsFromMatchPositions( + const MatchPositions& match_positions) { + std::vector<size_t> offsets; + for (MatchPositions::const_iterator i = match_positions.begin(); + i != match_positions.end(); ++i) { + offsets.push_back(i->first); + offsets.push_back(i->second); + } + return offsets; +} + +// static +BookmarkMatch::MatchPositions BookmarkMatch::ReplaceOffsetsInMatchPositions( + const MatchPositions& match_positions, + const std::vector<size_t>& offsets) { + DCHECK_EQ(2 * match_positions.size(), offsets.size()); + MatchPositions new_match_positions; + std::vector<size_t>::const_iterator offset_iter = offsets.begin(); + for (MatchPositions::const_iterator match_iter = match_positions.begin(); + match_iter != match_positions.end(); ++match_iter, ++offset_iter) { + const size_t begin = *offset_iter; + ++offset_iter; + const size_t end = *offset_iter; + if ((begin != base::string16::npos) && (end != base::string16::npos)) { + const MatchPosition new_match_position(begin, end); + new_match_positions.push_back(new_match_position); + } + } + return new_match_positions; +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_match.h b/chromium/components/bookmarks/browser/bookmark_match.h new file mode 100644 index 00000000000..ce658939ca8 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_match.h @@ -0,0 +1,51 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_TITLE_MATCH_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_TITLE_MATCH_H_ + +#include <stddef.h> + +#include <cstddef> +#include <utility> +#include <vector> + +namespace bookmarks { + +class BookmarkNode; + +struct BookmarkMatch { + // Each MatchPosition is the [begin, end) positions of a match within a + // string. + typedef std::pair<size_t, size_t> MatchPosition; + typedef std::vector<MatchPosition> MatchPositions; + + BookmarkMatch(); + BookmarkMatch(const BookmarkMatch& other); + ~BookmarkMatch(); + + // Extracts and returns the offsets from |match_positions|. + static std::vector<size_t> OffsetsFromMatchPositions( + const MatchPositions& match_positions); + + // Replaces the offsets in |match_positions| with those given in |offsets|, + // deleting any which are npos, and returns the updated list of match + // positions. + static MatchPositions ReplaceOffsetsInMatchPositions( + const MatchPositions& match_positions, + const std::vector<size_t>& offsets); + + // The matching node of a query. + const BookmarkNode* node; + + // Location of the matching words in the title of the node. + MatchPositions title_match_positions; + + // Location of the matching words in the URL of the node. + MatchPositions url_match_positions; +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_TITLE_MATCH_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_model.cc b/chromium/components/bookmarks/browser/bookmark_model.cc new file mode 100644 index 00000000000..3343af0be6d --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_model.cc @@ -0,0 +1,1118 @@ +// 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/bookmarks/browser/bookmark_model.h" + +#include <algorithm> +#include <functional> +#include <utility> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/i18n/string_compare.h" +#include "base/logging.h" +#include "base/macros.h" +#include "base/metrics/histogram_macros.h" +#include "base/profiler/scoped_tracker.h" +#include "base/strings/string_util.h" +#include "components/bookmarks/browser/bookmark_expanded_state_tracker.h" +#include "components/bookmarks/browser/bookmark_index.h" +#include "components/bookmarks/browser/bookmark_match.h" +#include "components/bookmarks/browser/bookmark_model_observer.h" +#include "components/bookmarks/browser/bookmark_node_data.h" +#include "components/bookmarks/browser/bookmark_storage.h" +#include "components/bookmarks/browser/bookmark_undo_delegate.h" +#include "components/bookmarks/browser/bookmark_utils.h" +#include "components/favicon_base/favicon_types.h" +#include "grit/components_strings.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/gfx/favicon_size.h" + +using base::Time; + +namespace bookmarks { + +namespace { + +// Helper to get a mutable bookmark node. +BookmarkNode* AsMutable(const BookmarkNode* node) { + return const_cast<BookmarkNode*>(node); +} + +// Helper to get a mutable permanent bookmark node. +BookmarkPermanentNode* AsMutable(const BookmarkPermanentNode* node) { + return const_cast<BookmarkPermanentNode*>(node); +} + +// Comparator used when sorting permanent nodes. Nodes that are initially +// visible are sorted before nodes that are initially hidden. +class VisibilityComparator { + public: + explicit VisibilityComparator(BookmarkClient* client) : client_(client) {} + + // Returns true if |n1| preceeds |n2|. + bool operator()(const BookmarkPermanentNode* n1, + const BookmarkPermanentNode* n2) { + bool n1_visible = client_->IsPermanentNodeVisible(n1); + bool n2_visible = client_->IsPermanentNodeVisible(n2); + return n1_visible != n2_visible && n1_visible; + } + + private: + BookmarkClient* client_; +}; + +// Comparator used when sorting bookmarks. Folders are sorted first, then +// bookmarks. +class SortComparator { + public: + explicit SortComparator(icu::Collator* collator) : collator_(collator) {} + + // Returns true if |n1| preceeds |n2|. + bool operator()(const BookmarkNode* n1, const BookmarkNode* n2) { + if (n1->type() == n2->type()) { + // Types are the same, compare the names. + if (!collator_) + return n1->GetTitle() < n2->GetTitle(); + return base::i18n::CompareString16WithCollator( + *collator_, n1->GetTitle(), n2->GetTitle()) == UCOL_LESS; + } + // Types differ, sort such that folders come first. + return n1->is_folder(); + } + + private: + icu::Collator* collator_; +}; + +// Delegate that does nothing. +class EmptyUndoDelegate : public BookmarkUndoDelegate { + public: + EmptyUndoDelegate() {} + ~EmptyUndoDelegate() override {} + + private: + // BookmarkUndoDelegate: + void SetUndoProvider(BookmarkUndoProvider* provider) override {} + void OnBookmarkNodeRemoved(BookmarkModel* model, + const BookmarkNode* parent, + int index, + scoped_ptr<BookmarkNode> node) override {} + + DISALLOW_COPY_AND_ASSIGN(EmptyUndoDelegate); +}; + +} // namespace + +// BookmarkModel -------------------------------------------------------------- + +BookmarkModel::BookmarkModel(scoped_ptr<BookmarkClient> client) + : client_(std::move(client)), + loaded_(false), + root_(GURL()), + bookmark_bar_node_(NULL), + other_node_(NULL), + mobile_node_(NULL), + next_node_id_(1), + observers_( + base::ObserverList<BookmarkModelObserver>::NOTIFY_EXISTING_ONLY), + loaded_signal_(true, false), + extensive_changes_(0), + undo_delegate_(nullptr), + empty_undo_delegate_(new EmptyUndoDelegate) { + DCHECK(client_); + client_->Init(this); +} + +BookmarkModel::~BookmarkModel() { + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + BookmarkModelBeingDeleted(this)); + + if (store_.get()) { + // The store maintains a reference back to us. We need to tell it we're gone + // so that it doesn't try and invoke a method back on us again. + store_->BookmarkModelDeleted(); + } +} + +void BookmarkModel::Shutdown() { + if (loaded_) + return; + + // See comment in HistoryService::ShutdownOnUIThread where this is invoked for + // details. It is also called when the BookmarkModel is deleted. + loaded_signal_.Signal(); +} + +void BookmarkModel::Load( + PrefService* pref_service, + const base::FilePath& profile_path, + const scoped_refptr<base::SequencedTaskRunner>& io_task_runner, + const scoped_refptr<base::SequencedTaskRunner>& ui_task_runner) { + if (store_.get()) { + // If the store is non-null, it means Load was already invoked. Load should + // only be invoked once. + NOTREACHED(); + return; + } + + expanded_state_tracker_.reset( + new BookmarkExpandedStateTracker(this, pref_service)); + + // Load the bookmarks. BookmarkStorage notifies us when done. + store_.reset(new BookmarkStorage(this, profile_path, io_task_runner.get())); + store_->LoadBookmarks(CreateLoadDetails(), ui_task_runner); +} + +const BookmarkNode* BookmarkModel::GetParentForNewNodes() { + std::vector<const BookmarkNode*> nodes = + GetMostRecentlyModifiedUserFolders(this, 1); + DCHECK(!nodes.empty()); // This list is always padded with default folders. + return nodes[0]; +} + +void BookmarkModel::AddObserver(BookmarkModelObserver* observer) { + observers_.AddObserver(observer); +} + +void BookmarkModel::RemoveObserver(BookmarkModelObserver* observer) { + observers_.RemoveObserver(observer); +} + +void BookmarkModel::BeginExtensiveChanges() { + if (++extensive_changes_ == 1) { + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + ExtensiveBookmarkChangesBeginning(this)); + } +} + +void BookmarkModel::EndExtensiveChanges() { + --extensive_changes_; + DCHECK_GE(extensive_changes_, 0); + if (extensive_changes_ == 0) { + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + ExtensiveBookmarkChangesEnded(this)); + } +} + +void BookmarkModel::BeginGroupedChanges() { + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + GroupedBookmarkChangesBeginning(this)); +} + +void BookmarkModel::EndGroupedChanges() { + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + GroupedBookmarkChangesEnded(this)); +} + +void BookmarkModel::Remove(const BookmarkNode* node) { + DCHECK(loaded_); + DCHECK(node); + DCHECK(!is_root_node(node)); + RemoveAndDeleteNode(AsMutable(node)); +} + +void BookmarkModel::RemoveAllUserBookmarks() { + std::set<GURL> removed_urls; + struct RemoveNodeData { + RemoveNodeData(const BookmarkNode* parent, int index, BookmarkNode* node) + : parent(parent), index(index), node(node) {} + + const BookmarkNode* parent; + int index; + BookmarkNode* node; + }; + std::vector<RemoveNodeData> removed_node_data_list; + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + OnWillRemoveAllUserBookmarks(this)); + + BeginExtensiveChanges(); + // Skip deleting permanent nodes. Permanent bookmark nodes are the root and + // its immediate children. For removing all non permanent nodes just remove + // all children of non-root permanent nodes. + { + base::AutoLock url_lock(url_lock_); + for (int i = 0; i < root_.child_count(); ++i) { + const BookmarkNode* permanent_node = root_.GetChild(i); + + if (!client_->CanBeEditedByUser(permanent_node)) + continue; + + for (int j = permanent_node->child_count() - 1; j >= 0; --j) { + BookmarkNode* child_node = AsMutable(permanent_node->GetChild(j)); + RemoveNodeAndGetRemovedUrls(child_node, &removed_urls); + removed_node_data_list.push_back( + RemoveNodeData(permanent_node, j, child_node)); + } + } + } + EndExtensiveChanges(); + if (store_.get()) + store_->ScheduleSave(); + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + BookmarkAllUserNodesRemoved(this, removed_urls)); + + BeginGroupedChanges(); + for (const auto& removed_node_data : removed_node_data_list) { + undo_delegate()->OnBookmarkNodeRemoved( + this, + removed_node_data.parent, + removed_node_data.index, + scoped_ptr<BookmarkNode>(removed_node_data.node)); + } + EndGroupedChanges(); +} + +void BookmarkModel::Move(const BookmarkNode* node, + const BookmarkNode* new_parent, + int index) { + if (!loaded_ || !node || !IsValidIndex(new_parent, index, true) || + is_root_node(new_parent) || is_permanent_node(node)) { + NOTREACHED(); + return; + } + + if (new_parent->HasAncestor(node)) { + // Can't make an ancestor of the node be a child of the node. + NOTREACHED(); + return; + } + + const BookmarkNode* old_parent = node->parent(); + int old_index = old_parent->GetIndexOf(node); + + if (old_parent == new_parent && + (index == old_index || index == old_index + 1)) { + // Node is already in this position, nothing to do. + return; + } + + SetDateFolderModified(new_parent, Time::Now()); + + if (old_parent == new_parent && index > old_index) + index--; + BookmarkNode* mutable_new_parent = AsMutable(new_parent); + mutable_new_parent->Add(AsMutable(node), index); + + if (store_.get()) + store_->ScheduleSave(); + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + BookmarkNodeMoved(this, old_parent, old_index, + new_parent, index)); +} + +void BookmarkModel::Copy(const BookmarkNode* node, + const BookmarkNode* new_parent, + int index) { + if (!loaded_ || !node || !IsValidIndex(new_parent, index, true) || + is_root_node(new_parent) || is_permanent_node(node)) { + NOTREACHED(); + return; + } + + if (new_parent->HasAncestor(node)) { + // Can't make an ancestor of the node be a child of the node. + NOTREACHED(); + return; + } + + SetDateFolderModified(new_parent, Time::Now()); + BookmarkNodeData drag_data(node); + // CloneBookmarkNode will use BookmarkModel methods to do the job, so we + // don't need to send notifications here. + CloneBookmarkNode(this, drag_data.elements, new_parent, index, true); + + if (store_.get()) + store_->ScheduleSave(); +} + +const gfx::Image& BookmarkModel::GetFavicon(const BookmarkNode* node) { + DCHECK(node); + if (node->favicon_state() == BookmarkNode::INVALID_FAVICON) { + BookmarkNode* mutable_node = AsMutable(node); + LoadFavicon(mutable_node, + client_->PreferTouchIcon() ? favicon_base::TOUCH_ICON + : favicon_base::FAVICON); + } + return node->favicon(); +} + +favicon_base::IconType BookmarkModel::GetFaviconType(const BookmarkNode* node) { + DCHECK(node); + return node->favicon_type(); +} + +void BookmarkModel::SetTitle(const BookmarkNode* node, + const base::string16& title) { + DCHECK(node); + + if (node->GetTitle() == title) + return; + + if (is_permanent_node(node) && !client_->CanSetPermanentNodeTitle(node)) { + NOTREACHED(); + return; + } + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + OnWillChangeBookmarkNode(this, node)); + + // The title index doesn't support changing the title, instead we remove then + // add it back. + index_->Remove(node); + AsMutable(node)->SetTitle(title); + index_->Add(node); + + if (store_.get()) + store_->ScheduleSave(); + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + BookmarkNodeChanged(this, node)); +} + +void BookmarkModel::SetURL(const BookmarkNode* node, const GURL& url) { + DCHECK(node && !node->is_folder()); + + if (node->url() == url) + return; + + BookmarkNode* mutable_node = AsMutable(node); + mutable_node->InvalidateFavicon(); + CancelPendingFaviconLoadRequests(mutable_node); + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + OnWillChangeBookmarkNode(this, node)); + + { + base::AutoLock url_lock(url_lock_); + RemoveNodeFromInternalMaps(mutable_node); + mutable_node->set_url(url); + AddNodeToInternalMaps(mutable_node); + } + + if (store_.get()) + store_->ScheduleSave(); + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + BookmarkNodeChanged(this, node)); +} + +void BookmarkModel::SetNodeMetaInfo(const BookmarkNode* node, + const std::string& key, + const std::string& value) { + std::string old_value; + if (node->GetMetaInfo(key, &old_value) && old_value == value) + return; + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + OnWillChangeBookmarkMetaInfo(this, node)); + + if (AsMutable(node)->SetMetaInfo(key, value) && store_.get()) + store_->ScheduleSave(); + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + BookmarkMetaInfoChanged(this, node)); +} + +void BookmarkModel::SetNodeMetaInfoMap( + const BookmarkNode* node, + const BookmarkNode::MetaInfoMap& meta_info_map) { + const BookmarkNode::MetaInfoMap* old_meta_info_map = node->GetMetaInfoMap(); + if ((!old_meta_info_map && meta_info_map.empty()) || + (old_meta_info_map && meta_info_map == *old_meta_info_map)) + return; + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + OnWillChangeBookmarkMetaInfo(this, node)); + + AsMutable(node)->SetMetaInfoMap(meta_info_map); + if (store_.get()) + store_->ScheduleSave(); + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + BookmarkMetaInfoChanged(this, node)); +} + +void BookmarkModel::DeleteNodeMetaInfo(const BookmarkNode* node, + const std::string& key) { + const BookmarkNode::MetaInfoMap* meta_info_map = node->GetMetaInfoMap(); + if (!meta_info_map || meta_info_map->find(key) == meta_info_map->end()) + return; + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + OnWillChangeBookmarkMetaInfo(this, node)); + + if (AsMutable(node)->DeleteMetaInfo(key) && store_.get()) + store_->ScheduleSave(); + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + BookmarkMetaInfoChanged(this, node)); +} + +void BookmarkModel::AddNonClonedKey(const std::string& key) { + non_cloned_keys_.insert(key); +} + +void BookmarkModel::SetNodeSyncTransactionVersion( + const BookmarkNode* node, + int64_t sync_transaction_version) { + DCHECK(client_->CanSyncNode(node)); + + if (sync_transaction_version == node->sync_transaction_version()) + return; + + AsMutable(node)->set_sync_transaction_version(sync_transaction_version); + if (store_.get()) + store_->ScheduleSave(); +} + +void BookmarkModel::OnFaviconsChanged(const std::set<GURL>& page_urls, + const GURL& icon_url) { + std::set<const BookmarkNode*> to_update; + for (const GURL& page_url : page_urls) { + std::vector<const BookmarkNode*> nodes; + GetNodesByURL(page_url, &nodes); + to_update.insert(nodes.begin(), nodes.end()); + } + + if (!icon_url.is_empty()) { + // Log Histogram to determine how often |icon_url| is non empty in + // practice. + // TODO(pkotwicz): Do something more efficient if |icon_url| is non-empty + // many times a day for each user. + UMA_HISTOGRAM_BOOLEAN("Bookmarks.OnFaviconsChangedIconURL", true); + + base::AutoLock url_lock(url_lock_); + for (const BookmarkNode* node : nodes_ordered_by_url_set_) { + if (icon_url == node->icon_url()) + to_update.insert(node); + } + } + + for (const BookmarkNode* node : to_update) { + // Rerequest the favicon. + BookmarkNode* mutable_node = AsMutable(node); + mutable_node->InvalidateFavicon(); + CancelPendingFaviconLoadRequests(mutable_node); + FOR_EACH_OBSERVER(BookmarkModelObserver, + observers_, + BookmarkNodeFaviconChanged(this, node)); + } +} + +void BookmarkModel::SetDateAdded(const BookmarkNode* node, Time date_added) { + DCHECK(node && !is_permanent_node(node)); + + if (node->date_added() == date_added) + return; + + AsMutable(node)->set_date_added(date_added); + + // Syncing might result in dates newer than the folder's last modified date. + if (date_added > node->parent()->date_folder_modified()) { + // Will trigger store_->ScheduleSave(). + SetDateFolderModified(node->parent(), date_added); + } else if (store_.get()) { + store_->ScheduleSave(); + } +} + +void BookmarkModel::GetNodesByURL(const GURL& url, + std::vector<const BookmarkNode*>* nodes) { + base::AutoLock url_lock(url_lock_); + BookmarkNode tmp_node(url); + NodesOrderedByURLSet::iterator i = nodes_ordered_by_url_set_.find(&tmp_node); + while (i != nodes_ordered_by_url_set_.end() && (*i)->url() == url) { + nodes->push_back(*i); + ++i; + } +} + +const BookmarkNode* BookmarkModel::GetMostRecentlyAddedUserNodeForURL( + const GURL& url) { + std::vector<const BookmarkNode*> nodes; + GetNodesByURL(url, &nodes); + std::sort(nodes.begin(), nodes.end(), &MoreRecentlyAdded); + + // Look for the first node that the user can edit. + for (size_t i = 0; i < nodes.size(); ++i) { + if (client_->CanBeEditedByUser(nodes[i])) + return nodes[i]; + } + + return NULL; +} + +bool BookmarkModel::HasBookmarks() { + base::AutoLock url_lock(url_lock_); + return !nodes_ordered_by_url_set_.empty(); +} + +bool BookmarkModel::IsBookmarked(const GURL& url) { + base::AutoLock url_lock(url_lock_); + return IsBookmarkedNoLock(url); +} + +void BookmarkModel::GetBookmarks( + std::vector<BookmarkModel::URLAndTitle>* bookmarks) { + base::AutoLock url_lock(url_lock_); + const GURL* last_url = NULL; + for (NodesOrderedByURLSet::iterator i = nodes_ordered_by_url_set_.begin(); + i != nodes_ordered_by_url_set_.end(); ++i) { + const GURL* url = &((*i)->url()); + // Only add unique URLs. + if (!last_url || *url != *last_url) { + BookmarkModel::URLAndTitle bookmark; + bookmark.url = *url; + bookmark.title = (*i)->GetTitle(); + bookmarks->push_back(bookmark); + } + last_url = url; + } +} + +void BookmarkModel::BlockTillLoaded() { + loaded_signal_.Wait(); +} + +const BookmarkNode* BookmarkModel::AddFolder(const BookmarkNode* parent, + int index, + const base::string16& title) { + return AddFolderWithMetaInfo(parent, index, title, NULL); +} +const BookmarkNode* BookmarkModel::AddFolderWithMetaInfo( + const BookmarkNode* parent, + int index, + const base::string16& title, + const BookmarkNode::MetaInfoMap* meta_info) { + if (!loaded_ || is_root_node(parent) || !IsValidIndex(parent, index, true)) { + // Can't add to the root. + NOTREACHED(); + return NULL; + } + + BookmarkNode* new_node = new BookmarkNode(generate_next_node_id(), GURL()); + new_node->set_date_folder_modified(Time::Now()); + // Folders shouldn't have line breaks in their titles. + new_node->SetTitle(title); + new_node->set_type(BookmarkNode::FOLDER); + if (meta_info) + new_node->SetMetaInfoMap(*meta_info); + + return AddNode(AsMutable(parent), index, new_node); +} + +const BookmarkNode* BookmarkModel::AddURL(const BookmarkNode* parent, + int index, + const base::string16& title, + const GURL& url) { + return AddURLWithCreationTimeAndMetaInfo( + parent, + index, + title, + url, + Time::Now(), + NULL); +} + +const BookmarkNode* BookmarkModel::AddURLWithCreationTimeAndMetaInfo( + const BookmarkNode* parent, + int index, + const base::string16& title, + const GURL& url, + const Time& creation_time, + const BookmarkNode::MetaInfoMap* meta_info) { + if (!loaded_ || !url.is_valid() || is_root_node(parent) || + !IsValidIndex(parent, index, true)) { + NOTREACHED(); + return NULL; + } + + // Syncing may result in dates newer than the last modified date. + if (creation_time > parent->date_folder_modified()) + SetDateFolderModified(parent, creation_time); + + BookmarkNode* new_node = new BookmarkNode(generate_next_node_id(), url); + new_node->SetTitle(title); + new_node->set_date_added(creation_time); + new_node->set_type(BookmarkNode::URL); + if (meta_info) + new_node->SetMetaInfoMap(*meta_info); + + return AddNode(AsMutable(parent), index, new_node); +} + +void BookmarkModel::SortChildren(const BookmarkNode* parent) { + DCHECK(client_->CanBeEditedByUser(parent)); + + if (!parent || !parent->is_folder() || is_root_node(parent) || + parent->child_count() <= 1) { + return; + } + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + OnWillReorderBookmarkNode(this, parent)); + + UErrorCode error = U_ZERO_ERROR; + scoped_ptr<icu::Collator> collator(icu::Collator::createInstance(error)); + if (U_FAILURE(error)) + collator.reset(NULL); + BookmarkNode* mutable_parent = AsMutable(parent); + std::sort(mutable_parent->children().begin(), + mutable_parent->children().end(), + SortComparator(collator.get())); + + if (store_.get()) + store_->ScheduleSave(); + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + BookmarkNodeChildrenReordered(this, parent)); +} + +void BookmarkModel::ReorderChildren( + const BookmarkNode* parent, + const std::vector<const BookmarkNode*>& ordered_nodes) { + DCHECK(client_->CanBeEditedByUser(parent)); + + // Ensure that all children in |parent| are in |ordered_nodes|. + DCHECK_EQ(static_cast<size_t>(parent->child_count()), ordered_nodes.size()); + for (size_t i = 0; i < ordered_nodes.size(); ++i) + DCHECK_EQ(parent, ordered_nodes[i]->parent()); + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + OnWillReorderBookmarkNode(this, parent)); + + AsMutable(parent)->SetChildren( + *(reinterpret_cast<const std::vector<BookmarkNode*>*>(&ordered_nodes))); + + if (store_.get()) + store_->ScheduleSave(); + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + BookmarkNodeChildrenReordered(this, parent)); +} + +void BookmarkModel::SetDateFolderModified(const BookmarkNode* parent, + const Time time) { + DCHECK(parent); + AsMutable(parent)->set_date_folder_modified(time); + + if (store_.get()) + store_->ScheduleSave(); +} + +void BookmarkModel::ResetDateFolderModified(const BookmarkNode* node) { + SetDateFolderModified(node, Time()); +} + +void BookmarkModel::GetBookmarksMatching(const base::string16& text, + size_t max_count, + std::vector<BookmarkMatch>* matches) { + GetBookmarksMatching(text, max_count, + query_parser::MatchingAlgorithm::DEFAULT, matches); +} + +void BookmarkModel::GetBookmarksMatching( + const base::string16& text, + size_t max_count, + query_parser::MatchingAlgorithm matching_algorithm, + std::vector<BookmarkMatch>* matches) { + if (!loaded_) + return; + + index_->GetBookmarksMatching(text, max_count, matching_algorithm, matches); +} + +void BookmarkModel::ClearStore() { + store_.reset(); +} + +void BookmarkModel::SetPermanentNodeVisible(BookmarkNode::Type type, + bool value) { + BookmarkPermanentNode* node = AsMutable(PermanentNode(type)); + node->set_visible(value || client_->IsPermanentNodeVisible(node)); +} + +const BookmarkPermanentNode* BookmarkModel::PermanentNode( + BookmarkNode::Type type) { + DCHECK(loaded_); + switch (type) { + case BookmarkNode::BOOKMARK_BAR: + return bookmark_bar_node_; + case BookmarkNode::OTHER_NODE: + return other_node_; + case BookmarkNode::MOBILE: + return mobile_node_; + default: + NOTREACHED(); + return NULL; + } +} + +void BookmarkModel::RestoreRemovedNode(const BookmarkNode* parent, + int index, + scoped_ptr<BookmarkNode> scoped_node) { + BookmarkNode* node = scoped_node.release(); + AddNode(AsMutable(parent), index, node); + + // We might be restoring a folder node that have already contained a set of + // child nodes. We need to notify all of them. + NotifyNodeAddedForAllDescendents(node); +} + +void BookmarkModel::NotifyNodeAddedForAllDescendents(const BookmarkNode* node) { + for (int i = 0; i < node->child_count(); ++i) { + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + BookmarkNodeAdded(this, node, i)); + NotifyNodeAddedForAllDescendents(node->GetChild(i)); + } +} + +bool BookmarkModel::IsBookmarkedNoLock(const GURL& url) { + BookmarkNode tmp_node(url); + return (nodes_ordered_by_url_set_.find(&tmp_node) != + nodes_ordered_by_url_set_.end()); +} + +void BookmarkModel::RemoveNode(BookmarkNode* node, + std::set<GURL>* removed_urls) { + if (!loaded_ || !node || is_permanent_node(node)) { + NOTREACHED(); + return; + } + + url_lock_.AssertAcquired(); + if (node->is_url()) { + RemoveNodeFromInternalMaps(node); + removed_urls->insert(node->url()); + } + + CancelPendingFaviconLoadRequests(node); + + // Recurse through children. + for (int i = node->child_count() - 1; i >= 0; --i) + RemoveNode(node->GetChild(i), removed_urls); +} + +void BookmarkModel::DoneLoading(scoped_ptr<BookmarkLoadDetails> details) { + DCHECK(details); + if (loaded_) { + // We should only ever be loaded once. + NOTREACHED(); + return; + } + + // TODO(robliao): Remove ScopedTracker below once https://crbug.com/467179 + // is fixed. + tracked_objects::ScopedTracker tracking_profile1( + FROM_HERE_WITH_EXPLICIT_FUNCTION("467179 BookmarkModel::DoneLoading1")); + + next_node_id_ = details->max_id(); + if (details->computed_checksum() != details->stored_checksum() || + details->ids_reassigned()) { + // TODO(robliao): Remove ScopedTracker below once https://crbug.com/467179 + // is fixed. + tracked_objects::ScopedTracker tracking_profile2( + FROM_HERE_WITH_EXPLICIT_FUNCTION("467179 BookmarkModel::DoneLoading2")); + + // If bookmarks file changed externally, the IDs may have changed + // externally. In that case, the decoder may have reassigned IDs to make + // them unique. So when the file has changed externally, we should save the + // bookmarks file to persist new IDs. + if (store_.get()) + store_->ScheduleSave(); + } + bookmark_bar_node_ = details->release_bb_node(); + other_node_ = details->release_other_folder_node(); + mobile_node_ = details->release_mobile_folder_node(); + index_.reset(details->release_index()); + + // Get any extra nodes and take ownership of them at the |root_|. + std::vector<BookmarkPermanentNode*> extra_nodes; + details->release_extra_nodes(&extra_nodes); + + // TODO(robliao): Remove ScopedTracker below once https://crbug.com/467179 + // is fixed. + tracked_objects::ScopedTracker tracking_profile3( + FROM_HERE_WITH_EXPLICIT_FUNCTION("467179 BookmarkModel::DoneLoading3")); + + // WARNING: order is important here, various places assume the order is + // constant (but can vary between embedders with the initial visibility + // of permanent nodes). + std::vector<BookmarkPermanentNode*> root_children; + root_children.push_back(bookmark_bar_node_); + root_children.push_back(other_node_); + root_children.push_back(mobile_node_); + for (size_t i = 0; i < extra_nodes.size(); ++i) + root_children.push_back(extra_nodes[i]); + + // TODO(robliao): Remove ScopedTracker below once https://crbug.com/467179 + // is fixed. + tracked_objects::ScopedTracker tracking_profile4( + FROM_HERE_WITH_EXPLICIT_FUNCTION("467179 BookmarkModel::DoneLoading4")); + + std::stable_sort(root_children.begin(), + root_children.end(), + VisibilityComparator(client_.get())); + for (size_t i = 0; i < root_children.size(); ++i) + root_.Add(root_children[i], static_cast<int>(i)); + + root_.SetMetaInfoMap(details->model_meta_info_map()); + root_.set_sync_transaction_version(details->model_sync_transaction_version()); + + // TODO(robliao): Remove ScopedTracker below once https://crbug.com/467179 + // is fixed. + tracked_objects::ScopedTracker tracking_profile5( + FROM_HERE_WITH_EXPLICIT_FUNCTION("467179 BookmarkModel::DoneLoading5")); + + { + base::AutoLock url_lock(url_lock_); + // Update nodes_ordered_by_url_set_ from the nodes. + PopulateNodesByURL(&root_); + } + + loaded_ = true; + + loaded_signal_.Signal(); + + // TODO(robliao): Remove ScopedTracker below once https://crbug.com/467179 + // is fixed. + tracked_objects::ScopedTracker tracking_profile6( + FROM_HERE_WITH_EXPLICIT_FUNCTION("467179 BookmarkModel::DoneLoading6")); + + // Notify our direct observers. + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + BookmarkModelLoaded(this, details->ids_reassigned())); +} + +void BookmarkModel::RemoveAndDeleteNode(BookmarkNode* delete_me) { + scoped_ptr<BookmarkNode> node(delete_me); + + const BookmarkNode* parent = node->parent(); + DCHECK(parent); + int index = parent->GetIndexOf(node.get()); + DCHECK_NE(-1, index); + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + OnWillRemoveBookmarks(this, parent, index, node.get())); + + std::set<GURL> removed_urls; + { + base::AutoLock url_lock(url_lock_); + RemoveNodeAndGetRemovedUrls(node.get(), &removed_urls); + } + + if (store_.get()) + store_->ScheduleSave(); + + FOR_EACH_OBSERVER( + BookmarkModelObserver, + observers_, + BookmarkNodeRemoved(this, parent, index, node.get(), removed_urls)); + + undo_delegate()->OnBookmarkNodeRemoved(this, parent, index, std::move(node)); +} + +void BookmarkModel::RemoveNodeFromInternalMaps(BookmarkNode* node) { + index_->Remove(node); + // NOTE: this is called in such a way that url_lock_ is already held. As + // such, this doesn't explicitly grab the lock. + url_lock_.AssertAcquired(); + NodesOrderedByURLSet::iterator i = nodes_ordered_by_url_set_.find(node); + DCHECK(i != nodes_ordered_by_url_set_.end()); + // i points to the first node with the URL, advance until we find the + // node we're removing. + while (*i != node) + ++i; + nodes_ordered_by_url_set_.erase(i); +} + +void BookmarkModel::RemoveNodeAndGetRemovedUrls(BookmarkNode* node, + std::set<GURL>* removed_urls) { + // NOTE: this method should be always called with |url_lock_| held. + // This method does not explicitly acquires a lock. + url_lock_.AssertAcquired(); + DCHECK(removed_urls); + BookmarkNode* parent = node->parent(); + DCHECK(parent); + parent->Remove(node); + RemoveNode(node, removed_urls); + // RemoveNode adds an entry to removed_urls for each node of type URL. As we + // allow duplicates we need to remove any entries that are still bookmarked. + for (std::set<GURL>::iterator i = removed_urls->begin(); + i != removed_urls->end();) { + if (IsBookmarkedNoLock(*i)) { + // When we erase the iterator pointing at the erasee is + // invalidated, so using i++ here within the "erase" call is + // important as it advances the iterator before passing the + // old value through to erase. + removed_urls->erase(i++); + } else { + ++i; + } + } +} + +BookmarkNode* BookmarkModel::AddNode(BookmarkNode* parent, + int index, + BookmarkNode* node) { + parent->Add(node, index); + + if (store_.get()) + store_->ScheduleSave(); + + { + base::AutoLock url_lock(url_lock_); + AddNodeToInternalMaps(node); + } + + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + BookmarkNodeAdded(this, parent, index)); + + return node; +} + +void BookmarkModel::AddNodeToInternalMaps(BookmarkNode* node) { + url_lock_.AssertAcquired(); + if (node->is_url()) { + index_->Add(node); + nodes_ordered_by_url_set_.insert(node); + } + for (int i = 0; i < node->child_count(); ++i) + AddNodeToInternalMaps(node->GetChild(i)); +} + +bool BookmarkModel::IsValidIndex(const BookmarkNode* parent, + int index, + bool allow_end) { + return (parent && parent->is_folder() && + (index >= 0 && (index < parent->child_count() || + (allow_end && index == parent->child_count())))); +} + +BookmarkPermanentNode* BookmarkModel::CreatePermanentNode( + BookmarkNode::Type type) { + DCHECK(type == BookmarkNode::BOOKMARK_BAR || + type == BookmarkNode::OTHER_NODE || + type == BookmarkNode::MOBILE); + BookmarkPermanentNode* node = + new BookmarkPermanentNode(generate_next_node_id()); + node->set_type(type); + node->set_visible(client_->IsPermanentNodeVisible(node)); + + int title_id; + switch (type) { + case BookmarkNode::BOOKMARK_BAR: + title_id = IDS_BOOKMARK_BAR_FOLDER_NAME; + break; + case BookmarkNode::OTHER_NODE: + title_id = IDS_BOOKMARK_BAR_OTHER_FOLDER_NAME; + break; + case BookmarkNode::MOBILE: + title_id = IDS_BOOKMARK_BAR_MOBILE_FOLDER_NAME; + break; + default: + NOTREACHED(); + title_id = IDS_BOOKMARK_BAR_FOLDER_NAME; + break; + } + node->SetTitle(l10n_util::GetStringUTF16(title_id)); + return node; +} + +void BookmarkModel::OnFaviconDataAvailable( + BookmarkNode* node, + favicon_base::IconType icon_type, + const favicon_base::FaviconImageResult& image_result) { + DCHECK(node); + node->set_favicon_load_task_id(base::CancelableTaskTracker::kBadTaskId); + node->set_favicon_state(BookmarkNode::LOADED_FAVICON); + if (!image_result.image.IsEmpty()) { + node->set_favicon_type(icon_type); + node->set_favicon(image_result.image); + node->set_icon_url(image_result.icon_url); + FaviconLoaded(node); + } else if (icon_type == favicon_base::TOUCH_ICON) { + // Couldn't load the touch icon, fallback to the regular favicon. + DCHECK(client_->PreferTouchIcon()); + LoadFavicon(node, favicon_base::FAVICON); + } +} + +void BookmarkModel::LoadFavicon(BookmarkNode* node, + favicon_base::IconType icon_type) { + if (node->is_folder()) + return; + + DCHECK(node->url().is_valid()); + node->set_favicon_state(BookmarkNode::LOADING_FAVICON); + base::CancelableTaskTracker::TaskId taskId = + client_->GetFaviconImageForPageURL( + node->url(), + icon_type, + base::Bind( + &BookmarkModel::OnFaviconDataAvailable, + base::Unretained(this), + node, + icon_type), + &cancelable_task_tracker_); + if (taskId != base::CancelableTaskTracker::kBadTaskId) + node->set_favicon_load_task_id(taskId); +} + +void BookmarkModel::FaviconLoaded(const BookmarkNode* node) { + FOR_EACH_OBSERVER(BookmarkModelObserver, observers_, + BookmarkNodeFaviconChanged(this, node)); +} + +void BookmarkModel::CancelPendingFaviconLoadRequests(BookmarkNode* node) { + if (node->favicon_load_task_id() != base::CancelableTaskTracker::kBadTaskId) { + cancelable_task_tracker_.TryCancel(node->favicon_load_task_id()); + node->set_favicon_load_task_id(base::CancelableTaskTracker::kBadTaskId); + } +} + +void BookmarkModel::PopulateNodesByURL(BookmarkNode* node) { + // NOTE: this is called with url_lock_ already held. As such, this doesn't + // explicitly grab the lock. + if (node->is_url()) + nodes_ordered_by_url_set_.insert(node); + for (int i = 0; i < node->child_count(); ++i) + PopulateNodesByURL(node->GetChild(i)); +} + +int64_t BookmarkModel::generate_next_node_id() { + return next_node_id_++; +} + +scoped_ptr<BookmarkLoadDetails> BookmarkModel::CreateLoadDetails() { + BookmarkPermanentNode* bb_node = + CreatePermanentNode(BookmarkNode::BOOKMARK_BAR); + BookmarkPermanentNode* other_node = + CreatePermanentNode(BookmarkNode::OTHER_NODE); + BookmarkPermanentNode* mobile_node = + CreatePermanentNode(BookmarkNode::MOBILE); + return scoped_ptr<BookmarkLoadDetails>(new BookmarkLoadDetails( + bb_node, + other_node, + mobile_node, + client_->GetLoadExtraNodesCallback(), + new BookmarkIndex(client_.get()), + next_node_id_)); +} + +void BookmarkModel::SetUndoDelegate(BookmarkUndoDelegate* undo_delegate) { + undo_delegate_ = undo_delegate; + if (undo_delegate_) + undo_delegate_->SetUndoProvider(this); +} + +BookmarkUndoDelegate* BookmarkModel::undo_delegate() const { + return undo_delegate_ ? undo_delegate_ : empty_undo_delegate_.get(); +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_model.h b/chromium/components/bookmarks/browser/bookmark_model.h new file mode 100644 index 00000000000..089f95ddec6 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_model.h @@ -0,0 +1,474 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_MODEL_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_MODEL_H_ + +#include <stddef.h> +#include <stdint.h> +#include <map> +#include <set> +#include <vector> + +#include "base/compiler_specific.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/observer_list.h" +#include "base/strings/string16.h" +#include "base/synchronization/lock.h" +#include "base/synchronization/waitable_event.h" +#include "components/bookmarks/browser/bookmark_client.h" +#include "components/bookmarks/browser/bookmark_node.h" +#include "components/bookmarks/browser/bookmark_undo_provider.h" +#include "components/keyed_service/core/keyed_service.h" +#include "ui/gfx/image/image.h" +#include "url/gurl.h" + +class PrefService; + +namespace base { +class FilePath; +class SequencedTaskRunner; +} + +namespace favicon_base { +struct FaviconImageResult; +} + +namespace query_parser { +enum class MatchingAlgorithm; +} + +namespace bookmarks { + +class BookmarkCodecTest; +class BookmarkExpandedStateTracker; +class BookmarkIndex; +class BookmarkLoadDetails; +class BookmarkModelObserver; +class BookmarkStorage; +class BookmarkUndoDelegate; +class ScopedGroupBookmarkActions; +class TestBookmarkClient; +struct BookmarkMatch; + +// BookmarkModel -------------------------------------------------------------- + +// BookmarkModel provides a directed acyclic graph of URLs and folders. +// Three graphs are provided for the three entry points: those on the 'bookmarks +// bar', those in the 'other bookmarks' folder and those in the 'mobile' folder. +// +// An observer may be attached to observe relevant events. +// +// You should NOT directly create a BookmarkModel, instead go through the +// BookmarkModelFactory. +class BookmarkModel : public BookmarkUndoProvider, + public KeyedService { + public: + struct URLAndTitle { + GURL url; + base::string16 title; + }; + + explicit BookmarkModel(scoped_ptr<BookmarkClient> client); + ~BookmarkModel() override; + + // KeyedService: + void Shutdown() override; + + // Loads the bookmarks. This is called upon creation of the + // BookmarkModel. You need not invoke this directly. + // All load operations will be executed on |io_task_runner| and the completion + // callback will be called from |ui_task_runner|. + void Load(PrefService* pref_service, + const base::FilePath& profile_path, + const scoped_refptr<base::SequencedTaskRunner>& io_task_runner, + const scoped_refptr<base::SequencedTaskRunner>& ui_task_runner); + + // Returns true if the model finished loading. + bool loaded() const { return loaded_; } + + // Returns the root node. The 'bookmark bar' node and 'other' node are + // children of the root node. + const BookmarkNode* root_node() const { return &root_; } + + // Returns the 'bookmark bar' node. This is NULL until loaded. + const BookmarkNode* bookmark_bar_node() const { return bookmark_bar_node_; } + + // Returns the 'other' node. This is NULL until loaded. + const BookmarkNode* other_node() const { return other_node_; } + + // Returns the 'mobile' node. This is NULL until loaded. + const BookmarkNode* mobile_node() const { return mobile_node_; } + + bool is_root_node(const BookmarkNode* node) const { return node == &root_; } + + // Returns whether the given |node| is one of the permanent nodes - root node, + // 'bookmark bar' node, 'other' node or 'mobile' node, or one of the root + // nodes supplied by the |client_|. + bool is_permanent_node(const BookmarkNode* node) const { + return node && (node == &root_ || node->parent() == &root_); + } + + // Returns the parent the last node was added to. This never returns NULL + // (as long as the model is loaded). + const BookmarkNode* GetParentForNewNodes(); + + void AddObserver(BookmarkModelObserver* observer); + void RemoveObserver(BookmarkModelObserver* observer); + + // Notifies the observers that an extensive set of changes is about to happen, + // such as during import or sync, so they can delay any expensive UI updates + // until it's finished. + void BeginExtensiveChanges(); + void EndExtensiveChanges(); + + // Returns true if this bookmark model is currently in a mode where extensive + // changes might happen, such as for import and sync. This is helpful for + // observers that are created after the mode has started, and want to check + // state during their own initializer, such as the NTP. + bool IsDoingExtensiveChanges() const { return extensive_changes_ > 0; } + + // Removes |node| from the model and deletes it. Removing a folder node + // recursively removes all nodes. Observers are notified immediately. + void Remove(const BookmarkNode* node); + + // Removes all the non-permanent bookmark nodes that are editable by the user. + // Observers are only notified when all nodes have been removed. There is no + // notification for individual node removals. + void RemoveAllUserBookmarks(); + + // Moves |node| to |new_parent| and inserts it at the given |index|. + void Move(const BookmarkNode* node, + const BookmarkNode* new_parent, + int index); + + // Inserts a copy of |node| into |new_parent| at |index|. + void Copy(const BookmarkNode* node, + const BookmarkNode* new_parent, + int index); + + // Returns the favicon for |node|. If the favicon has not yet been + // loaded it is loaded and the observer of the model notified when done. + const gfx::Image& GetFavicon(const BookmarkNode* node); + + // Returns the type of the favicon for |node|. If the favicon has not yet + // been loaded, it returns |favicon_base::INVALID_ICON|. + favicon_base::IconType GetFaviconType(const BookmarkNode* node); + + // Sets the title of |node|. + void SetTitle(const BookmarkNode* node, const base::string16& title); + + // Sets the URL of |node|. + void SetURL(const BookmarkNode* node, const GURL& url); + + // Sets the date added time of |node|. + void SetDateAdded(const BookmarkNode* node, base::Time date_added); + + // Returns the set of nodes with the |url|. + void GetNodesByURL(const GURL& url, std::vector<const BookmarkNode*>* nodes); + + // Returns the most recently added user node for the |url|; urls from any + // nodes that are not editable by the user are never returned by this call. + // Returns NULL if |url| is not bookmarked. + const BookmarkNode* GetMostRecentlyAddedUserNodeForURL(const GURL& url); + + // Returns true if there are bookmarks, otherwise returns false. + // This method is thread safe. + bool HasBookmarks(); + + // Returns true if the specified URL is bookmarked. + // + // If not on the main thread you *must* invoke BlockTillLoaded first. + bool IsBookmarked(const GURL& url); + + // Returns, by reference in |bookmarks|, the set of bookmarked urls and their + // titles. This returns the unique set of URLs. For example, if two bookmarks + // reference the same URL only one entry is added not matter the titles are + // same or not. + // + // If not on the main thread you *must* invoke BlockTillLoaded first. + void GetBookmarks(std::vector<BookmarkModel::URLAndTitle>* urls); + + // Blocks until loaded. This is intended for usage on a thread other than + // the main thread. + void BlockTillLoaded(); + + // Adds a new folder node at the specified position. + const BookmarkNode* AddFolder(const BookmarkNode* parent, + int index, + const base::string16& title); + + // Adds a new folder with meta info. + const BookmarkNode* AddFolderWithMetaInfo( + const BookmarkNode* parent, + int index, + const base::string16& title, + const BookmarkNode::MetaInfoMap* meta_info); + + // Adds a url at the specified position. + const BookmarkNode* AddURL(const BookmarkNode* parent, + int index, + const base::string16& title, + const GURL& url); + + // Adds a url with a specific creation date and meta info. + const BookmarkNode* AddURLWithCreationTimeAndMetaInfo( + const BookmarkNode* parent, + int index, + const base::string16& title, + const GURL& url, + const base::Time& creation_time, + const BookmarkNode::MetaInfoMap* meta_info); + + // Sorts the children of |parent|, notifying observers by way of the + // BookmarkNodeChildrenReordered method. + void SortChildren(const BookmarkNode* parent); + + // Order the children of |parent| as specified in |ordered_nodes|. This + // function should only be used to reorder the child nodes of |parent| and + // is not meant to move nodes between different parent. Notifies observers + // using the BookmarkNodeChildrenReordered method. + void ReorderChildren(const BookmarkNode* parent, + const std::vector<const BookmarkNode*>& ordered_nodes); + + // Sets the date when the folder was modified. + void SetDateFolderModified(const BookmarkNode* node, const base::Time time); + + // Resets the 'date modified' time of the node to 0. This is used during + // importing to exclude the newly created folders from showing up in the + // combobox of most recently modified folders. + void ResetDateFolderModified(const BookmarkNode* node); + + // Returns up to |max_count| of bookmarks containing each term from |text| + // in either the title or the URL. It uses default matching algorithm. + void GetBookmarksMatching(const base::string16& text, + size_t max_count, + std::vector<BookmarkMatch>* matches); + + // Returns up to |max_count| of bookmarks containing each term from |text| + // in either the title or the URL. + void GetBookmarksMatching(const base::string16& text, + size_t max_count, + query_parser::MatchingAlgorithm matching_algorithm, + std::vector<BookmarkMatch>* matches); + + // Sets the store to NULL, making it so the BookmarkModel does not persist + // any changes to disk. This is only useful during testing to speed up + // testing. + void ClearStore(); + + // Returns the next node ID. + int64_t next_node_id() const { return next_node_id_; } + + // Returns the object responsible for tracking the set of expanded nodes in + // the bookmark editor. + BookmarkExpandedStateTracker* expanded_state_tracker() { + return expanded_state_tracker_.get(); + } + + // Sets the visibility of one of the permanent nodes (unless the node must + // always be visible, see |BookmarkClient::IsPermanentNodeVisible| for more + // details). This is set by sync. + void SetPermanentNodeVisible(BookmarkNode::Type type, bool value); + + // Returns the permanent node of type |type|. + const BookmarkPermanentNode* PermanentNode(BookmarkNode::Type type); + + // Sets/deletes meta info of |node|. + void SetNodeMetaInfo(const BookmarkNode* node, + const std::string& key, + const std::string& value); + void SetNodeMetaInfoMap(const BookmarkNode* node, + const BookmarkNode::MetaInfoMap& meta_info_map); + void DeleteNodeMetaInfo(const BookmarkNode* node, + const std::string& key); + + // Adds |key| to the set of meta info keys that are not copied when a node is + // cloned. + void AddNonClonedKey(const std::string& key); + + // Returns the set of meta info keys that should not be copied when a node is + // cloned. + const std::set<std::string>& non_cloned_keys() const { + return non_cloned_keys_; + } + + // Sets the sync transaction version of |node|. + void SetNodeSyncTransactionVersion(const BookmarkNode* node, + int64_t sync_transaction_version); + + // Notify BookmarkModel that the favicons for the given page URLs (e.g. + // http://www.google.com) and the given icon URL (e.g. + // http://www.google.com/favicon.ico) have changed. It is valid to call + // OnFaviconsChanged() with non-empty |page_urls| and an empty |icon_url| and + // vice versa. + void OnFaviconsChanged(const std::set<GURL>& page_urls, + const GURL& icon_url); + + // Returns the client used by this BookmarkModel. + BookmarkClient* client() const { return client_.get(); } + + void SetUndoDelegate(BookmarkUndoDelegate* undo_delegate); + + private: + friend class BookmarkCodecTest; + friend class BookmarkModelFaviconTest; + friend class BookmarkStorage; + friend class ScopedGroupBookmarkActions; + friend class TestBookmarkClient; + + // Used to order BookmarkNodes by URL. + class NodeURLComparator { + public: + bool operator()(const BookmarkNode* n1, const BookmarkNode* n2) const { + return n1->url() < n2->url(); + } + }; + + // BookmarkUndoProvider: + void RestoreRemovedNode(const BookmarkNode* parent, + int index, + scoped_ptr<BookmarkNode> node) override; + + // Notifies the observers for adding every descedent of |node|. + void NotifyNodeAddedForAllDescendents(const BookmarkNode* node); + + // Implementation of IsBookmarked. Before calling this the caller must obtain + // a lock on |url_lock_|. + bool IsBookmarkedNoLock(const GURL& url); + + // Removes the node from internal maps and recurses through all children. If + // the node is a url, its url is added to removed_urls. + // + // This does NOT delete the node. + void RemoveNode(BookmarkNode* node, std::set<GURL>* removed_urls); + + // Invoked when loading is finished. Sets |loaded_| and notifies observers. + // BookmarkModel takes ownership of |details|. + void DoneLoading(scoped_ptr<BookmarkLoadDetails> details); + + // Populates |nodes_ordered_by_url_set_| from root. + void PopulateNodesByURL(BookmarkNode* node); + + // Removes the node from its parent, but does not delete it. No notifications + // are sent. |removed_urls| is populated with the urls which no longer have + // any bookmarks associated with them. + // This method should be called after acquiring |url_lock_|. + void RemoveNodeAndGetRemovedUrls(BookmarkNode* node, + std::set<GURL>* removed_urls); + + // Removes the node from its parent, sends notification, and deletes it. + // type specifies how the node should be removed. + void RemoveAndDeleteNode(BookmarkNode* delete_me); + + // Remove |node| from |nodes_ordered_by_url_set_| and |index_|. + void RemoveNodeFromInternalMaps(BookmarkNode* node); + + // Adds the |node| at |parent| in the specified |index| and notifies its + // observers. + BookmarkNode* AddNode(BookmarkNode* parent, + int index, + BookmarkNode* node); + + // Adds the |node| to |nodes_ordered_by_url_set_| and |index_|. + void AddNodeToInternalMaps(BookmarkNode* node); + + // Returns true if the parent and index are valid. + bool IsValidIndex(const BookmarkNode* parent, int index, bool allow_end); + + // Creates one of the possible permanent nodes (bookmark bar node, other node + // and mobile node) from |type|. + BookmarkPermanentNode* CreatePermanentNode(BookmarkNode::Type type); + + // Notification that a favicon has finished loading. If we can decode the + // favicon, FaviconLoaded is invoked. + void OnFaviconDataAvailable( + BookmarkNode* node, + favicon_base::IconType icon_type, + const favicon_base::FaviconImageResult& image_result); + + // Invoked from the node to load the favicon. Requests the favicon from the + // favicon service. + void LoadFavicon(BookmarkNode* node, favicon_base::IconType icon_type); + + // Called to notify the observers that the favicon has been loaded. + void FaviconLoaded(const BookmarkNode* node); + + // If we're waiting on a favicon for node, the load request is canceled. + void CancelPendingFaviconLoadRequests(BookmarkNode* node); + + // Notifies the observers that a set of changes initiated by a single user + // action is about to happen and has completed. + void BeginGroupedChanges(); + void EndGroupedChanges(); + + // Generates and returns the next node ID. + int64_t generate_next_node_id(); + + // Sets the maximum node ID to the given value. + // This is used by BookmarkCodec to report the maximum ID after it's done + // decoding since during decoding codec assigns node IDs. + void set_next_node_id(int64_t id) { next_node_id_ = id; } + + // Creates and returns a new BookmarkLoadDetails. It's up to the caller to + // delete the returned object. + scoped_ptr<BookmarkLoadDetails> CreateLoadDetails(); + + BookmarkUndoDelegate* undo_delegate() const; + + scoped_ptr<BookmarkClient> client_; + + // Whether the initial set of data has been loaded. + bool loaded_; + + // The root node. This contains the bookmark bar node, the 'other' node and + // the mobile node as children. + BookmarkNode root_; + + BookmarkPermanentNode* bookmark_bar_node_; + BookmarkPermanentNode* other_node_; + BookmarkPermanentNode* mobile_node_; + + // The maximum ID assigned to the bookmark nodes in the model. + int64_t next_node_id_; + + // The observers. + base::ObserverList<BookmarkModelObserver> observers_; + + // Set of nodes ordered by URL. This is not a map to avoid copying the + // urls. + // WARNING: |nodes_ordered_by_url_set_| is accessed on multiple threads. As + // such, be sure and wrap all usage of it around |url_lock_|. + typedef std::multiset<BookmarkNode*, NodeURLComparator> NodesOrderedByURLSet; + NodesOrderedByURLSet nodes_ordered_by_url_set_; + base::Lock url_lock_; + + // Used for loading favicons. + base::CancelableTaskTracker cancelable_task_tracker_; + + // Reads/writes bookmarks to disk. + scoped_ptr<BookmarkStorage> store_; + + scoped_ptr<BookmarkIndex> index_; + + base::WaitableEvent loaded_signal_; + + // See description of IsDoingExtensiveChanges above. + int extensive_changes_; + + scoped_ptr<BookmarkExpandedStateTracker> expanded_state_tracker_; + + std::set<std::string> non_cloned_keys_; + + BookmarkUndoDelegate* undo_delegate_; + scoped_ptr<BookmarkUndoDelegate> empty_undo_delegate_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkModel); +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_MODEL_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_model_observer.h b/chromium/components/bookmarks/browser/bookmark_model_observer.h new file mode 100644 index 00000000000..a946d532ac8 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_model_observer.h @@ -0,0 +1,143 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_MODEL_OBSERVER_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_MODEL_OBSERVER_H_ + +#include <set> + +class GURL; + +namespace bookmarks { + +class BookmarkModel; +class BookmarkNode; + +// Observer for the BookmarkModel. +class BookmarkModelObserver { + public: + // Invoked when the model has finished loading. |ids_reassigned| mirrors + // that of BookmarkLoadDetails::ids_reassigned. See it for details. + virtual void BookmarkModelLoaded(BookmarkModel* model, + bool ids_reassigned) = 0; + + // Invoked from the destructor of the BookmarkModel. + virtual void BookmarkModelBeingDeleted(BookmarkModel* model) {} + + // Invoked when a node has moved. + virtual void BookmarkNodeMoved(BookmarkModel* model, + const BookmarkNode* old_parent, + int old_index, + const BookmarkNode* new_parent, + int new_index) = 0; + + // Invoked when a node has been added. + virtual void BookmarkNodeAdded(BookmarkModel* model, + const BookmarkNode* parent, + int index) = 0; + + // Invoked prior to removing a node from the model. When a node is removed + // it's descendants are implicitly removed from the model as + // well. Notification is only sent for the node itself, not any + // descendants. For example, if folder 'A' has the children 'A1' and 'A2', + // model->Remove('A') generates a single notification for 'A'; no notification + // is sent for 'A1' or 'A2'. + // + // |parent| the parent of the node that will be removed. + // |old_index| the index of the node about to be removed in |parent|. + // |node| is the node to be removed. + virtual void OnWillRemoveBookmarks(BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node) {} + + // Invoked after a node has been removed from the model. Removing a node + // implicitly removes all descendants. Notification is only sent for the node + // that BookmarkModel::Remove() is invoked on. See description of + // OnWillRemoveBookmarks() for details. + // + // |parent| the parent of the node that was removed. + // |old_index| the index of the removed node in |parent| before it was + // removed. + // |node| the node that was removed. + // |no_longer_bookmarked| contains the urls of any nodes that are no longer + // bookmarked as a result of the removal. + virtual void BookmarkNodeRemoved( + BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node, + const std::set<GURL>& no_longer_bookmarked) = 0; + + // Invoked before the title or url of a node is changed. + virtual void OnWillChangeBookmarkNode(BookmarkModel* model, + const BookmarkNode* node) {} + + // Invoked when the title or url of a node changes. + virtual void BookmarkNodeChanged(BookmarkModel* model, + const BookmarkNode* node) = 0; + + // Invoked before the metainfo of a node is changed. + virtual void OnWillChangeBookmarkMetaInfo(BookmarkModel* model, + const BookmarkNode* node) {} + + // Invoked when the metainfo on a node changes. + virtual void BookmarkMetaInfoChanged(BookmarkModel* model, + const BookmarkNode* node) {} + + // Invoked when a favicon has been loaded or changed. + virtual void BookmarkNodeFaviconChanged(BookmarkModel* model, + const BookmarkNode* node) = 0; + + // Invoked before the direct children of |node| have been reordered in some + // way, such as sorted. + virtual void OnWillReorderBookmarkNode(BookmarkModel* model, + const BookmarkNode* node) {} + + // Invoked when the children (just direct children, not descendants) of + // |node| have been reordered in some way, such as sorted. + virtual void BookmarkNodeChildrenReordered(BookmarkModel* model, + const BookmarkNode* node) = 0; + + // Invoked before an extensive set of model changes is about to begin. + // This tells UI intensive observers to wait until the updates finish to + // update themselves. + // These methods should only be used for imports and sync. + // Observers should still respond to BookmarkNodeRemoved immediately, + // to avoid holding onto stale node pointers. + virtual void ExtensiveBookmarkChangesBeginning(BookmarkModel* model) {} + + // Invoked after an extensive set of model changes has ended. + // This tells observers to update themselves if they were waiting for the + // update to finish. + virtual void ExtensiveBookmarkChangesEnded(BookmarkModel* model) {} + + // Invoked before all non-permanent bookmark nodes that are editable by + // the user are removed. + virtual void OnWillRemoveAllUserBookmarks(BookmarkModel* model) {} + + // Invoked when all non-permanent bookmark nodes that are editable by the + // user have been removed. + // |removed_urls| is populated with the urls which no longer have any + // bookmarks associated with them. + virtual void BookmarkAllUserNodesRemoved( + BookmarkModel* model, + const std::set<GURL>& removed_urls) = 0; + + // Invoked before a set of model changes that is initiated by a single user + // action. For example, this is called a single time when pasting from the + // clipboard before each pasted bookmark is added to the bookmark model. + virtual void GroupedBookmarkChangesBeginning(BookmarkModel* model) {} + + // Invoked after a set of model changes triggered by a single user action has + // ended. + virtual void GroupedBookmarkChangesEnded(BookmarkModel* model) {} + + protected: + virtual ~BookmarkModelObserver() {} +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_MODEL_OBSERVER_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_model_unittest.cc b/chromium/components/bookmarks/browser/bookmark_model_unittest.cc new file mode 100644 index 00000000000..906766596b5 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_model_unittest.cc @@ -0,0 +1,1403 @@ +// 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/bookmarks/browser/bookmark_model.h" + +#include <stddef.h> +#include <stdint.h> +#include <set> +#include <string> +#include <utility> + +#include "base/base_paths.h" +#include "base/command_line.h" +#include "base/compiler_specific.h" +#include "base/containers/hash_tables.h" +#include "base/macros.h" +#include "base/strings/string16.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "components/bookmarks/browser/bookmark_model_observer.h" +#include "components/bookmarks/browser/bookmark_undo_delegate.h" +#include "components/bookmarks/browser/bookmark_utils.h" +#include "components/bookmarks/test/bookmark_test_helpers.h" +#include "components/bookmarks/test/test_bookmark_client.h" +#include "components/favicon_base/favicon_callback.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "ui/base/models/tree_node_iterator.h" +#include "ui/base/models/tree_node_model.h" +#include "ui/gfx/image/image.h" +#include "url/gurl.h" + +using base::ASCIIToUTF16; +using base::Time; +using base::TimeDelta; + +namespace bookmarks { +namespace { + +// Test cases used to test the removal of extra whitespace when adding +// a new folder/bookmark or updating a title of a folder/bookmark. +// Note that whitespace characters are all replaced with spaces, but spaces are +// not collapsed or trimmed. +static struct { + const std::string input_title; + const std::string expected_title; +} url_whitespace_test_cases[] = { + {"foobar", "foobar"}, + // Newlines. + {"foo\nbar", "foo bar"}, + {"foo\n\nbar", "foo bar"}, + {"foo\n\n\nbar", "foo bar"}, + {"foo\r\nbar", "foo bar"}, + {"foo\r\n\r\nbar", "foo bar"}, + {"\nfoo\nbar\n", " foo bar "}, + // Spaces should not collapse. + {"foo bar", "foo bar"}, + {" foo bar ", " foo bar "}, + {" foo bar ", " foo bar "}, + // Tabs. + {"\tfoo\tbar\t", " foo bar "}, + {"\tfoo bar\t", " foo bar "}, + // Mixed cases. + {"\tfoo\nbar\t", " foo bar "}, + {"\tfoo\r\nbar\t", " foo bar "}, + {" foo\tbar\n", " foo bar "}, + {"\t foo \t bar \t", " foo bar "}, + {"\n foo\r\n\tbar\n \t", " foo bar "}, +}; + +// Test cases used to test the removal of extra whitespace when adding +// a new folder/bookmark or updating a title of a folder/bookmark. +static struct { + const std::string input_title; + const std::string expected_title; +} title_whitespace_test_cases[] = { + {"foobar", "foobar"}, + // Newlines. + {"foo\nbar", "foo bar"}, + {"foo\n\nbar", "foo bar"}, + {"foo\n\n\nbar", "foo bar"}, + {"foo\r\nbar", "foo bar"}, + {"foo\r\n\r\nbar", "foo bar"}, + {"\nfoo\nbar\n", " foo bar "}, + // Spaces. + {"foo bar", "foo bar"}, + {" foo bar ", " foo bar "}, + {" foo bar ", " foo bar "}, + // Tabs. + {"\tfoo\tbar\t", " foo bar "}, + {"\tfoo bar\t", " foo bar "}, + // Mixed cases. + {"\tfoo\nbar\t", " foo bar "}, + {"\tfoo\r\nbar\t", " foo bar "}, + {" foo\tbar\n", " foo bar "}, + {"\t foo \t bar \t", " foo bar "}, + {"\n foo\r\n\tbar\n \t", " foo bar "}, +}; + +// Helper to get a mutable bookmark node. +BookmarkNode* AsMutable(const BookmarkNode* node) { + return const_cast<BookmarkNode*>(node); +} + +void SwapDateAdded(BookmarkNode* n1, BookmarkNode* n2) { + Time tmp = n1->date_added(); + n1->set_date_added(n2->date_added()); + n2->set_date_added(tmp); +} + +// See comment in PopulateNodeFromString. +using TestNode = ui::TreeNodeWithValue<BookmarkNode::Type>; + +// Does the work of PopulateNodeFromString. index gives the index of the current +// element in description to process. +void PopulateNodeImpl(const std::vector<std::string>& description, + size_t* index, + TestNode* parent) { + while (*index < description.size()) { + const std::string& element = description[*index]; + (*index)++; + if (element == "[") { + // Create a new folder and recurse to add all the children. + // Folders are given a unique named by way of an ever increasing integer + // value. The folders need not have a name, but one is assigned to help + // in debugging. + static int next_folder_id = 1; + TestNode* new_node = new TestNode(base::IntToString16(next_folder_id++), + BookmarkNode::FOLDER); + parent->Add(new_node, parent->child_count()); + PopulateNodeImpl(description, index, new_node); + } else if (element == "]") { + // End the current folder. + return; + } else { + // Add a new URL. + + // All tokens must be space separated. If there is a [ or ] in the name it + // likely means a space was forgotten. + DCHECK(element.find('[') == std::string::npos); + DCHECK(element.find(']') == std::string::npos); + parent->Add(new TestNode(base::UTF8ToUTF16(element), BookmarkNode::URL), + parent->child_count()); + } + } +} + +// Creates and adds nodes to parent based on description. description consists +// of the following tokens (all space separated): +// [ : creates a new USER_FOLDER node. All elements following the [ until the +// next balanced ] is encountered are added as children to the node. +// ] : closes the last folder created by [ so that any further nodes are added +// to the current folders parent. +// text: creates a new URL node. +// For example, "a [b] c" creates the following nodes: +// a 1 c +// | +// b +// In words: a node of type URL with the title a, followed by a folder node with +// the title 1 having the single child of type url with name b, followed by +// the url node with the title c. +// +// NOTE: each name must be unique, and folders are assigned a unique title by +// way of an increasing integer. +void PopulateNodeFromString(const std::string& description, TestNode* parent) { + std::vector<std::string> elements = base::SplitString( + description, base::kWhitespaceASCII, + base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); + size_t index = 0; + PopulateNodeImpl(elements, &index, parent); +} + +// Populates the BookmarkNode with the children of parent. +void PopulateBookmarkNode(TestNode* parent, + BookmarkModel* model, + const BookmarkNode* bb_node) { + for (int i = 0; i < parent->child_count(); ++i) { + TestNode* child = parent->GetChild(i); + if (child->value == BookmarkNode::FOLDER) { + const BookmarkNode* new_bb_node = + model->AddFolder(bb_node, i, child->GetTitle()); + PopulateBookmarkNode(child, model, new_bb_node); + } else { + model->AddURL(bb_node, i, child->GetTitle(), + GURL("http://" + base::UTF16ToASCII(child->GetTitle()))); + } + } +} + +// Verifies the contents of the bookmark bar node match the contents of the +// TestNode. +void VerifyModelMatchesNode(TestNode* expected, const BookmarkNode* actual) { + ASSERT_EQ(expected->child_count(), actual->child_count()); + for (int i = 0; i < expected->child_count(); ++i) { + TestNode* expected_child = expected->GetChild(i); + const BookmarkNode* actual_child = actual->GetChild(i); + ASSERT_EQ(expected_child->GetTitle(), actual_child->GetTitle()); + if (expected_child->value == BookmarkNode::FOLDER) { + ASSERT_TRUE(actual_child->type() == BookmarkNode::FOLDER); + // Recurse throught children. + VerifyModelMatchesNode(expected_child, actual_child); + } else { + // No need to check the URL, just the title is enough. + ASSERT_TRUE(actual_child->is_url()); + } + } +} + +void VerifyNoDuplicateIDs(BookmarkModel* model) { + ui::TreeNodeIterator<const BookmarkNode> it(model->root_node()); + base::hash_set<int64_t> ids; + while (it.has_next()) + ASSERT_TRUE(ids.insert(it.Next()->id()).second); +} + +class BookmarkModelTest : public testing::Test, + public BookmarkModelObserver, + public BookmarkUndoDelegate { + public: + struct ObserverDetails { + ObserverDetails() { + Set(NULL, NULL, -1, -1); + } + + void Set(const BookmarkNode* node1, + const BookmarkNode* node2, + int index1, + int index2) { + node1_ = node1; + node2_ = node2; + index1_ = index1; + index2_ = index2; + } + + void ExpectEquals(const BookmarkNode* node1, + const BookmarkNode* node2, + int index1, + int index2) { + EXPECT_EQ(node1_, node1); + EXPECT_EQ(node2_, node2); + EXPECT_EQ(index1_, index1); + EXPECT_EQ(index2_, index2); + } + + private: + const BookmarkNode* node1_; + const BookmarkNode* node2_; + int index1_; + int index2_; + }; + + struct NodeRemovalDetail { + NodeRemovalDetail(const BookmarkNode* parent, + int index, + const BookmarkNode* node) + : parent_node_id(parent->id()), index(index), node_id(node->id()) {} + + bool operator==(const NodeRemovalDetail& other) const { + return parent_node_id == other.parent_node_id && + index == other.index && + node_id == other.node_id; + } + + int64_t parent_node_id; + int index; + int64_t node_id; + }; + + BookmarkModelTest() : model_(TestBookmarkClient::CreateModel()) { + model_->AddObserver(this); + ClearCounts(); + } + + void BookmarkModelLoaded(BookmarkModel* model, bool ids_reassigned) override { + // We never load from the db, so that this should never get invoked. + NOTREACHED(); + } + + void BookmarkNodeMoved(BookmarkModel* model, + const BookmarkNode* old_parent, + int old_index, + const BookmarkNode* new_parent, + int new_index) override { + ++moved_count_; + observer_details_.Set(old_parent, new_parent, old_index, new_index); + } + + void BookmarkNodeAdded(BookmarkModel* model, + const BookmarkNode* parent, + int index) override { + ++added_count_; + observer_details_.Set(parent, NULL, index, -1); + } + + void OnWillRemoveBookmarks(BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node) override { + ++before_remove_count_; + } + + void SetUndoProvider(BookmarkUndoProvider* provider) override {} + + void BookmarkNodeRemoved(BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node, + const std::set<GURL>& removed_urls) override { + ++removed_count_; + observer_details_.Set(parent, NULL, old_index, -1); + } + + void BookmarkNodeChanged(BookmarkModel* model, + const BookmarkNode* node) override { + ++changed_count_; + observer_details_.Set(node, NULL, -1, -1); + } + + void OnWillChangeBookmarkNode(BookmarkModel* model, + const BookmarkNode* node) override { + ++before_change_count_; + } + + void BookmarkNodeChildrenReordered(BookmarkModel* model, + const BookmarkNode* node) override { + ++reordered_count_; + } + + void OnWillReorderBookmarkNode(BookmarkModel* model, + const BookmarkNode* node) override { + ++before_reorder_count_; + } + + void BookmarkNodeFaviconChanged(BookmarkModel* model, + const BookmarkNode* node) override { + // We never attempt to load favicons, so that this method never + // gets invoked. + } + + void ExtensiveBookmarkChangesBeginning(BookmarkModel* model) override { + ++extensive_changes_beginning_count_; + } + + void ExtensiveBookmarkChangesEnded(BookmarkModel* model) override { + ++extensive_changes_ended_count_; + } + + void BookmarkAllUserNodesRemoved( + BookmarkModel* model, + const std::set<GURL>& removed_urls) override { + ++all_bookmarks_removed_; + } + + void OnWillRemoveAllUserBookmarks(BookmarkModel* model) override { + ++before_remove_all_count_; + } + + void GroupedBookmarkChangesBeginning(BookmarkModel* model) override { + ++grouped_changes_beginning_count_; + } + + void GroupedBookmarkChangesEnded(BookmarkModel* model) override { + ++grouped_changes_ended_count_; + } + + void OnBookmarkNodeRemoved(BookmarkModel* model, + const BookmarkNode* parent, + int index, + scoped_ptr<BookmarkNode> node) override { + node_removal_details_.push_back( + NodeRemovalDetail(parent, index, node.get())); + } + + void ClearCounts() { + added_count_ = moved_count_ = removed_count_ = changed_count_ = + reordered_count_ = extensive_changes_beginning_count_ = + extensive_changes_ended_count_ = all_bookmarks_removed_ = + before_remove_count_ = before_change_count_ = before_reorder_count_ = + before_remove_all_count_ = grouped_changes_beginning_count_ = + grouped_changes_ended_count_ = 0; + } + + void AssertObserverCount(int added_count, + int moved_count, + int removed_count, + int changed_count, + int reordered_count, + int before_remove_count, + int before_change_count, + int before_reorder_count, + int before_remove_all_count) { + EXPECT_EQ(added_count, added_count_); + EXPECT_EQ(moved_count, moved_count_); + EXPECT_EQ(removed_count, removed_count_); + EXPECT_EQ(changed_count, changed_count_); + EXPECT_EQ(reordered_count, reordered_count_); + EXPECT_EQ(before_remove_count, before_remove_count_); + EXPECT_EQ(before_change_count, before_change_count_); + EXPECT_EQ(before_reorder_count, before_reorder_count_); + EXPECT_EQ(before_remove_all_count, before_remove_all_count_); + } + + void AssertExtensiveChangesObserverCount( + int extensive_changes_beginning_count, + int extensive_changes_ended_count) { + EXPECT_EQ(extensive_changes_beginning_count, + extensive_changes_beginning_count_); + EXPECT_EQ(extensive_changes_ended_count, extensive_changes_ended_count_); + } + + void AssertGroupedChangesObserverCount( + int grouped_changes_beginning_count, + int grouped_changes_ended_count) { + EXPECT_EQ(grouped_changes_beginning_count, + grouped_changes_beginning_count_); + EXPECT_EQ(grouped_changes_ended_count, grouped_changes_ended_count_); + } + + int AllNodesRemovedObserverCount() const { return all_bookmarks_removed_; } + + BookmarkPermanentNode* ReloadModelWithExtraNode() { + model_->RemoveObserver(this); + + BookmarkPermanentNode* extra_node = new BookmarkPermanentNode(100); + BookmarkPermanentNodeList extra_nodes; + extra_nodes.push_back(extra_node); + + scoped_ptr<TestBookmarkClient> client(new TestBookmarkClient); + client->SetExtraNodesToLoad(std::move(extra_nodes)); + + model_ = TestBookmarkClient::CreateModelWithClient(std::move(client)); + model_->AddObserver(this); + ClearCounts(); + + if (model_->root_node()->GetIndexOf(extra_node) == -1) + ADD_FAILURE(); + + return extra_node; + } + + protected: + scoped_ptr<BookmarkModel> model_; + ObserverDetails observer_details_; + std::vector<NodeRemovalDetail> node_removal_details_; + + private: + int added_count_; + int moved_count_; + int removed_count_; + int changed_count_; + int reordered_count_; + int extensive_changes_beginning_count_; + int extensive_changes_ended_count_; + int all_bookmarks_removed_; + int before_remove_count_; + int before_change_count_; + int before_reorder_count_; + int before_remove_all_count_; + int grouped_changes_beginning_count_; + int grouped_changes_ended_count_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkModelTest); +}; + +TEST_F(BookmarkModelTest, InitialState) { + const BookmarkNode* bb_node = model_->bookmark_bar_node(); + ASSERT_TRUE(bb_node != NULL); + EXPECT_EQ(0, bb_node->child_count()); + EXPECT_EQ(BookmarkNode::BOOKMARK_BAR, bb_node->type()); + + const BookmarkNode* other_node = model_->other_node(); + ASSERT_TRUE(other_node != NULL); + EXPECT_EQ(0, other_node->child_count()); + EXPECT_EQ(BookmarkNode::OTHER_NODE, other_node->type()); + + const BookmarkNode* mobile_node = model_->mobile_node(); + ASSERT_TRUE(mobile_node != NULL); + EXPECT_EQ(0, mobile_node->child_count()); + EXPECT_EQ(BookmarkNode::MOBILE, mobile_node->type()); + + EXPECT_TRUE(bb_node->id() != other_node->id()); + EXPECT_TRUE(bb_node->id() != mobile_node->id()); + EXPECT_TRUE(other_node->id() != mobile_node->id()); +} + +TEST_F(BookmarkModelTest, AddURL) { + const BookmarkNode* root = model_->bookmark_bar_node(); + const base::string16 title(ASCIIToUTF16("foo")); + const GURL url("http://foo.com"); + + const BookmarkNode* new_node = model_->AddURL(root, 0, title, url); + AssertObserverCount(1, 0, 0, 0, 0, 0, 0, 0, 0); + observer_details_.ExpectEquals(root, NULL, 0, -1); + + ASSERT_EQ(1, root->child_count()); + ASSERT_EQ(title, new_node->GetTitle()); + ASSERT_TRUE(url == new_node->url()); + ASSERT_EQ(BookmarkNode::URL, new_node->type()); + ASSERT_TRUE(new_node == model_->GetMostRecentlyAddedUserNodeForURL(url)); + + EXPECT_TRUE(new_node->id() != root->id() && + new_node->id() != model_->other_node()->id() && + new_node->id() != model_->mobile_node()->id()); +} + +TEST_F(BookmarkModelTest, AddURLWithUnicodeTitle) { + const BookmarkNode* root = model_->bookmark_bar_node(); + const base::string16 title(base::WideToUTF16( + L"\u767e\u5ea6\u4e00\u4e0b\uff0c\u4f60\u5c31\u77e5\u9053")); + const GURL url("https://www.baidu.com/"); + + const BookmarkNode* new_node = model_->AddURL(root, 0, title, url); + AssertObserverCount(1, 0, 0, 0, 0, 0, 0, 0, 0); + observer_details_.ExpectEquals(root, NULL, 0, -1); + + ASSERT_EQ(1, root->child_count()); + ASSERT_EQ(title, new_node->GetTitle()); + ASSERT_TRUE(url == new_node->url()); + ASSERT_EQ(BookmarkNode::URL, new_node->type()); + ASSERT_TRUE(new_node == model_->GetMostRecentlyAddedUserNodeForURL(url)); + + EXPECT_TRUE(new_node->id() != root->id() && + new_node->id() != model_->other_node()->id() && + new_node->id() != model_->mobile_node()->id()); +} + +TEST_F(BookmarkModelTest, AddURLWithWhitespaceTitle) { + for (size_t i = 0; i < arraysize(url_whitespace_test_cases); ++i) { + const BookmarkNode* root = model_->bookmark_bar_node(); + const base::string16 title( + ASCIIToUTF16(url_whitespace_test_cases[i].input_title)); + const GURL url("http://foo.com"); + + const BookmarkNode* new_node = model_->AddURL(root, i, title, url); + + int size = i + 1; + EXPECT_EQ(size, root->child_count()); + EXPECT_EQ(ASCIIToUTF16(url_whitespace_test_cases[i].expected_title), + new_node->GetTitle()); + EXPECT_EQ(BookmarkNode::URL, new_node->type()); + } +} + +TEST_F(BookmarkModelTest, AddURLWithCreationTimeAndMetaInfo) { + const BookmarkNode* root = model_->bookmark_bar_node(); + const base::string16 title(ASCIIToUTF16("foo")); + const GURL url("http://foo.com"); + const Time time = Time::Now() - TimeDelta::FromDays(1); + BookmarkNode::MetaInfoMap meta_info; + meta_info["foo"] = "bar"; + + const BookmarkNode* new_node = model_->AddURLWithCreationTimeAndMetaInfo( + root, 0, title, url, time, &meta_info); + AssertObserverCount(1, 0, 0, 0, 0, 0, 0, 0, 0); + observer_details_.ExpectEquals(root, NULL, 0, -1); + + ASSERT_EQ(1, root->child_count()); + ASSERT_EQ(title, new_node->GetTitle()); + ASSERT_TRUE(url == new_node->url()); + ASSERT_EQ(BookmarkNode::URL, new_node->type()); + ASSERT_EQ(time, new_node->date_added()); + ASSERT_TRUE(new_node->GetMetaInfoMap()); + ASSERT_EQ(meta_info, *new_node->GetMetaInfoMap()); + ASSERT_TRUE(new_node == model_->GetMostRecentlyAddedUserNodeForURL(url)); + + EXPECT_TRUE(new_node->id() != root->id() && + new_node->id() != model_->other_node()->id() && + new_node->id() != model_->mobile_node()->id()); +} + +TEST_F(BookmarkModelTest, AddURLToMobileBookmarks) { + const BookmarkNode* root = model_->mobile_node(); + const base::string16 title(ASCIIToUTF16("foo")); + const GURL url("http://foo.com"); + + const BookmarkNode* new_node = model_->AddURL(root, 0, title, url); + AssertObserverCount(1, 0, 0, 0, 0, 0, 0, 0, 0); + observer_details_.ExpectEquals(root, NULL, 0, -1); + + ASSERT_EQ(1, root->child_count()); + ASSERT_EQ(title, new_node->GetTitle()); + ASSERT_TRUE(url == new_node->url()); + ASSERT_EQ(BookmarkNode::URL, new_node->type()); + ASSERT_TRUE(new_node == model_->GetMostRecentlyAddedUserNodeForURL(url)); + + EXPECT_TRUE(new_node->id() != root->id() && + new_node->id() != model_->other_node()->id() && + new_node->id() != model_->mobile_node()->id()); +} + +TEST_F(BookmarkModelTest, AddFolder) { + const BookmarkNode* root = model_->bookmark_bar_node(); + const base::string16 title(ASCIIToUTF16("foo")); + + const BookmarkNode* new_node = model_->AddFolder(root, 0, title); + AssertObserverCount(1, 0, 0, 0, 0, 0, 0, 0, 0); + observer_details_.ExpectEquals(root, NULL, 0, -1); + + ASSERT_EQ(1, root->child_count()); + ASSERT_EQ(title, new_node->GetTitle()); + ASSERT_EQ(BookmarkNode::FOLDER, new_node->type()); + + EXPECT_TRUE(new_node->id() != root->id() && + new_node->id() != model_->other_node()->id() && + new_node->id() != model_->mobile_node()->id()); + + // Add another folder, just to make sure folder_ids are incremented correctly. + ClearCounts(); + model_->AddFolder(root, 0, title); + AssertObserverCount(1, 0, 0, 0, 0, 0, 0, 0, 0); + observer_details_.ExpectEquals(root, NULL, 0, -1); +} + +TEST_F(BookmarkModelTest, AddFolderWithWhitespaceTitle) { + for (size_t i = 0; i < arraysize(title_whitespace_test_cases); ++i) { + const BookmarkNode* root = model_->bookmark_bar_node(); + const base::string16 title( + ASCIIToUTF16(title_whitespace_test_cases[i].input_title)); + + const BookmarkNode* new_node = model_->AddFolder(root, i, title); + + int size = i + 1; + EXPECT_EQ(size, root->child_count()); + EXPECT_EQ(ASCIIToUTF16(title_whitespace_test_cases[i].expected_title), + new_node->GetTitle()); + EXPECT_EQ(BookmarkNode::FOLDER, new_node->type()); + } +} + +TEST_F(BookmarkModelTest, RemoveURL) { + const BookmarkNode* root = model_->bookmark_bar_node(); + const base::string16 title(ASCIIToUTF16("foo")); + const GURL url("http://foo.com"); + model_->AddURL(root, 0, title, url); + ClearCounts(); + + model_->Remove(root->GetChild(0)); + ASSERT_EQ(0, root->child_count()); + AssertObserverCount(0, 0, 1, 0, 0, 1, 0, 0, 0); + observer_details_.ExpectEquals(root, NULL, 0, -1); + + // Make sure there is no mapping for the URL. + ASSERT_TRUE(model_->GetMostRecentlyAddedUserNodeForURL(url) == NULL); +} + +TEST_F(BookmarkModelTest, RemoveFolder) { + const BookmarkNode* root = model_->bookmark_bar_node(); + const BookmarkNode* folder = model_->AddFolder(root, 0, ASCIIToUTF16("foo")); + + ClearCounts(); + + // Add a URL as a child. + const base::string16 title(ASCIIToUTF16("foo")); + const GURL url("http://foo.com"); + model_->AddURL(folder, 0, title, url); + + ClearCounts(); + + // Now remove the folder. + model_->Remove(root->GetChild(0)); + ASSERT_EQ(0, root->child_count()); + AssertObserverCount(0, 0, 1, 0, 0, 1, 0, 0, 0); + observer_details_.ExpectEquals(root, NULL, 0, -1); + + // Make sure there is no mapping for the URL. + ASSERT_TRUE(model_->GetMostRecentlyAddedUserNodeForURL(url) == NULL); +} + +TEST_F(BookmarkModelTest, RemoveAllUserBookmarks) { + const BookmarkNode* bookmark_bar_node = model_->bookmark_bar_node(); + + ClearCounts(); + + // Add a url to bookmark bar. + base::string16 title(ASCIIToUTF16("foo")); + GURL url("http://foo.com"); + const BookmarkNode* url_node = + model_->AddURL(bookmark_bar_node, 0, title, url); + + // Add a folder with child URL. + const BookmarkNode* folder = model_->AddFolder(bookmark_bar_node, 0, title); + model_->AddURL(folder, 0, title, url); + + AssertObserverCount(3, 0, 0, 0, 0, 0, 0, 0, 0); + ClearCounts(); + + int permanent_node_count = model_->root_node()->child_count(); + + NodeRemovalDetail expected_node_removal_details[] = { + NodeRemovalDetail(bookmark_bar_node, 1, url_node), + NodeRemovalDetail(bookmark_bar_node, 0, folder), + }; + + model_->SetUndoDelegate(this); + model_->RemoveAllUserBookmarks(); + + EXPECT_EQ(0, bookmark_bar_node->child_count()); + // No permanent node should be removed. + EXPECT_EQ(permanent_node_count, model_->root_node()->child_count()); + // No individual BookmarkNodeRemoved events are fired, so removed count + // should be 0. + AssertObserverCount(0, 0, 0, 0, 0, 0, 0, 0, 1); + AssertExtensiveChangesObserverCount(1, 1); + AssertGroupedChangesObserverCount(1, 1); + EXPECT_EQ(1, AllNodesRemovedObserverCount()); + EXPECT_EQ(1, AllNodesRemovedObserverCount()); + ASSERT_EQ(2u, node_removal_details_.size()); + EXPECT_EQ(expected_node_removal_details[0], node_removal_details_[0]); + EXPECT_EQ(expected_node_removal_details[1], node_removal_details_[1]); +} + +TEST_F(BookmarkModelTest, SetTitle) { + const BookmarkNode* root = model_->bookmark_bar_node(); + base::string16 title(ASCIIToUTF16("foo")); + const GURL url("http://foo.com"); + const BookmarkNode* node = model_->AddURL(root, 0, title, url); + + ClearCounts(); + + title = ASCIIToUTF16("foo2"); + model_->SetTitle(node, title); + AssertObserverCount(0, 0, 0, 1, 0, 0, 1, 0, 0); + observer_details_.ExpectEquals(node, NULL, -1, -1); + EXPECT_EQ(title, node->GetTitle()); +} + +TEST_F(BookmarkModelTest, SetTitleWithWhitespace) { + for (size_t i = 0; i < arraysize(title_whitespace_test_cases); ++i) { + const BookmarkNode* root = model_->bookmark_bar_node(); + base::string16 title(ASCIIToUTF16("dummy")); + const GURL url("http://foo.com"); + const BookmarkNode* node = model_->AddURL(root, 0, title, url); + + title = ASCIIToUTF16(title_whitespace_test_cases[i].input_title); + model_->SetTitle(node, title); + EXPECT_EQ(ASCIIToUTF16(title_whitespace_test_cases[i].expected_title), + node->GetTitle()); + } +} + +TEST_F(BookmarkModelTest, SetURL) { + const BookmarkNode* root = model_->bookmark_bar_node(); + const base::string16 title(ASCIIToUTF16("foo")); + GURL url("http://foo.com"); + const BookmarkNode* node = model_->AddURL(root, 0, title, url); + + ClearCounts(); + + url = GURL("http://foo2.com"); + model_->SetURL(node, url); + AssertObserverCount(0, 0, 0, 1, 0, 0, 1, 0, 0); + observer_details_.ExpectEquals(node, NULL, -1, -1); + EXPECT_EQ(url, node->url()); +} + +TEST_F(BookmarkModelTest, SetDateAdded) { + const BookmarkNode* root = model_->bookmark_bar_node(); + const base::string16 title(ASCIIToUTF16("foo")); + GURL url("http://foo.com"); + const BookmarkNode* node = model_->AddURL(root, 0, title, url); + + ClearCounts(); + + base::Time new_time = base::Time::Now() + base::TimeDelta::FromMinutes(20); + model_->SetDateAdded(node, new_time); + AssertObserverCount(0, 0, 0, 0, 0, 0, 0, 0, 0); + EXPECT_EQ(new_time, node->date_added()); + EXPECT_EQ(new_time, model_->bookmark_bar_node()->date_folder_modified()); +} + +TEST_F(BookmarkModelTest, Move) { + const BookmarkNode* root = model_->bookmark_bar_node(); + const base::string16 title(ASCIIToUTF16("foo")); + const GURL url("http://foo.com"); + const BookmarkNode* node = model_->AddURL(root, 0, title, url); + const BookmarkNode* folder1 = model_->AddFolder(root, 0, ASCIIToUTF16("foo")); + ClearCounts(); + + model_->Move(node, folder1, 0); + + AssertObserverCount(0, 1, 0, 0, 0, 0, 0, 0, 0); + observer_details_.ExpectEquals(root, folder1, 1, 0); + EXPECT_TRUE(folder1 == node->parent()); + EXPECT_EQ(1, root->child_count()); + EXPECT_EQ(folder1, root->GetChild(0)); + EXPECT_EQ(1, folder1->child_count()); + EXPECT_EQ(node, folder1->GetChild(0)); + + // And remove the folder. + ClearCounts(); + model_->Remove(root->GetChild(0)); + AssertObserverCount(0, 0, 1, 0, 0, 1, 0, 0, 0); + observer_details_.ExpectEquals(root, NULL, 0, -1); + EXPECT_TRUE(model_->GetMostRecentlyAddedUserNodeForURL(url) == NULL); + EXPECT_EQ(0, root->child_count()); +} + +TEST_F(BookmarkModelTest, NonMovingMoveCall) { + const BookmarkNode* root = model_->bookmark_bar_node(); + const base::string16 title(ASCIIToUTF16("foo")); + const GURL url("http://foo.com"); + const base::Time old_date(base::Time::Now() - base::TimeDelta::FromDays(1)); + + const BookmarkNode* node = model_->AddURL(root, 0, title, url); + model_->SetDateFolderModified(root, old_date); + + // Since |node| is already at the index 0 of |root|, this is no-op. + model_->Move(node, root, 0); + + // Check that the modification date is kept untouched. + EXPECT_EQ(old_date, root->date_folder_modified()); +} + +TEST_F(BookmarkModelTest, Copy) { + const BookmarkNode* root = model_->bookmark_bar_node(); + static const std::string model_string("a 1:[ b c ] d 2:[ e f g ] h "); + test::AddNodesFromModelString(model_.get(), root, model_string); + + // Validate initial model. + std::string actual_model_string = test::ModelStringFromNode(root); + EXPECT_EQ(model_string, actual_model_string); + + // Copy 'd' to be after '1:b': URL item from bar to folder. + const BookmarkNode* node_to_copy = root->GetChild(2); + const BookmarkNode* destination = root->GetChild(1); + model_->Copy(node_to_copy, destination, 1); + actual_model_string = test::ModelStringFromNode(root); + EXPECT_EQ("a 1:[ b d c ] d 2:[ e f g ] h ", actual_model_string); + + // Copy '1:d' to be after 'a': URL item from folder to bar. + const BookmarkNode* folder = root->GetChild(1); + node_to_copy = folder->GetChild(1); + model_->Copy(node_to_copy, root, 1); + actual_model_string = test::ModelStringFromNode(root); + EXPECT_EQ("a d 1:[ b d c ] d 2:[ e f g ] h ", actual_model_string); + + // Copy '1' to be after '2:e': Folder from bar to folder. + node_to_copy = root->GetChild(2); + destination = root->GetChild(4); + model_->Copy(node_to_copy, destination, 1); + actual_model_string = test::ModelStringFromNode(root); + EXPECT_EQ("a d 1:[ b d c ] d 2:[ e 1:[ b d c ] f g ] h ", + actual_model_string); + + // Copy '2:1' to be after '2:f': Folder within same folder. + folder = root->GetChild(4); + node_to_copy = folder->GetChild(1); + model_->Copy(node_to_copy, folder, 3); + actual_model_string = test::ModelStringFromNode(root); + EXPECT_EQ("a d 1:[ b d c ] d 2:[ e 1:[ b d c ] f 1:[ b d c ] g ] h ", + actual_model_string); + + // Copy first 'd' to be after 'h': URL item within the bar. + node_to_copy = root->GetChild(1); + model_->Copy(node_to_copy, root, 6); + actual_model_string = test::ModelStringFromNode(root); + EXPECT_EQ("a d 1:[ b d c ] d 2:[ e 1:[ b d c ] f 1:[ b d c ] g ] h d ", + actual_model_string); + + // Copy '2' to be after 'a': Folder within the bar. + node_to_copy = root->GetChild(4); + model_->Copy(node_to_copy, root, 1); + actual_model_string = test::ModelStringFromNode(root); + EXPECT_EQ("a 2:[ e 1:[ b d c ] f 1:[ b d c ] g ] d 1:[ b d c ] " + "d 2:[ e 1:[ b d c ] f 1:[ b d c ] g ] h d ", + actual_model_string); +} + +// Tests that adding a URL to a folder updates the last modified time. +TEST_F(BookmarkModelTest, ParentForNewNodes) { + ASSERT_EQ(model_->bookmark_bar_node(), model_->GetParentForNewNodes()); + + const base::string16 title(ASCIIToUTF16("foo")); + const GURL url("http://foo.com"); + + model_->AddURL(model_->other_node(), 0, title, url); + ASSERT_EQ(model_->other_node(), model_->GetParentForNewNodes()); +} + +// Tests that adding a URL to a folder updates the last modified time. +TEST_F(BookmarkModelTest, ParentForNewMobileNodes) { + ASSERT_EQ(model_->bookmark_bar_node(), model_->GetParentForNewNodes()); + + const base::string16 title(ASCIIToUTF16("foo")); + const GURL url("http://foo.com"); + + model_->AddURL(model_->mobile_node(), 0, title, url); + ASSERT_EQ(model_->mobile_node(), model_->GetParentForNewNodes()); +} + +// Make sure recently modified stays in sync when adding a URL. +TEST_F(BookmarkModelTest, MostRecentlyModifiedFolders) { + // Add a folder. + const BookmarkNode* folder = + model_->AddFolder(model_->other_node(), 0, ASCIIToUTF16("foo")); + // Add a URL to it. + model_->AddURL(folder, 0, ASCIIToUTF16("blah"), GURL("http://foo.com")); + + // Make sure folder is in the most recently modified. + std::vector<const BookmarkNode*> most_recent_folders = + GetMostRecentlyModifiedUserFolders(model_.get(), 1); + ASSERT_EQ(1U, most_recent_folders.size()); + ASSERT_EQ(folder, most_recent_folders[0]); + + // Nuke the folder and do another fetch, making sure folder isn't in the + // returned list. + model_->Remove(folder->parent()->GetChild(0)); + most_recent_folders = GetMostRecentlyModifiedUserFolders(model_.get(), 1); + ASSERT_EQ(1U, most_recent_folders.size()); + ASSERT_TRUE(most_recent_folders[0] != folder); +} + +// Make sure MostRecentlyAddedEntries stays in sync. +TEST_F(BookmarkModelTest, MostRecentlyAddedEntries) { + // Add a couple of nodes such that the following holds for the time of the + // nodes: n1 > n2 > n3 > n4. + Time base_time = Time::Now(); + BookmarkNode* n1 = AsMutable(model_->AddURL(model_->bookmark_bar_node(), + 0, + ASCIIToUTF16("blah"), + GURL("http://foo.com/0"))); + BookmarkNode* n2 = AsMutable(model_->AddURL(model_->bookmark_bar_node(), + 1, + ASCIIToUTF16("blah"), + GURL("http://foo.com/1"))); + BookmarkNode* n3 = AsMutable(model_->AddURL(model_->bookmark_bar_node(), + 2, + ASCIIToUTF16("blah"), + GURL("http://foo.com/2"))); + BookmarkNode* n4 = AsMutable(model_->AddURL(model_->bookmark_bar_node(), + 3, + ASCIIToUTF16("blah"), + GURL("http://foo.com/3"))); + n1->set_date_added(base_time + TimeDelta::FromDays(4)); + n2->set_date_added(base_time + TimeDelta::FromDays(3)); + n3->set_date_added(base_time + TimeDelta::FromDays(2)); + n4->set_date_added(base_time + TimeDelta::FromDays(1)); + + // Make sure order is honored. + std::vector<const BookmarkNode*> recently_added; + GetMostRecentlyAddedEntries(model_.get(), 2, &recently_added); + ASSERT_EQ(2U, recently_added.size()); + ASSERT_TRUE(n1 == recently_added[0]); + ASSERT_TRUE(n2 == recently_added[1]); + + // swap 1 and 2, then check again. + recently_added.clear(); + SwapDateAdded(n1, n2); + GetMostRecentlyAddedEntries(model_.get(), 4, &recently_added); + ASSERT_EQ(4U, recently_added.size()); + ASSERT_TRUE(n2 == recently_added[0]); + ASSERT_TRUE(n1 == recently_added[1]); + ASSERT_TRUE(n3 == recently_added[2]); + ASSERT_TRUE(n4 == recently_added[3]); +} + +// Makes sure GetMostRecentlyAddedUserNodeForURL stays in sync. +TEST_F(BookmarkModelTest, GetMostRecentlyAddedUserNodeForURL) { + // Add a couple of nodes such that the following holds for the time of the + // nodes: n1 > n2 + Time base_time = Time::Now(); + const GURL url("http://foo.com/0"); + BookmarkNode* n1 = AsMutable(model_->AddURL( + model_->bookmark_bar_node(), 0, ASCIIToUTF16("blah"), url)); + BookmarkNode* n2 = AsMutable(model_->AddURL( + model_->bookmark_bar_node(), 1, ASCIIToUTF16("blah"), url)); + n1->set_date_added(base_time + TimeDelta::FromDays(4)); + n2->set_date_added(base_time + TimeDelta::FromDays(3)); + + // Make sure order is honored. + ASSERT_EQ(n1, model_->GetMostRecentlyAddedUserNodeForURL(url)); + + // swap 1 and 2, then check again. + SwapDateAdded(n1, n2); + ASSERT_EQ(n2, model_->GetMostRecentlyAddedUserNodeForURL(url)); +} + +// Makes sure GetBookmarks removes duplicates. +TEST_F(BookmarkModelTest, GetBookmarksWithDups) { + const GURL url("http://foo.com/0"); + const base::string16 title(ASCIIToUTF16("blah")); + model_->AddURL(model_->bookmark_bar_node(), 0, title, url); + model_->AddURL(model_->bookmark_bar_node(), 1, title, url); + + std::vector<BookmarkModel::URLAndTitle> bookmarks; + model_->GetBookmarks(&bookmarks); + ASSERT_EQ(1U, bookmarks.size()); + EXPECT_EQ(url, bookmarks[0].url); + EXPECT_EQ(title, bookmarks[0].title); + + model_->AddURL(model_->bookmark_bar_node(), 2, ASCIIToUTF16("Title2"), url); + // Only one returned, even titles are different. + bookmarks.clear(); + model_->GetBookmarks(&bookmarks); + EXPECT_EQ(1U, bookmarks.size()); +} + +TEST_F(BookmarkModelTest, HasBookmarks) { + const GURL url("http://foo.com/"); + model_->AddURL(model_->bookmark_bar_node(), 0, ASCIIToUTF16("bar"), url); + + EXPECT_TRUE(model_->HasBookmarks()); +} + +// http://crbug.com/450464 +TEST_F(BookmarkModelTest, DISABLED_Sort) { + // Populate the bookmark bar node with nodes for 'B', 'a', 'd' and 'C'. + // 'C' and 'a' are folders. + TestNode bbn; + PopulateNodeFromString("B [ a ] d [ a ]", &bbn); + const BookmarkNode* parent = model_->bookmark_bar_node(); + PopulateBookmarkNode(&bbn, model_.get(), parent); + + BookmarkNode* child1 = AsMutable(parent->GetChild(1)); + child1->SetTitle(ASCIIToUTF16("a")); + delete child1->Remove(child1->GetChild(0)); + BookmarkNode* child3 = AsMutable(parent->GetChild(3)); + child3->SetTitle(ASCIIToUTF16("C")); + delete child3->Remove(child3->GetChild(0)); + + ClearCounts(); + + // Sort the children of the bookmark bar node. + model_->SortChildren(parent); + + // Make sure we were notified. + AssertObserverCount(0, 0, 0, 0, 1, 0, 0, 1, 0); + + // Make sure the order matches (remember, 'a' and 'C' are folders and + // come first). + EXPECT_EQ(parent->GetChild(0)->GetTitle(), ASCIIToUTF16("a")); + EXPECT_EQ(parent->GetChild(1)->GetTitle(), ASCIIToUTF16("C")); + EXPECT_EQ(parent->GetChild(2)->GetTitle(), ASCIIToUTF16("B")); + EXPECT_EQ(parent->GetChild(3)->GetTitle(), ASCIIToUTF16("d")); +} + +TEST_F(BookmarkModelTest, Reorder) { + // Populate the bookmark bar node with nodes 'A', 'B', 'C' and 'D'. + TestNode bbn; + PopulateNodeFromString("A B C D", &bbn); + BookmarkNode* parent = AsMutable(model_->bookmark_bar_node()); + PopulateBookmarkNode(&bbn, model_.get(), parent); + + ClearCounts(); + + // Reorder bar node's bookmarks in reverse order. + std::vector<const BookmarkNode*> new_order; + new_order.push_back(parent->GetChild(3)); + new_order.push_back(parent->GetChild(2)); + new_order.push_back(parent->GetChild(1)); + new_order.push_back(parent->GetChild(0)); + model_->ReorderChildren(parent, new_order); + + // Make sure we were notified. + AssertObserverCount(0, 0, 0, 0, 1, 0, 0, 1, 0); + + // Make sure the order matches is correct (it should be reversed). + ASSERT_EQ(4, parent->child_count()); + EXPECT_EQ("D", base::UTF16ToASCII(parent->GetChild(0)->GetTitle())); + EXPECT_EQ("C", base::UTF16ToASCII(parent->GetChild(1)->GetTitle())); + EXPECT_EQ("B", base::UTF16ToASCII(parent->GetChild(2)->GetTitle())); + EXPECT_EQ("A", base::UTF16ToASCII(parent->GetChild(3)->GetTitle())); +} + +TEST_F(BookmarkModelTest, NodeVisibility) { + // Mobile node invisible by default + EXPECT_TRUE(model_->bookmark_bar_node()->IsVisible()); + EXPECT_TRUE(model_->other_node()->IsVisible()); + EXPECT_FALSE(model_->mobile_node()->IsVisible()); + + // Visibility of permanent node can only be changed if they are not + // forced to be visible by the client. + model_->SetPermanentNodeVisible(BookmarkNode::BOOKMARK_BAR, false); + EXPECT_TRUE(model_->bookmark_bar_node()->IsVisible()); + model_->SetPermanentNodeVisible(BookmarkNode::OTHER_NODE, false); + EXPECT_TRUE(model_->other_node()->IsVisible()); + model_->SetPermanentNodeVisible(BookmarkNode::MOBILE, true); + EXPECT_TRUE(model_->mobile_node()->IsVisible()); + model_->SetPermanentNodeVisible(BookmarkNode::MOBILE, false); + EXPECT_FALSE(model_->mobile_node()->IsVisible()); + + // Arbitrary node should be visible + TestNode bbn; + PopulateNodeFromString("B", &bbn); + const BookmarkNode* parent = model_->mobile_node(); + PopulateBookmarkNode(&bbn, model_.get(), parent); + EXPECT_TRUE(parent->GetChild(0)->IsVisible()); + + // Mobile folder should be visible now that it has a child. + EXPECT_TRUE(model_->mobile_node()->IsVisible()); +} + +TEST_F(BookmarkModelTest, MobileNodeVisibileWithChildren) { + const BookmarkNode* root = model_->mobile_node(); + const base::string16 title(ASCIIToUTF16("foo")); + const GURL url("http://foo.com"); + + model_->AddURL(root, 0, title, url); + EXPECT_TRUE(model_->mobile_node()->IsVisible()); +} + +TEST_F(BookmarkModelTest, ExtensiveChangesObserver) { + AssertExtensiveChangesObserverCount(0, 0); + EXPECT_FALSE(model_->IsDoingExtensiveChanges()); + model_->BeginExtensiveChanges(); + EXPECT_TRUE(model_->IsDoingExtensiveChanges()); + AssertExtensiveChangesObserverCount(1, 0); + model_->EndExtensiveChanges(); + EXPECT_FALSE(model_->IsDoingExtensiveChanges()); + AssertExtensiveChangesObserverCount(1, 1); +} + +TEST_F(BookmarkModelTest, MultipleExtensiveChangesObserver) { + AssertExtensiveChangesObserverCount(0, 0); + EXPECT_FALSE(model_->IsDoingExtensiveChanges()); + model_->BeginExtensiveChanges(); + EXPECT_TRUE(model_->IsDoingExtensiveChanges()); + AssertExtensiveChangesObserverCount(1, 0); + model_->BeginExtensiveChanges(); + EXPECT_TRUE(model_->IsDoingExtensiveChanges()); + AssertExtensiveChangesObserverCount(1, 0); + model_->EndExtensiveChanges(); + EXPECT_TRUE(model_->IsDoingExtensiveChanges()); + AssertExtensiveChangesObserverCount(1, 0); + model_->EndExtensiveChanges(); + EXPECT_FALSE(model_->IsDoingExtensiveChanges()); + AssertExtensiveChangesObserverCount(1, 1); +} + +// Verifies that IsBookmarked is true if any bookmark matches the given URL, +// and that IsBookmarkedByUser is true only if at least one of the matching +// bookmarks can be edited by the user. +TEST_F(BookmarkModelTest, IsBookmarked) { + // Reload the model with an extra node that is not editable by the user. + BookmarkPermanentNode* extra_node = ReloadModelWithExtraNode(); + + // "google.com" is a "user" bookmark. + model_->AddURL(model_->other_node(), 0, base::ASCIIToUTF16("User"), + GURL("http://google.com")); + // "youtube.com" is not. + model_->AddURL(extra_node, 0, base::ASCIIToUTF16("Extra"), + GURL("http://youtube.com")); + + EXPECT_TRUE(model_->IsBookmarked(GURL("http://google.com"))); + EXPECT_TRUE(model_->IsBookmarked(GURL("http://youtube.com"))); + EXPECT_FALSE(model_->IsBookmarked(GURL("http://reddit.com"))); + + EXPECT_TRUE(IsBookmarkedByUser(model_.get(), GURL("http://google.com"))); + EXPECT_FALSE(IsBookmarkedByUser(model_.get(), GURL("http://youtube.com"))); + EXPECT_FALSE(IsBookmarkedByUser(model_.get(), GURL("http://reddit.com"))); +} + +// Verifies that GetMostRecentlyAddedUserNodeForURL skips bookmarks that +// are not owned by the user. +TEST_F(BookmarkModelTest, GetMostRecentlyAddedUserNodeForURLSkipsManagedNodes) { + // Reload the model with an extra node that is not editable by the user. + BookmarkPermanentNode* extra_node = ReloadModelWithExtraNode(); + + const base::string16 title = base::ASCIIToUTF16("Title"); + const BookmarkNode* user_parent = model_->other_node(); + const BookmarkNode* managed_parent = extra_node; + const GURL url("http://google.com"); + + // |url| is not bookmarked yet. + EXPECT_TRUE(model_->GetMostRecentlyAddedUserNodeForURL(url) == NULL); + + // Having a managed node doesn't count. + model_->AddURL(managed_parent, 0, title, url); + EXPECT_TRUE(model_->GetMostRecentlyAddedUserNodeForURL(url) == NULL); + + // Now add a user node. + const BookmarkNode* user = model_->AddURL(user_parent, 0, title, url); + EXPECT_EQ(user, model_->GetMostRecentlyAddedUserNodeForURL(url)); + + // Having a more recent managed node doesn't count either. + const BookmarkNode* managed = model_->AddURL(managed_parent, 0, title, url); + EXPECT_GE(managed->date_added(), user->date_added()); + EXPECT_EQ(user, model_->GetMostRecentlyAddedUserNodeForURL(url)); +} + +TEST(BookmarkNodeTest, NodeMetaInfo) { + GURL url; + BookmarkNode node(url); + EXPECT_FALSE(node.GetMetaInfoMap()); + + EXPECT_TRUE(node.SetMetaInfo("key1", "value1")); + std::string out_value; + EXPECT_TRUE(node.GetMetaInfo("key1", &out_value)); + EXPECT_EQ("value1", out_value); + EXPECT_FALSE(node.SetMetaInfo("key1", "value1")); + + EXPECT_FALSE(node.GetMetaInfo("key2.subkey1", &out_value)); + EXPECT_TRUE(node.SetMetaInfo("key2.subkey1", "value2")); + EXPECT_TRUE(node.GetMetaInfo("key2.subkey1", &out_value)); + EXPECT_EQ("value2", out_value); + + EXPECT_FALSE(node.GetMetaInfo("key2.subkey2.leaf", &out_value)); + EXPECT_TRUE(node.SetMetaInfo("key2.subkey2.leaf", "")); + EXPECT_TRUE(node.GetMetaInfo("key2.subkey2.leaf", &out_value)); + EXPECT_EQ("", out_value); + + EXPECT_TRUE(node.DeleteMetaInfo("key1")); + EXPECT_TRUE(node.DeleteMetaInfo("key2.subkey1")); + EXPECT_TRUE(node.DeleteMetaInfo("key2.subkey2.leaf")); + EXPECT_FALSE(node.DeleteMetaInfo("key3")); + EXPECT_FALSE(node.GetMetaInfo("key1", &out_value)); + EXPECT_FALSE(node.GetMetaInfo("key2.subkey1", &out_value)); + EXPECT_FALSE(node.GetMetaInfo("key2.subkey2", &out_value)); + EXPECT_FALSE(node.GetMetaInfo("key2.subkey2.leaf", &out_value)); + EXPECT_FALSE(node.GetMetaInfoMap()); +} + +// Creates a set of nodes in the bookmark model, and checks that the loaded +// structure is what we first created. +TEST(BookmarkModelTest2, CreateAndRestore) { + struct TestData { + // Structure of the children of the bookmark model node. + const std::string bbn_contents; + // Structure of the children of the other node. + const std::string other_contents; + // Structure of the children of the synced node. + const std::string mobile_contents; + } data[] = { + // See PopulateNodeFromString for a description of these strings. + { "", "" }, + { "a", "b" }, + { "a [ b ]", "" }, + { "", "[ b ] a [ c [ d e [ f ] ] ]" }, + { "a [ b ]", "" }, + { "a b c [ d e [ f ] ]", "g h i [ j k [ l ] ]"}, + }; + scoped_ptr<BookmarkModel> model; + for (size_t i = 0; i < arraysize(data); ++i) { + model = TestBookmarkClient::CreateModel(); + + TestNode bbn; + PopulateNodeFromString(data[i].bbn_contents, &bbn); + PopulateBookmarkNode(&bbn, model.get(), model->bookmark_bar_node()); + + TestNode other; + PopulateNodeFromString(data[i].other_contents, &other); + PopulateBookmarkNode(&other, model.get(), model->other_node()); + + TestNode mobile; + PopulateNodeFromString(data[i].mobile_contents, &mobile); + PopulateBookmarkNode(&mobile, model.get(), model->mobile_node()); + + VerifyModelMatchesNode(&bbn, model->bookmark_bar_node()); + VerifyModelMatchesNode(&other, model->other_node()); + VerifyModelMatchesNode(&mobile, model->mobile_node()); + VerifyNoDuplicateIDs(model.get()); + } +} + +} // namespace + +class BookmarkModelFaviconTest : public testing::Test, + public BookmarkModelObserver { + public: + BookmarkModelFaviconTest() : model_(TestBookmarkClient::CreateModel()) { + model_->AddObserver(this); + } + + // Emulates the favicon getting asynchronously loaded. In production, the + // favicon is asynchronously loaded when BookmarkModel::GetFavicon() is + // called. + void OnFaviconLoaded(BookmarkNode* node, const GURL& icon_url) { + SkBitmap bitmap; + bitmap.allocN32Pixels(16, 16); + bitmap.eraseColor(SK_ColorBLUE); + gfx::Image image = gfx::Image::CreateFrom1xBitmap(bitmap); + + favicon_base::FaviconImageResult image_result; + image_result.image = image; + image_result.icon_url = icon_url; + model_->OnFaviconDataAvailable(node, favicon_base::IconType::FAVICON, + image_result); + } + + bool WasNodeUpdated(const BookmarkNode* node) { + return std::find(updated_nodes_.begin(), updated_nodes_.end(), node) != + updated_nodes_.end(); + } + + void ClearUpdatedNodes() { + updated_nodes_.clear(); + } + + protected: + void BookmarkModelLoaded(BookmarkModel* model, bool ids_reassigned) override { + } + + void BookmarkNodeMoved(BookmarkModel* model, + const BookmarkNode* old_parent, + int old_index, + const BookmarkNode* new_parent, + int new_index) override {} + + void BookmarkNodeAdded(BookmarkModel* model, + const BookmarkNode* parent, + int index) override {} + + void BookmarkNodeRemoved(BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node, + const std::set<GURL>& removed_urls) override {} + + void BookmarkNodeChanged(BookmarkModel* model, + const BookmarkNode* node) override {} + + void BookmarkNodeFaviconChanged(BookmarkModel* model, + const BookmarkNode* node) override { + updated_nodes_.push_back(node); + } + + void BookmarkNodeChildrenReordered(BookmarkModel* model, + const BookmarkNode* node) override {} + + void BookmarkAllUserNodesRemoved( + BookmarkModel* model, + const std::set<GURL>& removed_urls) override { + } + + scoped_ptr<BookmarkModel> model_; + std::vector<const BookmarkNode*> updated_nodes_; + + private: + DISALLOW_COPY_AND_ASSIGN(BookmarkModelFaviconTest); +}; + +// Test that BookmarkModel::OnFaviconsChanged() sends a notification that the +// favicon changed to each BookmarkNode which has either a matching page URL +// (e.g. http://www.google.com) or a matching icon URL +// (e.g. http://www.google.com/favicon.ico). +TEST_F(BookmarkModelFaviconTest, FaviconsChangedObserver) { + const BookmarkNode* root = model_->bookmark_bar_node(); + base::string16 kTitle(ASCIIToUTF16("foo")); + GURL kPageURL1("http://www.google.com"); + GURL kPageURL2("http://www.google.ca"); + GURL kPageURL3("http://www.amazon.com"); + GURL kFaviconURL12("http://www.google.com/favicon.ico"); + GURL kFaviconURL3("http://www.amazon.com/favicon.ico"); + + const BookmarkNode* node1 = model_->AddURL(root, 0, kTitle, kPageURL1); + const BookmarkNode* node2 = model_->AddURL(root, 0, kTitle, kPageURL2); + const BookmarkNode* node3 = model_->AddURL(root, 0, kTitle, kPageURL3); + const BookmarkNode* node4 = model_->AddURL(root, 0, kTitle, kPageURL3); + + { + OnFaviconLoaded(AsMutable(node1), kFaviconURL12); + OnFaviconLoaded(AsMutable(node2), kFaviconURL12); + OnFaviconLoaded(AsMutable(node3), kFaviconURL3); + OnFaviconLoaded(AsMutable(node4), kFaviconURL3); + + ClearUpdatedNodes(); + std::set<GURL> changed_page_urls; + changed_page_urls.insert(kPageURL2); + changed_page_urls.insert(kPageURL3); + model_->OnFaviconsChanged(changed_page_urls, GURL()); + ASSERT_EQ(3u, updated_nodes_.size()); + EXPECT_TRUE(WasNodeUpdated(node2)); + EXPECT_TRUE(WasNodeUpdated(node3)); + EXPECT_TRUE(WasNodeUpdated(node4)); + } + + { + // Reset the favicon data because BookmarkModel::OnFaviconsChanged() clears + // the BookmarkNode's favicon data for all of the BookmarkNodes whose + // favicon data changed. + OnFaviconLoaded(AsMutable(node1), kFaviconURL12); + OnFaviconLoaded(AsMutable(node2), kFaviconURL12); + OnFaviconLoaded(AsMutable(node3), kFaviconURL3); + OnFaviconLoaded(AsMutable(node4), kFaviconURL3); + + ClearUpdatedNodes(); + model_->OnFaviconsChanged(std::set<GURL>(), kFaviconURL12); + ASSERT_EQ(2u, updated_nodes_.size()); + EXPECT_TRUE(WasNodeUpdated(node1)); + EXPECT_TRUE(WasNodeUpdated(node2)); + } + + { + OnFaviconLoaded(AsMutable(node1), kFaviconURL12); + OnFaviconLoaded(AsMutable(node2), kFaviconURL12); + OnFaviconLoaded(AsMutable(node3), kFaviconURL3); + OnFaviconLoaded(AsMutable(node4), kFaviconURL3); + + ClearUpdatedNodes(); + std::set<GURL> changed_page_urls; + changed_page_urls.insert(kPageURL1); + model_->OnFaviconsChanged(changed_page_urls, kFaviconURL12); + ASSERT_EQ(2u, updated_nodes_.size()); + EXPECT_TRUE(WasNodeUpdated(node1)); + EXPECT_TRUE(WasNodeUpdated(node2)); + } +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_node.cc b/chromium/components/bookmarks/browser/bookmark_node.cc new file mode 100644 index 00000000000..9574cf1ea5b --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_node.cc @@ -0,0 +1,136 @@ +// 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/bookmarks/browser/bookmark_node.h" + +#include <map> +#include <string> + +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" + +namespace bookmarks { + +namespace { + +// Whitespace characters to strip from bookmark titles. +const base::char16 kInvalidChars[] = { + '\n', '\r', '\t', + 0x2028, // Line separator + 0x2029, // Paragraph separator + 0 +}; + +} // namespace + +// BookmarkNode --------------------------------------------------------------- + +const int64_t BookmarkNode::kInvalidSyncTransactionVersion = -1; + +BookmarkNode::BookmarkNode(const GURL& url) + : url_(url) { + Initialize(0); +} + +BookmarkNode::BookmarkNode(int64_t id, const GURL& url) : url_(url) { + Initialize(id); +} + +BookmarkNode::~BookmarkNode() { +} + +void BookmarkNode::SetTitle(const base::string16& title) { + // Replace newlines and other problematic whitespace characters in + // folder/bookmark names with spaces. + base::string16 trimmed_title; + base::ReplaceChars(title, kInvalidChars, base::ASCIIToUTF16(" "), + &trimmed_title); + ui::TreeNode<BookmarkNode>::SetTitle(trimmed_title); +} + +bool BookmarkNode::IsVisible() const { + return true; +} + +bool BookmarkNode::GetMetaInfo(const std::string& key, + std::string* value) const { + if (!meta_info_map_) + return false; + + MetaInfoMap::const_iterator it = meta_info_map_->find(key); + if (it == meta_info_map_->end()) + return false; + + *value = it->second; + return true; +} + +bool BookmarkNode::SetMetaInfo(const std::string& key, + const std::string& value) { + if (!meta_info_map_) + meta_info_map_.reset(new MetaInfoMap); + + MetaInfoMap::iterator it = meta_info_map_->find(key); + if (it == meta_info_map_->end()) { + (*meta_info_map_)[key] = value; + return true; + } + // Key already in map, check if the value has changed. + if (it->second == value) + return false; + it->second = value; + return true; +} + +bool BookmarkNode::DeleteMetaInfo(const std::string& key) { + if (!meta_info_map_) + return false; + bool erased = meta_info_map_->erase(key) != 0; + if (meta_info_map_->empty()) + meta_info_map_.reset(); + return erased; +} + +void BookmarkNode::SetMetaInfoMap(const MetaInfoMap& meta_info_map) { + if (meta_info_map.empty()) + meta_info_map_.reset(); + else + meta_info_map_.reset(new MetaInfoMap(meta_info_map)); +} + +const BookmarkNode::MetaInfoMap* BookmarkNode::GetMetaInfoMap() const { + return meta_info_map_.get(); +} + +void BookmarkNode::Initialize(int64_t id) { + id_ = id; + type_ = url_.is_empty() ? FOLDER : URL; + date_added_ = base::Time::Now(); + favicon_type_ = favicon_base::INVALID_ICON; + favicon_state_ = INVALID_FAVICON; + favicon_load_task_id_ = base::CancelableTaskTracker::kBadTaskId; + meta_info_map_.reset(); + sync_transaction_version_ = kInvalidSyncTransactionVersion; +} + +void BookmarkNode::InvalidateFavicon() { + icon_url_ = GURL(); + favicon_ = gfx::Image(); + favicon_type_ = favicon_base::INVALID_ICON; + favicon_state_ = INVALID_FAVICON; +} + +// BookmarkPermanentNode ------------------------------------------------------- + +BookmarkPermanentNode::BookmarkPermanentNode(int64_t id) + : BookmarkNode(id, GURL()), visible_(true) {} + +BookmarkPermanentNode::~BookmarkPermanentNode() { +} + +bool BookmarkPermanentNode::IsVisible() const { + return visible_ || !empty(); +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_node.h b/chromium/components/bookmarks/browser/bookmark_node.h new file mode 100644 index 00000000000..bfd83026b7e --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_node.h @@ -0,0 +1,217 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_NODE_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_NODE_H_ + +#include <stdint.h> + +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/task/cancelable_task_tracker.h" +#include "base/time/time.h" +#include "components/favicon_base/favicon_types.h" +#include "ui/base/models/tree_node_model.h" +#include "ui/gfx/image/image.h" +#include "url/gurl.h" + +namespace bookmarks { + +class BookmarkModel; + +// BookmarkNode --------------------------------------------------------------- + +// BookmarkNode contains information about a starred entry: title, URL, favicon, +// id and type. BookmarkNodes are returned from BookmarkModel. +class BookmarkNode : public ui::TreeNode<BookmarkNode> { + public: + enum Type { + URL, + FOLDER, + BOOKMARK_BAR, + OTHER_NODE, + MOBILE + }; + + enum FaviconState { + INVALID_FAVICON, + LOADING_FAVICON, + LOADED_FAVICON, + }; + + typedef std::map<std::string, std::string> MetaInfoMap; + + static const int64_t kInvalidSyncTransactionVersion; + + // Creates a new node with an id of 0 and |url|. + explicit BookmarkNode(const GURL& url); + // Creates a new node with |id| and |url|. + BookmarkNode(int64_t id, const GURL& url); + + ~BookmarkNode() override; + + // Set the node's internal title. Note that this neither invokes observers + // nor updates any bookmark model this node may be in. For that functionality, + // BookmarkModel::SetTitle(..) should be used instead. + void SetTitle(const base::string16& title) override; + + // Returns an unique id for this node. + // For bookmark nodes that are managed by the bookmark model, the IDs are + // persisted across sessions. + int64_t id() const { return id_; } + void set_id(int64_t id) { id_ = id; } + + const GURL& url() const { return url_; } + void set_url(const GURL& url) { url_ = url; } + + // Returns the favicon's URL. Returns an empty URL if there is no favicon + // associated with this bookmark. + const GURL& icon_url() const { return icon_url_; } + + Type type() const { return type_; } + void set_type(Type type) { type_ = type; } + + // Returns the time the node was added. + const base::Time& date_added() const { return date_added_; } + void set_date_added(const base::Time& date) { date_added_ = date; } + + // Returns the last time the folder was modified. This is only maintained + // for folders (including the bookmark bar and other folder). + const base::Time& date_folder_modified() const { + return date_folder_modified_; + } + void set_date_folder_modified(const base::Time& date) { + date_folder_modified_ = date; + } + + // Convenience for testing if this node represents a folder. A folder is a + // node whose type is not URL. + bool is_folder() const { return type_ != URL; } + bool is_url() const { return type_ == URL; } + + bool is_favicon_loaded() const { return favicon_state_ == LOADED_FAVICON; } + + // Accessor method for controlling the visibility of a bookmark node/sub-tree. + // Note that visibility is not propagated down the tree hierarchy so if a + // parent node is marked as invisible, a child node may return "Visible". This + // function is primarily useful when traversing the model to generate a UI + // representation but we may want to suppress some nodes. + virtual bool IsVisible() const; + + // Gets/sets/deletes value of |key| in the meta info represented by + // |meta_info_str_|. Return true if key is found in meta info for gets or + // meta info is changed indeed for sets/deletes. + bool GetMetaInfo(const std::string& key, std::string* value) const; + bool SetMetaInfo(const std::string& key, const std::string& value); + bool DeleteMetaInfo(const std::string& key); + void SetMetaInfoMap(const MetaInfoMap& meta_info_map); + // Returns NULL if there are no values in the map. + const MetaInfoMap* GetMetaInfoMap() const; + + void set_sync_transaction_version(int64_t sync_transaction_version) { + sync_transaction_version_ = sync_transaction_version; + } + int64_t sync_transaction_version() const { return sync_transaction_version_; } + + // TODO(sky): Consider adding last visit time here, it'll greatly simplify + // HistoryContentsProvider. + + private: + friend class BookmarkModel; + + // A helper function to initialize various fields during construction. + void Initialize(int64_t id); + + // Called when the favicon becomes invalid. + void InvalidateFavicon(); + + // Sets the favicon's URL. + void set_icon_url(const GURL& icon_url) { + icon_url_ = icon_url; + } + + // Returns the favicon. In nearly all cases you should use the method + // BookmarkModel::GetFavicon rather than this one as it takes care of + // loading the favicon if it isn't already loaded. + const gfx::Image& favicon() const { return favicon_; } + void set_favicon(const gfx::Image& icon) { favicon_ = icon; } + + favicon_base::IconType favicon_type() const { return favicon_type_; } + void set_favicon_type(favicon_base::IconType type) { favicon_type_ = type; } + + FaviconState favicon_state() const { return favicon_state_; } + void set_favicon_state(FaviconState state) { favicon_state_ = state; } + + base::CancelableTaskTracker::TaskId favicon_load_task_id() const { + return favicon_load_task_id_; + } + void set_favicon_load_task_id(base::CancelableTaskTracker::TaskId id) { + favicon_load_task_id_ = id; + } + + // The unique identifier for this node. + int64_t id_; + + // The URL of this node. BookmarkModel maintains maps off this URL, so changes + // to the URL must be done through the BookmarkModel. + GURL url_; + + // The type of this node. See enum above. + Type type_; + + // Date of when this node was created. + base::Time date_added_; + + // Date of the last modification. Only used for folders. + base::Time date_folder_modified_; + + // The favicon of this node. + gfx::Image favicon_; + + // The type of favicon currently loaded. + favicon_base::IconType favicon_type_; + + // The URL of the node's favicon. + GURL icon_url_; + + // The loading state of the favicon. + FaviconState favicon_state_; + + // If not base::CancelableTaskTracker::kBadTaskId, it indicates + // we're loading the + // favicon and the task is tracked by CancelabelTaskTracker. + base::CancelableTaskTracker::TaskId favicon_load_task_id_; + + // A map that stores arbitrary meta information about the node. + scoped_ptr<MetaInfoMap> meta_info_map_; + + // The sync transaction version. Defaults to kInvalidSyncTransactionVersion. + int64_t sync_transaction_version_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkNode); +}; + +// BookmarkPermanentNode ------------------------------------------------------- + +// Node used for the permanent folders (excluding the root). +class BookmarkPermanentNode : public BookmarkNode { + public: + explicit BookmarkPermanentNode(int64_t id); + ~BookmarkPermanentNode() override; + + // WARNING: this code is used for other projects. Contact noyau@ for details. + void set_visible(bool value) { visible_ = value; } + + // BookmarkNode overrides: + bool IsVisible() const override; + + private: + bool visible_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkPermanentNode); +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_NODE_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_node_data.cc b/chromium/components/bookmarks/browser/bookmark_node_data.cc new file mode 100644 index 00000000000..e634fd3c7b8 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_node_data.cc @@ -0,0 +1,308 @@ +// 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/bookmarks/browser/bookmark_node_data.h" + +#include <string> + +#include "base/pickle.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "build/build_config.h" +#include "components/bookmarks/browser/bookmark_utils.h" +#include "ui/base/clipboard/scoped_clipboard_writer.h" + +namespace bookmarks { + +const char BookmarkNodeData::kClipboardFormatString[] = + "chromium/x-bookmark-entries"; + +BookmarkNodeData::Element::Element() : is_url(false), id_(0) { +} + +BookmarkNodeData::Element::Element(const BookmarkNode* node) + : is_url(node->is_url()), + url(node->url()), + title(node->GetTitle()), + date_added(node->date_added()), + date_folder_modified(node->date_folder_modified()), + id_(node->id()) { + if (node->GetMetaInfoMap()) + meta_info_map = *node->GetMetaInfoMap(); + for (int i = 0; i < node->child_count(); ++i) + children.push_back(Element(node->GetChild(i))); +} + +BookmarkNodeData::Element::Element(const Element& other) = default; + +BookmarkNodeData::Element::~Element() { +} + +void BookmarkNodeData::Element::WriteToPickle(base::Pickle* pickle) const { + pickle->WriteBool(is_url); + pickle->WriteString(url.spec()); + pickle->WriteString16(title); + pickle->WriteInt64(id_); + pickle->WriteUInt32(static_cast<uint32_t>(meta_info_map.size())); + for (BookmarkNode::MetaInfoMap::const_iterator it = meta_info_map.begin(); + it != meta_info_map.end(); ++it) { + pickle->WriteString(it->first); + pickle->WriteString(it->second); + } + if (!is_url) { + pickle->WriteUInt32(static_cast<uint32_t>(children.size())); + for (std::vector<Element>::const_iterator i = children.begin(); + i != children.end(); ++i) { + i->WriteToPickle(pickle); + } + } +} + +bool BookmarkNodeData::Element::ReadFromPickle(base::PickleIterator* iterator) { + std::string url_spec; + if (!iterator->ReadBool(&is_url) || + !iterator->ReadString(&url_spec) || + !iterator->ReadString16(&title) || + !iterator->ReadInt64(&id_)) { + return false; + } + url = GURL(url_spec); + date_added = base::Time(); + date_folder_modified = base::Time(); + meta_info_map.clear(); + uint32_t meta_field_count; + if (!iterator->ReadUInt32(&meta_field_count)) + return false; + for (size_t i = 0; i < meta_field_count; ++i) { + std::string key; + std::string value; + if (!iterator->ReadString(&key) || + !iterator->ReadString(&value)) { + return false; + } + meta_info_map[key] = value; + } + children.clear(); + if (!is_url) { + uint32_t children_count; + if (!iterator->ReadUInt32(&children_count)) + return false; + children.reserve(children_count); + for (size_t i = 0; i < children_count; ++i) { + children.push_back(Element()); + if (!children.back().ReadFromPickle(iterator)) + return false; + } + } + return true; +} + +// BookmarkNodeData ----------------------------------------------------------- + +BookmarkNodeData::BookmarkNodeData() { +} + +BookmarkNodeData::BookmarkNodeData(const BookmarkNodeData& other) = default; + +BookmarkNodeData::BookmarkNodeData(const BookmarkNode* node) { + elements.push_back(Element(node)); +} + +BookmarkNodeData::BookmarkNodeData( + const std::vector<const BookmarkNode*>& nodes) { + ReadFromVector(nodes); +} + +BookmarkNodeData::~BookmarkNodeData() { +} + +#if !defined(OS_MACOSX) +// static +bool BookmarkNodeData::ClipboardContainsBookmarks() { + return ui::Clipboard::GetForCurrentThread()->IsFormatAvailable( + ui::Clipboard::GetFormatType(kClipboardFormatString), + ui::CLIPBOARD_TYPE_COPY_PASTE); +} +#endif + +bool BookmarkNodeData::ReadFromVector( + const std::vector<const BookmarkNode*>& nodes) { + Clear(); + + if (nodes.empty()) + return false; + + for (size_t i = 0; i < nodes.size(); ++i) + elements.push_back(Element(nodes[i])); + + return true; +} + +bool BookmarkNodeData::ReadFromTuple(const GURL& url, + const base::string16& title) { + Clear(); + + if (!url.is_valid()) + return false; + + Element element; + element.title = title; + element.url = url; + element.is_url = true; + + elements.push_back(element); + + return true; +} + +#if !defined(OS_MACOSX) +void BookmarkNodeData::WriteToClipboard(ui::ClipboardType clipboard_type) { + DCHECK(clipboard_type == ui::CLIPBOARD_TYPE_COPY_PASTE || + clipboard_type == ui::CLIPBOARD_TYPE_SELECTION); + ui::ScopedClipboardWriter scw(clipboard_type); + + // If there is only one element and it is a URL, write the URL to the + // clipboard. + if (has_single_url()) { + const base::string16& title = elements[0].title; + const std::string url = elements[0].url.spec(); + + scw.WriteBookmark(title, url); + + // Don't call scw.WriteHyperlink() here, since some rich text editors will + // change fonts when such data is pasted in; besides, most such editors + // auto-linkify at some point anyway. + + // Also write the URL to the clipboard as text so that it can be pasted + // into text fields. We use WriteText instead of WriteURL because we don't + // want to clobber the X clipboard when the user copies out of the omnibox + // on Linux (on Windows and Mac, there is no difference between these + // functions). + scw.WriteText(base::UTF8ToUTF16(url)); + } else { + // We have either more than one URL, a folder, or a combination of URLs + // and folders. + base::string16 text; + for (size_t i = 0; i < size(); i++) { + text += i == 0 ? base::ASCIIToUTF16("") : base::ASCIIToUTF16("\n"); + if (!elements[i].is_url) { + // Then it's a folder. Only copy the name of the folder. + const base::string16 title = elements[i].title; + text += title; + } else { + const base::string16 url = base::UTF8ToUTF16(elements[i].url.spec()); + text += url; + } + } + scw.WriteText(text); + } + + base::Pickle pickle; + WriteToPickle(base::FilePath(), &pickle); + scw.WritePickledData(pickle, + ui::Clipboard::GetFormatType(kClipboardFormatString)); +} + +bool BookmarkNodeData::ReadFromClipboard(ui::ClipboardType type) { + DCHECK_EQ(type, ui::CLIPBOARD_TYPE_COPY_PASTE); + std::string data; + ui::Clipboard* clipboard = ui::Clipboard::GetForCurrentThread(); + clipboard->ReadData(ui::Clipboard::GetFormatType(kClipboardFormatString), + &data); + + if (!data.empty()) { + base::Pickle pickle(data.data(), static_cast<int>(data.size())); + if (ReadFromPickle(&pickle)) + return true; + } + + base::string16 title; + std::string url; + clipboard->ReadBookmark(&title, &url); + if (!url.empty()) { + Element element; + element.is_url = true; + element.url = GURL(url); + element.title = title; + + elements.clear(); + elements.push_back(element); + return true; + } + + return false; +} +#endif + +void BookmarkNodeData::WriteToPickle(const base::FilePath& profile_path, + base::Pickle* pickle) const { + profile_path.WriteToPickle(pickle); + pickle->WriteUInt32(static_cast<uint32_t>(size())); + + for (size_t i = 0; i < size(); ++i) + elements[i].WriteToPickle(pickle); +} + +bool BookmarkNodeData::ReadFromPickle(base::Pickle* pickle) { + base::PickleIterator data_iterator(*pickle); + uint32_t element_count; + if (profile_path_.ReadFromPickle(&data_iterator) && + data_iterator.ReadUInt32(&element_count)) { + std::vector<Element> tmp_elements; + tmp_elements.resize(element_count); + for (size_t i = 0; i < element_count; ++i) { + if (!tmp_elements[i].ReadFromPickle(&data_iterator)) { + return false; + } + } + elements.swap(tmp_elements); + } + + return true; +} + +std::vector<const BookmarkNode*> BookmarkNodeData::GetNodes( + BookmarkModel* model, + const base::FilePath& profile_path) const { + std::vector<const BookmarkNode*> nodes; + + if (!IsFromProfilePath(profile_path)) + return nodes; + + for (size_t i = 0; i < size(); ++i) { + const BookmarkNode* node = GetBookmarkNodeByID(model, elements[i].id_); + if (!node) { + nodes.clear(); + return nodes; + } + nodes.push_back(node); + } + return nodes; +} + +const BookmarkNode* BookmarkNodeData::GetFirstNode( + BookmarkModel* model, + const base::FilePath& profile_path) const { + std::vector<const BookmarkNode*> nodes = GetNodes(model, profile_path); + return nodes.size() == 1 ? nodes[0] : NULL; +} + +void BookmarkNodeData::Clear() { + profile_path_.clear(); + elements.clear(); +} + +void BookmarkNodeData::SetOriginatingProfilePath( + const base::FilePath& profile_path) { + DCHECK(profile_path_.empty()); + profile_path_ = profile_path; +} + +bool BookmarkNodeData::IsFromProfilePath( + const base::FilePath& profile_path) const { + // An empty path means the data is not associated with any profile. + return !profile_path_.empty() && profile_path_ == profile_path; +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_node_data.h b/chromium/components/bookmarks/browser/bookmark_node_data.h new file mode 100644 index 00000000000..cbad37cd9a9 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_node_data.h @@ -0,0 +1,193 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_NODE_DATA_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_NODE_DATA_H_ + +#include <stddef.h> +#include <stdint.h> + +#include <vector> + +#include "base/files/file_path.h" +#include "base/strings/string16.h" +#include "base/time/time.h" +#include "components/bookmarks/browser/bookmark_node.h" +#include "ui/base/clipboard/clipboard_types.h" +#include "url/gurl.h" + +#if defined(TOOLKIT_VIEWS) +#include "ui/base/clipboard/clipboard.h" +#endif + +namespace base { +class Pickle; +class PickleIterator; +} + +#if defined(TOOLKIT_VIEWS) +namespace ui { +class OSExchangeData; +} +#endif + +namespace bookmarks { + +class BookmarkModel; + +// BookmarkNodeData is used to represent the following: +// +// . A single URL. +// . A single node from the bookmark model. +// . A set of nodes from the bookmark model. +// +// BookmarkNodeData is used by bookmark related views to represent a dragged +// bookmark or bookmarks. +// +// Typical usage when writing data for a drag is: +// BookmarkNodeData data(node_user_is_dragging); +// data.Write(os_exchange_data_for_drag); +// +// Typical usage to read is: +// BookmarkNodeData data; +// if (data.Read(os_exchange_data)) +// // data is valid, contents are in elements. + +struct BookmarkNodeData { + // Element represents a single node. + struct Element { + Element(); + explicit Element(const BookmarkNode* node); + Element(const Element& other); + ~Element(); + + // If true, this element represents a URL. + bool is_url; + + // The URL, only valid if is_url is true. + GURL url; + + // Title of the entry, used for both urls and folders. + base::string16 title; + + // Date of when this node was created. + base::Time date_added; + + // Date of the last modification. Only used for folders. + base::Time date_folder_modified; + + // Children, only used for non-URL nodes. + std::vector<Element> children; + + // Meta info for the bookmark node. + BookmarkNode::MetaInfoMap meta_info_map; + + int64_t id() const { return id_; } + + private: + friend struct BookmarkNodeData; + + // For reading/writing this Element. + void WriteToPickle(base::Pickle* pickle) const; + bool ReadFromPickle(base::PickleIterator* iterator); + + // ID of the node. + int64_t id_; + }; + + // The MIME type for the clipboard format for BookmarkNodeData. + static const char kClipboardFormatString[]; + + BookmarkNodeData(); + BookmarkNodeData(const BookmarkNodeData& other); + + // Created a BookmarkNodeData populated from the arguments. + explicit BookmarkNodeData(const BookmarkNode* node); + explicit BookmarkNodeData(const std::vector<const BookmarkNode*>& nodes); + + ~BookmarkNodeData(); + +#if defined(TOOLKIT_VIEWS) + static const ui::Clipboard::FormatType& GetBookmarkFormatType(); +#endif + + static bool ClipboardContainsBookmarks(); + + // Reads bookmarks from the given vector. + bool ReadFromVector(const std::vector<const BookmarkNode*>& nodes); + + // Creates a single-bookmark DragData from url/title pair. + bool ReadFromTuple(const GURL& url, const base::string16& title); + + // Writes bookmarks to the specified clipboard. + void WriteToClipboard(ui::ClipboardType type); + + // Reads bookmarks from the specified clipboard. Prefers data written via + // WriteToClipboard() but will also attempt to read a plain bookmark. + bool ReadFromClipboard(ui::ClipboardType type); + +#if defined(TOOLKIT_VIEWS) + // Writes elements to data. If there is only one element and it is a URL + // the URL and title are written to the clipboard in a format other apps can + // use. + // |profile_path| is used to identify which profile the data came from. Use an + // empty path to indicate that the data is not associated with any profile. + void Write(const base::FilePath& profile_path, + ui::OSExchangeData* data) const; + + // Restores this data from the clipboard, returning true on success. + bool Read(const ui::OSExchangeData& data); +#endif + + // Writes the data for a drag to |pickle|. + void WriteToPickle(const base::FilePath& profile_path, + base::Pickle* pickle) const; + + // Reads the data for a drag from a |pickle|. + bool ReadFromPickle(base::Pickle* pickle); + + // Returns the nodes represented by this DragData. If this DragData was + // created from the same profile then the nodes from the model are returned. + // If the nodes can't be found (may have been deleted), an empty vector is + // returned. + std::vector<const BookmarkNode*> GetNodes( + BookmarkModel* model, + const base::FilePath& profile_path) const; + + // Convenience for getting the first node. Returns NULL if the data doesn't + // match any nodes or there is more than one node. + const BookmarkNode* GetFirstNode(BookmarkModel* model, + const base::FilePath& profile_path) const; + + // Do we contain valid data? + bool is_valid() const { return !elements.empty(); } + + // Returns true if there is a single url. + bool has_single_url() const { return size() == 1 && elements[0].is_url; } + + // Number of elements. + size_t size() const { return elements.size(); } + + // Clears the data. + void Clear(); + + // Sets |profile_path_|. This is useful for the constructors/readers that + // don't set it. This should only be called if the profile path is not + // already set. + void SetOriginatingProfilePath(const base::FilePath& profile_path); + + // Returns true if this data is from the specified profile path. + bool IsFromProfilePath(const base::FilePath& profile_path) const; + + // The actual elements written to the clipboard. + std::vector<Element> elements; + + private: + // Path of the profile we originated from. + base::FilePath profile_path_; +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_NODE_DATA_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_node_data_ios.cc b/chromium/components/bookmarks/browser/bookmark_node_data_ios.cc new file mode 100644 index 00000000000..44c45cbfe27 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_node_data_ios.cc @@ -0,0 +1,26 @@ +// 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/bookmarks/browser/bookmark_node_data.h" + +#include "base/logging.h" + +namespace bookmarks { + +// static +bool BookmarkNodeData::ClipboardContainsBookmarks() { + NOTREACHED(); + return false; +} + +void BookmarkNodeData::WriteToClipboard(ui::ClipboardType type) { + NOTREACHED(); +} + +bool BookmarkNodeData::ReadFromClipboard(ui::ClipboardType type) { + NOTREACHED(); + return false; +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_node_data_mac.cc b/chromium/components/bookmarks/browser/bookmark_node_data_mac.cc new file mode 100644 index 00000000000..2dde55e777f --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_node_data_mac.cc @@ -0,0 +1,30 @@ +// 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/bookmarks/browser/bookmark_node_data.h" + +#include "components/bookmarks/browser/bookmark_pasteboard_helper_mac.h" + +namespace bookmarks { + +// static +bool BookmarkNodeData::ClipboardContainsBookmarks() { + return PasteboardContainsBookmarks(ui::CLIPBOARD_TYPE_COPY_PASTE); +} + +void BookmarkNodeData::WriteToClipboard(ui::ClipboardType type) { + WriteBookmarksToPasteboard(type, elements, profile_path_); +} + +bool BookmarkNodeData::ReadFromClipboard(ui::ClipboardType type) { + base::FilePath file_path; + if (ReadBookmarksFromPasteboard(type, elements, &file_path)) { + profile_path_ = file_path; + return true; + } + + return false; +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_node_data_unittest.cc b/chromium/components/bookmarks/browser/bookmark_node_data_unittest.cc new file mode 100644 index 00000000000..fc457ce608e --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_node_data_unittest.cc @@ -0,0 +1,406 @@ +// 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/bookmarks/browser/bookmark_node_data.h" + +#include "base/files/scoped_temp_dir.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/string16.h" +#include "base/strings/utf_string_conversions.h" +#include "components/bookmarks/browser/bookmark_model.h" +#include "components/bookmarks/test/bookmark_test_helpers.h" +#include "components/bookmarks/test/test_bookmark_client.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/clipboard/clipboard.h" +#include "ui/base/dragdrop/os_exchange_data.h" +#include "ui/events/platform/platform_event_source.h" +#include "url/gurl.h" + +using base::ASCIIToUTF16; + +namespace bookmarks { + +class BookmarkNodeDataTest : public testing::Test { + public: + BookmarkNodeDataTest() {} + + void SetUp() override { + event_source_ = ui::PlatformEventSource::CreateDefault(); + model_ = TestBookmarkClient::CreateModel(); + test::WaitForBookmarkModelToLoad(model_.get()); + bool success = profile_dir_.CreateUniqueTempDir(); + ASSERT_TRUE(success); + } + + void TearDown() override { + model_.reset(); + event_source_.reset(); + bool success = profile_dir_.Delete(); + ASSERT_TRUE(success); + ui::Clipboard::DestroyClipboardForCurrentThread(); + } + + const base::FilePath& GetProfilePath() const { return profile_dir_.path(); } + + BookmarkModel* model() { return model_.get(); } + + protected: + ui::Clipboard& clipboard() { return *ui::Clipboard::GetForCurrentThread(); } + + private: + base::ScopedTempDir profile_dir_; + scoped_ptr<BookmarkModel> model_; + scoped_ptr<ui::PlatformEventSource> event_source_; + base::MessageLoopForUI loop_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkNodeDataTest); +}; + +namespace { + +ui::OSExchangeData::Provider* CloneProvider(const ui::OSExchangeData& data) { + return data.provider().Clone(); +} + +} // namespace + +// Makes sure BookmarkNodeData is initially invalid. +TEST_F(BookmarkNodeDataTest, InitialState) { + BookmarkNodeData data; + EXPECT_FALSE(data.is_valid()); +} + +// Makes sure reading bogus data leaves the BookmarkNodeData invalid. +TEST_F(BookmarkNodeDataTest, BogusRead) { + ui::OSExchangeData data; + BookmarkNodeData drag_data; + EXPECT_FALSE(drag_data.Read(ui::OSExchangeData(CloneProvider(data)))); + EXPECT_FALSE(drag_data.is_valid()); +} + +// Writes a URL to the clipboard and make sure BookmarkNodeData can correctly +// read it. +TEST_F(BookmarkNodeDataTest, JustURL) { + const GURL url("http://google.com"); + const base::string16 title(ASCIIToUTF16("google.com")); + + ui::OSExchangeData data; + data.SetURL(url, title); + + BookmarkNodeData drag_data; + EXPECT_TRUE(drag_data.Read(ui::OSExchangeData(CloneProvider(data)))); + EXPECT_TRUE(drag_data.is_valid()); + ASSERT_EQ(1u, drag_data.size()); + EXPECT_TRUE(drag_data.elements[0].is_url); + EXPECT_EQ(url, drag_data.elements[0].url); + EXPECT_EQ(title, drag_data.elements[0].title); + EXPECT_TRUE(drag_data.elements[0].date_added.is_null()); + EXPECT_TRUE(drag_data.elements[0].date_folder_modified.is_null()); + EXPECT_EQ(0u, drag_data.elements[0].children.size()); +} + +TEST_F(BookmarkNodeDataTest, URL) { + // Write a single node representing a URL to the clipboard. + const BookmarkNode* root = model()->bookmark_bar_node(); + GURL url(GURL("http://foo.com")); + const base::string16 title(ASCIIToUTF16("foo.com")); + const BookmarkNode* node = model()->AddURL(root, 0, title, url); + BookmarkNodeData drag_data(node); + EXPECT_TRUE(drag_data.is_valid()); + ASSERT_EQ(1u, drag_data.size()); + EXPECT_TRUE(drag_data.elements[0].is_url); + EXPECT_EQ(url, drag_data.elements[0].url); + EXPECT_EQ(title, drag_data.elements[0].title); + EXPECT_EQ(node->date_added(), drag_data.elements[0].date_added); + EXPECT_EQ(node->date_folder_modified(), + drag_data.elements[0].date_folder_modified); + ui::OSExchangeData data; + drag_data.Write(GetProfilePath(), &data); + + // Now read the data back in. + ui::OSExchangeData data2(CloneProvider(data)); + BookmarkNodeData read_data; + EXPECT_TRUE(read_data.Read(data2)); + EXPECT_TRUE(read_data.is_valid()); + ASSERT_EQ(1u, read_data.size()); + EXPECT_TRUE(read_data.elements[0].is_url); + EXPECT_EQ(url, read_data.elements[0].url); + EXPECT_EQ(title, read_data.elements[0].title); + EXPECT_TRUE(read_data.elements[0].date_added.is_null()); + EXPECT_TRUE(read_data.elements[0].date_folder_modified.is_null()); + EXPECT_TRUE(read_data.GetFirstNode(model(), GetProfilePath()) == node); + + // Make sure asking for the node with a different profile returns NULL. + base::ScopedTempDir other_profile_dir; + EXPECT_TRUE(other_profile_dir.CreateUniqueTempDir()); + EXPECT_TRUE(read_data.GetFirstNode(model(), other_profile_dir.path()) == + NULL); + + // Writing should also put the URL and title on the clipboard. + GURL read_url; + base::string16 read_title; + EXPECT_TRUE(data2.GetURLAndTitle( + ui::OSExchangeData::CONVERT_FILENAMES, &read_url, &read_title)); + EXPECT_EQ(url, read_url); + EXPECT_EQ(title, read_title); +} + +// Tests writing a folder to the clipboard. +TEST_F(BookmarkNodeDataTest, Folder) { + const BookmarkNode* root = model()->bookmark_bar_node(); + const BookmarkNode* g1 = model()->AddFolder(root, 0, ASCIIToUTF16("g1")); + model()->AddFolder(g1, 0, ASCIIToUTF16("g11")); + const BookmarkNode* g12 = model()->AddFolder(g1, 0, ASCIIToUTF16("g12")); + + BookmarkNodeData drag_data(g12); + EXPECT_TRUE(drag_data.is_valid()); + ASSERT_EQ(1u, drag_data.size()); + EXPECT_EQ(g12->GetTitle(), drag_data.elements[0].title); + EXPECT_FALSE(drag_data.elements[0].is_url); + EXPECT_EQ(g12->date_added(), drag_data.elements[0].date_added); + EXPECT_EQ(g12->date_folder_modified(), + drag_data.elements[0].date_folder_modified); + + ui::OSExchangeData data; + drag_data.Write(GetProfilePath(), &data); + + // Now read the data back in. + ui::OSExchangeData data2(CloneProvider(data)); + BookmarkNodeData read_data; + EXPECT_TRUE(read_data.Read(data2)); + EXPECT_TRUE(read_data.is_valid()); + ASSERT_EQ(1u, read_data.size()); + EXPECT_EQ(g12->GetTitle(), read_data.elements[0].title); + EXPECT_FALSE(read_data.elements[0].is_url); + EXPECT_TRUE(read_data.elements[0].date_added.is_null()); + EXPECT_TRUE(read_data.elements[0].date_folder_modified.is_null()); + + // We should get back the same node when asking for the same profile. + const BookmarkNode* r_g12 = read_data.GetFirstNode(model(), GetProfilePath()); + EXPECT_TRUE(g12 == r_g12); + + // A different profile should return NULL for the node. + base::ScopedTempDir other_profile_dir; + EXPECT_TRUE(other_profile_dir.CreateUniqueTempDir()); + EXPECT_TRUE(read_data.GetFirstNode(model(), other_profile_dir.path()) == + NULL); +} + +// Tests reading/writing a folder with children. +TEST_F(BookmarkNodeDataTest, FolderWithChild) { + const BookmarkNode* root = model()->bookmark_bar_node(); + const BookmarkNode* folder = model()->AddFolder(root, 0, ASCIIToUTF16("g1")); + + GURL url(GURL("http://foo.com")); + const base::string16 title(ASCIIToUTF16("blah2")); + + model()->AddURL(folder, 0, title, url); + + BookmarkNodeData drag_data(folder); + + ui::OSExchangeData data; + drag_data.Write(GetProfilePath(), &data); + + // Now read the data back in. + ui::OSExchangeData data2(CloneProvider(data)); + BookmarkNodeData read_data; + EXPECT_TRUE(read_data.Read(data2)); + ASSERT_EQ(1u, read_data.size()); + ASSERT_EQ(1u, read_data.elements[0].children.size()); + const BookmarkNodeData::Element& read_child = + read_data.elements[0].children[0]; + + EXPECT_TRUE(read_child.is_url); + EXPECT_EQ(title, read_child.title); + EXPECT_EQ(url, read_child.url); + EXPECT_TRUE(read_data.elements[0].date_added.is_null()); + EXPECT_TRUE(read_data.elements[0].date_folder_modified.is_null()); + EXPECT_TRUE(read_child.is_url); + + // And make sure we get the node back. + const BookmarkNode* r_folder = + read_data.GetFirstNode(model(), GetProfilePath()); + EXPECT_TRUE(folder == r_folder); +} + +// Tests reading/writing of multiple nodes. +TEST_F(BookmarkNodeDataTest, MultipleNodes) { + const BookmarkNode* root = model()->bookmark_bar_node(); + const BookmarkNode* folder = model()->AddFolder(root, 0, ASCIIToUTF16("g1")); + + GURL url(GURL("http://foo.com")); + const base::string16 title(ASCIIToUTF16("blah2")); + + const BookmarkNode* url_node = model()->AddURL(folder, 0, title, url); + + // Write the nodes to the clipboard. + std::vector<const BookmarkNode*> nodes; + nodes.push_back(folder); + nodes.push_back(url_node); + BookmarkNodeData drag_data(nodes); + ui::OSExchangeData data; + drag_data.Write(GetProfilePath(), &data); + + // Read the data back in. + ui::OSExchangeData data2(CloneProvider(data)); + BookmarkNodeData read_data; + EXPECT_TRUE(read_data.Read(data2)); + EXPECT_TRUE(read_data.is_valid()); + ASSERT_EQ(2u, read_data.size()); + ASSERT_EQ(1u, read_data.elements[0].children.size()); + EXPECT_TRUE(read_data.elements[0].date_added.is_null()); + EXPECT_TRUE(read_data.elements[0].date_folder_modified.is_null()); + + const BookmarkNodeData::Element& read_folder = read_data.elements[0]; + EXPECT_FALSE(read_folder.is_url); + EXPECT_EQ(ASCIIToUTF16("g1"), read_folder.title); + EXPECT_EQ(1u, read_folder.children.size()); + + const BookmarkNodeData::Element& read_url = read_data.elements[1]; + EXPECT_TRUE(read_url.is_url); + EXPECT_EQ(title, read_url.title); + EXPECT_EQ(0u, read_url.children.size()); + + // And make sure we get the node back. + std::vector<const BookmarkNode*> read_nodes = + read_data.GetNodes(model(), GetProfilePath()); + ASSERT_EQ(2u, read_nodes.size()); + EXPECT_TRUE(read_nodes[0] == folder); + EXPECT_TRUE(read_nodes[1] == url_node); + + // Asking for the first node should return NULL with more than one element + // present. + EXPECT_TRUE(read_data.GetFirstNode(model(), GetProfilePath()) == NULL); +} + +TEST_F(BookmarkNodeDataTest, WriteToClipboardURL) { + BookmarkNodeData data; + GURL url(GURL("http://foo.com")); + const base::string16 title(ASCIIToUTF16("blah")); + + data.ReadFromTuple(url, title); + data.WriteToClipboard(ui::CLIPBOARD_TYPE_COPY_PASTE); + + // Now read the data back in. + base::string16 clipboard_result; + clipboard().ReadText(ui::CLIPBOARD_TYPE_COPY_PASTE, &clipboard_result); + EXPECT_EQ(base::UTF8ToUTF16(url.spec()), clipboard_result); +} + +TEST_F(BookmarkNodeDataTest, WriteToClipboardMultipleURLs) { + BookmarkNodeData data; + const BookmarkNode* root = model()->bookmark_bar_node(); + GURL url(GURL("http://foo.com")); + const base::string16 title(ASCIIToUTF16("blah")); + GURL url2(GURL("http://bar.com")); + const base::string16 title2(ASCIIToUTF16("blah2")); + const BookmarkNode* url_node = model()->AddURL(root, 0, title, url); + const BookmarkNode* url_node2 = model()->AddURL(root, 1, title2, url2); + std::vector<const BookmarkNode*> nodes; + nodes.push_back(url_node); + nodes.push_back(url_node2); + + data.ReadFromVector(nodes); + data.WriteToClipboard(ui::CLIPBOARD_TYPE_COPY_PASTE); + + // Now read the data back in. + base::string16 combined_text; + base::string16 new_line = base::ASCIIToUTF16("\n"); + combined_text = base::UTF8ToUTF16(url.spec()) + new_line + + base::UTF8ToUTF16(url2.spec()); + base::string16 clipboard_result; + clipboard().ReadText(ui::CLIPBOARD_TYPE_COPY_PASTE, &clipboard_result); + EXPECT_EQ(combined_text, clipboard_result); +} + +TEST_F(BookmarkNodeDataTest, WriteToClipboardEmptyFolder) { + BookmarkNodeData data; + const BookmarkNode* root = model()->bookmark_bar_node(); + const BookmarkNode* folder = model()->AddFolder(root, 0, ASCIIToUTF16("g1")); + std::vector<const BookmarkNode*> nodes; + nodes.push_back(folder); + + data.ReadFromVector(nodes); + data.WriteToClipboard(ui::CLIPBOARD_TYPE_COPY_PASTE); + + // Now read the data back in. + base::string16 clipboard_result; + clipboard().ReadText(ui::CLIPBOARD_TYPE_COPY_PASTE, &clipboard_result); + EXPECT_EQ(base::ASCIIToUTF16("g1"), clipboard_result); +} + +TEST_F(BookmarkNodeDataTest, WriteToClipboardFolderWithChildren) { + BookmarkNodeData data; + const BookmarkNode* root = model()->bookmark_bar_node(); + const BookmarkNode* folder = model()->AddFolder(root, 0, ASCIIToUTF16("g1")); + GURL url(GURL("http://foo.com")); + const base::string16 title(ASCIIToUTF16("blah")); + model()->AddURL(folder, 0, title, url); + std::vector<const BookmarkNode*> nodes; + nodes.push_back(folder); + + data.ReadFromVector(nodes); + data.WriteToClipboard(ui::CLIPBOARD_TYPE_COPY_PASTE); + + // Now read the data back in. + base::string16 clipboard_result; + clipboard().ReadText(ui::CLIPBOARD_TYPE_COPY_PASTE, &clipboard_result); + EXPECT_EQ(base::ASCIIToUTF16("g1"), clipboard_result); +} + +TEST_F(BookmarkNodeDataTest, WriteToClipboardFolderAndURL) { + BookmarkNodeData data; + GURL url(GURL("http://foo.com")); + const base::string16 title(ASCIIToUTF16("blah")); + const BookmarkNode* root = model()->bookmark_bar_node(); + const BookmarkNode* url_node = model()->AddURL(root, 0, title, url); + const BookmarkNode* folder = model()->AddFolder(root, 0, ASCIIToUTF16("g1")); + std::vector<const BookmarkNode*> nodes; + nodes.push_back(url_node); + nodes.push_back(folder); + + data.ReadFromVector(nodes); + data.WriteToClipboard(ui::CLIPBOARD_TYPE_COPY_PASTE); + + // Now read the data back in. + base::string16 combined_text; + base::string16 new_line = base::ASCIIToUTF16("\n"); + base::string16 folder_title = ASCIIToUTF16("g1"); + combined_text = base::ASCIIToUTF16(url.spec()) + new_line + folder_title; + base::string16 clipboard_result; + clipboard().ReadText(ui::CLIPBOARD_TYPE_COPY_PASTE, &clipboard_result); + EXPECT_EQ(combined_text, clipboard_result); +} + +// Tests reading/writing of meta info. +TEST_F(BookmarkNodeDataTest, MetaInfo) { + // Create a node containing meta info. + const BookmarkNode* node = model()->AddURL(model()->other_node(), + 0, + ASCIIToUTF16("foo bar"), + GURL("http://www.google.com")); + model()->SetNodeMetaInfo(node, "somekey", "somevalue"); + model()->SetNodeMetaInfo(node, "someotherkey", "someothervalue"); + + BookmarkNodeData node_data(node); + ui::OSExchangeData data; + node_data.Write(GetProfilePath(), &data); + + // Read the data back in. + ui::OSExchangeData data2(CloneProvider(data)); + BookmarkNodeData read_data; + EXPECT_TRUE(read_data.Read(data2)); + EXPECT_TRUE(read_data.is_valid()); + ASSERT_EQ(1u, read_data.size()); + + // Verify that the read data contains the same meta info. + BookmarkNode::MetaInfoMap meta_info_map = read_data.elements[0].meta_info_map; + EXPECT_EQ(2u, meta_info_map.size()); + EXPECT_EQ("somevalue", meta_info_map["somekey"]); + EXPECT_EQ("someothervalue", meta_info_map["someotherkey"]); +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_node_data_views.cc b/chromium/components/bookmarks/browser/bookmark_node_data_views.cc new file mode 100644 index 00000000000..a56c0d2d842 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_node_data_views.cc @@ -0,0 +1,68 @@ +// 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/bookmarks/browser/bookmark_node_data.h" + +#include "base/logging.h" +#include "base/pickle.h" +#include "base/strings/utf_string_conversions.h" +#include "ui/base/dragdrop/os_exchange_data.h" +#include "url/url_constants.h" + +namespace bookmarks { + +// static +const ui::Clipboard::FormatType& BookmarkNodeData::GetBookmarkFormatType() { + CR_DEFINE_STATIC_LOCAL( + ui::Clipboard::FormatType, + format, + (ui::Clipboard::GetFormatType(BookmarkNodeData::kClipboardFormatString))); + + return format; +} + +void BookmarkNodeData::Write(const base::FilePath& profile_path, + ui::OSExchangeData* data) const { + DCHECK(data); + + // If there is only one element and it is a URL, write the URL to the + // clipboard. + if (has_single_url()) { + if (elements[0].url.SchemeIs(url::kJavaScriptScheme)) { + data->SetString(base::UTF8ToUTF16(elements[0].url.spec())); + } else { + data->SetURL(elements[0].url, elements[0].title); + } + } + + base::Pickle data_pickle; + WriteToPickle(profile_path, &data_pickle); + + data->SetPickledData(GetBookmarkFormatType(), data_pickle); +} + +bool BookmarkNodeData::Read(const ui::OSExchangeData& data) { + elements.clear(); + + profile_path_.clear(); + + if (data.HasCustomFormat(GetBookmarkFormatType())) { + base::Pickle drag_data_pickle; + if (data.GetPickledData(GetBookmarkFormatType(), &drag_data_pickle)) { + if (!ReadFromPickle(&drag_data_pickle)) + return false; + } + } else { + // See if there is a URL on the clipboard. + GURL url; + base::string16 title; + if (data.GetURLAndTitle( + ui::OSExchangeData::CONVERT_FILENAMES, &url, &title)) + ReadFromTuple(url, title); + } + + return is_valid(); +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_pasteboard_helper_mac.h b/chromium/components/bookmarks/browser/bookmark_pasteboard_helper_mac.h new file mode 100644 index 00000000000..839fd510b3a --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_pasteboard_helper_mac.h @@ -0,0 +1,55 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_PASTEBOARD_HELPER_MAC_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_PASTEBOARD_HELPER_MAC_H_ + +#include "components/bookmarks/browser/bookmark_node_data.h" + +#if defined(__OBJC__) +@class NSPasteboardItem; +@class NSString; +#endif // __OBJC__ + +namespace base { +class FilePath; +} + +namespace bookmarks { + +// This set of functions lets C++ code interact with the cocoa pasteboard and +// dragging methods. + +#if defined(__OBJC__) +// Creates a NSPasteboardItem that contains all the data for the bookmarks. +NSPasteboardItem* PasteboardItemFromBookmarks( + const std::vector<BookmarkNodeData::Element>& elements, + const base::FilePath& profile_path); +#endif // __OBJC__ + +// Writes a set of bookmark elements from a profile to the specified pasteboard. +void WriteBookmarksToPasteboard( + ui::ClipboardType type, + const std::vector<BookmarkNodeData::Element>& elements, + const base::FilePath& profile_path); + +// Reads a set of bookmark elements from the specified pasteboard. +bool ReadBookmarksFromPasteboard( + ui::ClipboardType type, + std::vector<BookmarkNodeData::Element>& elements, + base::FilePath* profile_path); + +// Returns true if the specified pasteboard contains any sort of bookmark +// elements. It currently does not consider a plaintext url a valid bookmark. +bool PasteboardContainsBookmarks(ui::ClipboardType type); + +} // namespace bookmarks + +#if defined(__OBJC__) +// Pasteboard type for dictionary containing bookmark structure consisting +// of individual bookmark nodes and/or bookmark folders. +extern "C" NSString* const kBookmarkDictionaryListPboardType; +#endif // __OBJC__ + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_PASTEBOARD_HELPER_MAC_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_pasteboard_helper_mac.mm b/chromium/components/bookmarks/browser/bookmark_pasteboard_helper_mac.mm new file mode 100644 index 00000000000..c37f806ea1c --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_pasteboard_helper_mac.mm @@ -0,0 +1,310 @@ +// 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/bookmarks/browser/bookmark_pasteboard_helper_mac.h" + +#import <Cocoa/Cocoa.h> +#include <stddef.h> +#include <stdint.h> + +#include "base/files/file_path.h" +#include "base/strings/sys_string_conversions.h" +#include "components/bookmarks/browser/bookmark_node.h" +#include "ui/base/clipboard/clipboard.h" +#include "ui/base/clipboard/clipboard_util_mac.h" + +NSString* const kBookmarkDictionaryListPboardType = + @"com.google.chrome.BookmarkDictionaryListPboardType"; + +namespace bookmarks { + +namespace { + +// Pasteboard type used to store profile path to determine which profile +// a set of bookmarks came from. +NSString* const kChromiumProfilePathPboardType = + @"com.google.chrome.ChromiumProfilePathPboardType"; + +// Internal bookmark ID for a bookmark node. Used only when moving inside +// of one profile. +NSString* const kChromiumBookmarkId = @"ChromiumBookmarkId"; + +// Internal bookmark meta info dictionary for a bookmark node. +NSString* const kChromiumBookmarkMetaInfo = @"ChromiumBookmarkMetaInfo"; + +// Keys for the type of node in BookmarkDictionaryListPboardType. +NSString* const kWebBookmarkType = @"WebBookmarkType"; + +NSString* const kWebBookmarkTypeList = @"WebBookmarkTypeList"; + +NSString* const kWebBookmarkTypeLeaf = @"WebBookmarkTypeLeaf"; + +BookmarkNode::MetaInfoMap MetaInfoMapFromDictionary(NSDictionary* dictionary) { + BookmarkNode::MetaInfoMap meta_info_map; + + for (NSString* key in dictionary) { + meta_info_map[base::SysNSStringToUTF8(key)] = + base::SysNSStringToUTF8([dictionary objectForKey:key]); + } + + return meta_info_map; +} + +void ConvertPlistToElements(NSArray* input, + std::vector<BookmarkNodeData::Element>& elements) { + NSUInteger len = [input count]; + for (NSUInteger i = 0; i < len; ++i) { + NSDictionary* pboardBookmark = [input objectAtIndex:i]; + scoped_ptr<BookmarkNode> new_node(new BookmarkNode(GURL())); + int64_t node_id = + [[pboardBookmark objectForKey:kChromiumBookmarkId] longLongValue]; + new_node->set_id(node_id); + + NSDictionary* metaInfoDictionary = + [pboardBookmark objectForKey:kChromiumBookmarkMetaInfo]; + if (metaInfoDictionary) + new_node->SetMetaInfoMap(MetaInfoMapFromDictionary(metaInfoDictionary)); + + BOOL is_folder = [[pboardBookmark objectForKey:kWebBookmarkType] + isEqualToString:kWebBookmarkTypeList]; + if (is_folder) { + new_node->set_type(BookmarkNode::FOLDER); + NSString* title = [pboardBookmark objectForKey:@"Title"]; + new_node->SetTitle(base::SysNSStringToUTF16(title)); + } else { + new_node->set_type(BookmarkNode::URL); + NSDictionary* uriDictionary = + [pboardBookmark objectForKey:@"URIDictionary"]; + NSString* title = [uriDictionary objectForKey:@"title"]; + NSString* urlString = [pboardBookmark objectForKey:@"URLString"]; + new_node->SetTitle(base::SysNSStringToUTF16(title)); + new_node->set_url(GURL(base::SysNSStringToUTF8(urlString))); + } + BookmarkNodeData::Element e = BookmarkNodeData::Element(new_node.get()); + if (is_folder) { + ConvertPlistToElements([pboardBookmark objectForKey:@"Children"], + e.children); + } + elements.push_back(e); + } +} + +bool ReadBookmarkDictionaryListPboardType( + NSPasteboard* pb, + std::vector<BookmarkNodeData::Element>& elements) { + NSString* uti = ui::ClipboardUtil::UTIForPasteboardType( + kBookmarkDictionaryListPboardType); + NSArray* bookmarks = [pb propertyListForType:uti]; + if (!bookmarks) + return false; + ConvertPlistToElements(bookmarks, elements); + return true; +} + +bool ReadWebURLsWithTitlesPboardType( + NSPasteboard* pb, + std::vector<BookmarkNodeData::Element>& elements) { + NSArray* urlsArr = nil; + NSArray* titlesArr = nil; + if (!ui::ClipboardUtil::URLsAndTitlesFromPasteboard(pb, &urlsArr, &titlesArr)) + return false; + + NSUInteger len = [titlesArr count]; + for (NSUInteger i = 0; i < len; ++i) { + base::string16 title = + base::SysNSStringToUTF16([titlesArr objectAtIndex:i]); + std::string url = base::SysNSStringToUTF8([urlsArr objectAtIndex:i]); + if (!url.empty()) { + BookmarkNodeData::Element element; + element.is_url = true; + element.url = GURL(url); + element.title = title; + elements.push_back(element); + } + } + return true; +} + +NSDictionary* DictionaryFromBookmarkMetaInfo( + const BookmarkNode::MetaInfoMap& meta_info_map) { + NSMutableDictionary* dictionary = [NSMutableDictionary dictionary]; + + for (BookmarkNode::MetaInfoMap::const_iterator it = meta_info_map.begin(); + it != meta_info_map.end(); ++it) { + [dictionary setObject:base::SysUTF8ToNSString(it->second) + forKey:base::SysUTF8ToNSString(it->first)]; + } + + return dictionary; +} + +NSArray* GetPlistForBookmarkList( + const std::vector<BookmarkNodeData::Element>& elements) { + NSMutableArray* plist = [NSMutableArray array]; + for (size_t i = 0; i < elements.size(); ++i) { + BookmarkNodeData::Element element = elements[i]; + NSDictionary* metaInfoDictionary = + DictionaryFromBookmarkMetaInfo(element.meta_info_map); + if (element.is_url) { + NSString* title = base::SysUTF16ToNSString(element.title); + NSString* url = base::SysUTF8ToNSString(element.url.spec()); + int64_t elementId = element.id(); + NSNumber* idNum = [NSNumber numberWithLongLong:elementId]; + NSDictionary* uriDictionary = [NSDictionary dictionaryWithObjectsAndKeys: + title, @"title", nil]; + NSDictionary* object = [NSDictionary dictionaryWithObjectsAndKeys: + uriDictionary, @"URIDictionary", + url, @"URLString", + kWebBookmarkTypeLeaf, kWebBookmarkType, + idNum, kChromiumBookmarkId, + metaInfoDictionary, kChromiumBookmarkMetaInfo, + nil]; + [plist addObject:object]; + } else { + NSString* title = base::SysUTF16ToNSString(element.title); + NSArray* children = GetPlistForBookmarkList(element.children); + int64_t elementId = element.id(); + NSNumber* idNum = [NSNumber numberWithLongLong:elementId]; + NSDictionary* object = [NSDictionary dictionaryWithObjectsAndKeys: + title, @"Title", + children, @"Children", + kWebBookmarkTypeList, kWebBookmarkType, + idNum, kChromiumBookmarkId, + metaInfoDictionary, kChromiumBookmarkMetaInfo, + nil]; + [plist addObject:object]; + } + } + return plist; +} + +void WriteBookmarkDictionaryListPboardType( + NSPasteboardItem* item, + const std::vector<BookmarkNodeData::Element>& elements) { + NSArray* plist = GetPlistForBookmarkList(elements); + NSString* uti = ui::ClipboardUtil::UTIForPasteboardType( + kBookmarkDictionaryListPboardType); + [item setPropertyList:plist forType:uti]; +} + +void FillFlattenedArraysForBookmarks( + const std::vector<BookmarkNodeData::Element>& elements, + NSMutableArray* url_titles, + NSMutableArray* urls, + NSMutableArray* toplevel_string_data) { + for (const BookmarkNodeData::Element& element : elements) { + NSString* title = base::SysUTF16ToNSString(element.title); + if (element.is_url) { + NSString* url = base::SysUTF8ToNSString(element.url.spec()); + [url_titles addObject:title]; + [urls addObject:url]; + if (toplevel_string_data) + [toplevel_string_data addObject:url]; + } else { + if (toplevel_string_data) + [toplevel_string_data addObject:title]; + FillFlattenedArraysForBookmarks(element.children, url_titles, urls, nil); + } + } +} + +base::scoped_nsobject<NSPasteboardItem> WriteSimplifiedBookmarkTypes( + const std::vector<BookmarkNodeData::Element>& elements) { + NSMutableArray* url_titles = [NSMutableArray array]; + NSMutableArray* urls = [NSMutableArray array]; + NSMutableArray* toplevel_string_data = [NSMutableArray array]; + FillFlattenedArraysForBookmarks( + elements, url_titles, urls, toplevel_string_data); + + base::scoped_nsobject<NSPasteboardItem> item; + if ([urls count] > 0) { + if ([urls count] == 1) { + item = ui::ClipboardUtil::PasteboardItemFromUrl([urls firstObject], + [url_titles firstObject]); + } else { + item = ui::ClipboardUtil::PasteboardItemFromUrls(urls, url_titles); + } + } + + if (!item) { + item = [[NSPasteboardItem alloc] init]; + } + + [item setString:[toplevel_string_data componentsJoinedByString:@"\n"] + forType:NSPasteboardTypeString]; + return item; +} + +NSPasteboard* PasteboardFromType(ui::ClipboardType type) { + NSString* type_string = nil; + switch (type) { + case ui::CLIPBOARD_TYPE_COPY_PASTE: + type_string = NSGeneralPboard; + break; + case ui::CLIPBOARD_TYPE_DRAG: + type_string = NSDragPboard; + break; + case ui::CLIPBOARD_TYPE_SELECTION: + NOTREACHED(); + break; + } + + return [NSPasteboard pasteboardWithName:type_string]; +} + +} // namespace + +NSPasteboardItem* PasteboardItemFromBookmarks( + const std::vector<BookmarkNodeData::Element>& elements, + const base::FilePath& profile_path) { + base::scoped_nsobject<NSPasteboardItem> item = + WriteSimplifiedBookmarkTypes(elements); + + WriteBookmarkDictionaryListPboardType(item, elements); + + NSString* uti = + ui::ClipboardUtil::UTIForPasteboardType(kChromiumProfilePathPboardType); + [item setString:base::SysUTF8ToNSString(profile_path.value()) forType:uti]; + return [[item retain] autorelease]; +} + +void WriteBookmarksToPasteboard( + ui::ClipboardType type, + const std::vector<BookmarkNodeData::Element>& elements, + const base::FilePath& profile_path) { + if (elements.empty()) + return; + + NSPasteboardItem* item = PasteboardItemFromBookmarks(elements, profile_path); + NSPasteboard* pb = PasteboardFromType(type); + [pb clearContents]; + [pb writeObjects:@[ item ]]; +} + +bool ReadBookmarksFromPasteboard( + ui::ClipboardType type, + std::vector<BookmarkNodeData::Element>& elements, + base::FilePath* profile_path) { + NSPasteboard* pb = PasteboardFromType(type); + + elements.clear(); + NSString* uti = + ui::ClipboardUtil::UTIForPasteboardType(kChromiumProfilePathPboardType); + NSString* profile = [pb stringForType:uti]; + *profile_path = base::FilePath(base::SysNSStringToUTF8(profile)); + return ReadBookmarkDictionaryListPboardType(pb, elements) || + ReadWebURLsWithTitlesPboardType(pb, elements); +} + +bool PasteboardContainsBookmarks(ui::ClipboardType type) { + NSPasteboard* pb = PasteboardFromType(type); + + NSArray* availableTypes = @[ + ui::ClipboardUtil::UTIForWebURLsAndTitles(), + ui::ClipboardUtil::UTIForPasteboardType(kBookmarkDictionaryListPboardType) + ]; + return [pb availableTypeFromArray:availableTypes] != nil; +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_storage.cc b/chromium/components/bookmarks/browser/bookmark_storage.cc new file mode 100644 index 00000000000..3a184206cfb --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_storage.cc @@ -0,0 +1,233 @@ +// 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/bookmarks/browser/bookmark_storage.h" + +#include <stddef.h> +#include <algorithm> +#include <utility> + +#include "base/bind.h" +#include "base/compiler_specific.h" +#include "base/files/file_util.h" +#include "base/json/json_file_value_serializer.h" +#include "base/json/json_string_value_serializer.h" +#include "base/metrics/histogram_macros.h" +#include "base/sequenced_task_runner.h" +#include "base/time/time.h" +#include "components/bookmarks/browser/bookmark_codec.h" +#include "components/bookmarks/browser/bookmark_index.h" +#include "components/bookmarks/browser/bookmark_model.h" +#include "components/bookmarks/common/bookmark_constants.h" + +using base::TimeTicks; + +namespace bookmarks { + +namespace { + +// Extension used for backup files (copy of main file created during startup). +const base::FilePath::CharType kBackupExtension[] = FILE_PATH_LITERAL("bak"); + +// How often we save. +const int kSaveDelayMS = 2500; + +void BackupCallback(const base::FilePath& path) { + base::FilePath backup_path = path.ReplaceExtension(kBackupExtension); + base::CopyFile(path, backup_path); +} + +// Adds node to the model's index, recursing through all children as well. +void AddBookmarksToIndex(BookmarkLoadDetails* details, + BookmarkNode* node) { + if (node->is_url()) { + if (node->url().is_valid()) + details->index()->Add(node); + } else { + for (int i = 0; i < node->child_count(); ++i) + AddBookmarksToIndex(details, node->GetChild(i)); + } +} + +void LoadCallback(const base::FilePath& path, + const base::WeakPtr<BookmarkStorage>& storage, + scoped_ptr<BookmarkLoadDetails> details, + base::SequencedTaskRunner* task_runner) { + bool load_index = false; + bool bookmark_file_exists = base::PathExists(path); + if (bookmark_file_exists) { + JSONFileValueDeserializer deserializer(path); + scoped_ptr<base::Value> root = deserializer.Deserialize(NULL, NULL); + + if (root.get()) { + // Building the index can take a while, so we do it on the background + // thread. + int64_t max_node_id = 0; + BookmarkCodec codec; + TimeTicks start_time = TimeTicks::Now(); + codec.Decode(details->bb_node(), details->other_folder_node(), + details->mobile_folder_node(), &max_node_id, *root.get()); + details->set_max_id(std::max(max_node_id, details->max_id())); + details->set_computed_checksum(codec.computed_checksum()); + details->set_stored_checksum(codec.stored_checksum()); + details->set_ids_reassigned(codec.ids_reassigned()); + details->set_model_meta_info_map(codec.model_meta_info_map()); + details->set_model_sync_transaction_version( + codec.model_sync_transaction_version()); + UMA_HISTOGRAM_TIMES("Bookmarks.DecodeTime", + TimeTicks::Now() - start_time); + + load_index = true; + } + } + + // Load any extra root nodes now, after the IDs have been potentially + // reassigned. + details->LoadExtraNodes(); + + // Load the index if there are any bookmarks in the extra nodes. + const BookmarkPermanentNodeList& extra_nodes = details->extra_nodes(); + for (size_t i = 0; i < extra_nodes.size(); ++i) { + if (!extra_nodes[i]->empty()) { + load_index = true; + break; + } + } + + if (load_index) { + TimeTicks start_time = TimeTicks::Now(); + AddBookmarksToIndex(details.get(), details->bb_node()); + AddBookmarksToIndex(details.get(), details->other_folder_node()); + AddBookmarksToIndex(details.get(), details->mobile_folder_node()); + for (size_t i = 0; i < extra_nodes.size(); ++i) + AddBookmarksToIndex(details.get(), extra_nodes[i]); + UMA_HISTOGRAM_TIMES("Bookmarks.CreateBookmarkIndexTime", + TimeTicks::Now() - start_time); + } + + task_runner->PostTask(FROM_HERE, + base::Bind(&BookmarkStorage::OnLoadFinished, storage, + base::Passed(&details))); +} + +} // namespace + +// BookmarkLoadDetails --------------------------------------------------------- + +BookmarkLoadDetails::BookmarkLoadDetails( + BookmarkPermanentNode* bb_node, + BookmarkPermanentNode* other_folder_node, + BookmarkPermanentNode* mobile_folder_node, + const LoadExtraCallback& load_extra_callback, + BookmarkIndex* index, + int64_t max_id) + : bb_node_(bb_node), + other_folder_node_(other_folder_node), + mobile_folder_node_(mobile_folder_node), + load_extra_callback_(load_extra_callback), + index_(index), + model_sync_transaction_version_( + BookmarkNode::kInvalidSyncTransactionVersion), + max_id_(max_id), + ids_reassigned_(false) {} + +BookmarkLoadDetails::~BookmarkLoadDetails() { +} + +void BookmarkLoadDetails::LoadExtraNodes() { + if (!load_extra_callback_.is_null()) + extra_nodes_ = load_extra_callback_.Run(&max_id_); +} + +// BookmarkStorage ------------------------------------------------------------- + +BookmarkStorage::BookmarkStorage( + BookmarkModel* model, + const base::FilePath& profile_path, + base::SequencedTaskRunner* sequenced_task_runner) + : model_(model), + writer_(profile_path.Append(kBookmarksFileName), + sequenced_task_runner, + base::TimeDelta::FromMilliseconds(kSaveDelayMS)), + sequenced_task_runner_(sequenced_task_runner), + weak_factory_(this) { +} + +BookmarkStorage::~BookmarkStorage() { + if (writer_.HasPendingWrite()) + writer_.DoScheduledWrite(); +} + +void BookmarkStorage::LoadBookmarks( + scoped_ptr<BookmarkLoadDetails> details, + const scoped_refptr<base::SequencedTaskRunner>& task_runner) { + sequenced_task_runner_->PostTask( + FROM_HERE, + base::Bind(&LoadCallback, writer_.path(), weak_factory_.GetWeakPtr(), + base::Passed(&details), base::RetainedRef(task_runner))); +} + +void BookmarkStorage::ScheduleSave() { + switch (backup_state_) { + case BACKUP_NONE: + backup_state_ = BACKUP_DISPATCHED; + sequenced_task_runner_->PostTaskAndReply( + FROM_HERE, base::Bind(&BackupCallback, writer_.path()), + base::Bind(&BookmarkStorage::OnBackupFinished, + weak_factory_.GetWeakPtr())); + return; + case BACKUP_DISPATCHED: + // Currently doing a backup which will call this function when done. + return; + case BACKUP_ATTEMPTED: + writer_.ScheduleWrite(this); + return; + } + NOTREACHED(); +} + +void BookmarkStorage::OnBackupFinished() { + backup_state_ = BACKUP_ATTEMPTED; + ScheduleSave(); +} + +void BookmarkStorage::BookmarkModelDeleted() { + // We need to save now as otherwise by the time SaveNow is invoked + // the model is gone. + if (writer_.HasPendingWrite()) + SaveNow(); + model_ = NULL; +} + +bool BookmarkStorage::SerializeData(std::string* output) { + BookmarkCodec codec; + scoped_ptr<base::Value> value(codec.Encode(model_)); + JSONStringValueSerializer serializer(output); + serializer.set_pretty_print(true); + return serializer.Serialize(*(value.get())); +} + +void BookmarkStorage::OnLoadFinished(scoped_ptr<BookmarkLoadDetails> details) { + if (!model_) + return; + + model_->DoneLoading(std::move(details)); +} + +bool BookmarkStorage::SaveNow() { + if (!model_ || !model_->loaded()) { + // We should only get here if we have a valid model and it's finished + // loading. + NOTREACHED(); + return false; + } + + scoped_ptr<std::string> data(new std::string); + if (!SerializeData(data.get())) + return false; + writer_.WriteNow(std::move(data)); + return true; +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_storage.h b/chromium/components/bookmarks/browser/bookmark_storage.h new file mode 100644 index 00000000000..cc72f675cde --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_storage.h @@ -0,0 +1,213 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_STORAGE_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_STORAGE_H_ + +#include <stdint.h> + +#include <string> +#include <vector> + +#include "base/callback_forward.h" +#include "base/files/file_path.h" +#include "base/files/important_file_writer.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/scoped_vector.h" +#include "base/memory/weak_ptr.h" +#include "components/bookmarks/browser/bookmark_node.h" + +namespace base { +class SequencedTaskRunner; +} + +namespace bookmarks { + +class BookmarkIndex; +class BookmarkModel; + +// A list of BookmarkPermanentNodes that owns them. +typedef ScopedVector<BookmarkPermanentNode> BookmarkPermanentNodeList; + +// A callback that generates a BookmarkPermanentNodeList, given a max ID to +// use. The max ID argument will be updated after any new nodes have been +// created and assigned IDs. +typedef base::Callback<BookmarkPermanentNodeList(int64_t*)> LoadExtraCallback; + +// BookmarkLoadDetails is used by BookmarkStorage when loading bookmarks. +// BookmarkModel creates a BookmarkLoadDetails and passes it (including +// ownership) to BookmarkStorage. BookmarkStorage loads the bookmarks (and +// index) in the background thread, then calls back to the BookmarkModel (on +// the main thread) when loading is done, passing ownership back to the +// BookmarkModel. While loading BookmarkModel does not maintain references to +// the contents of the BookmarkLoadDetails, this ensures we don't have any +// threading problems. +class BookmarkLoadDetails { + public: + BookmarkLoadDetails(BookmarkPermanentNode* bb_node, + BookmarkPermanentNode* other_folder_node, + BookmarkPermanentNode* mobile_folder_node, + const LoadExtraCallback& load_extra_callback, + BookmarkIndex* index, + int64_t max_id); + ~BookmarkLoadDetails(); + + void LoadExtraNodes(); + + BookmarkPermanentNode* bb_node() { return bb_node_.get(); } + BookmarkPermanentNode* release_bb_node() { return bb_node_.release(); } + BookmarkPermanentNode* mobile_folder_node() { + return mobile_folder_node_.get(); + } + BookmarkPermanentNode* release_mobile_folder_node() { + return mobile_folder_node_.release(); + } + BookmarkPermanentNode* other_folder_node() { + return other_folder_node_.get(); + } + BookmarkPermanentNode* release_other_folder_node() { + return other_folder_node_.release(); + } + const BookmarkPermanentNodeList& extra_nodes() { + return extra_nodes_; + } + void release_extra_nodes(std::vector<BookmarkPermanentNode*>* extra_nodes) { + extra_nodes_.release(extra_nodes); + } + BookmarkIndex* index() { return index_.get(); } + BookmarkIndex* release_index() { return index_.release(); } + + const BookmarkNode::MetaInfoMap& model_meta_info_map() const { + return model_meta_info_map_; + } + void set_model_meta_info_map(const BookmarkNode::MetaInfoMap& meta_info_map) { + model_meta_info_map_ = meta_info_map; + } + + int64_t model_sync_transaction_version() const { + return model_sync_transaction_version_; + } + void set_model_sync_transaction_version(int64_t sync_transaction_version) { + model_sync_transaction_version_ = sync_transaction_version; + } + + // Max id of the nodes. + void set_max_id(int64_t max_id) { max_id_ = max_id; } + int64_t max_id() const { return max_id_; } + + // Computed checksum. + void set_computed_checksum(const std::string& value) { + computed_checksum_ = value; + } + const std::string& computed_checksum() const { return computed_checksum_; } + + // Stored checksum. + void set_stored_checksum(const std::string& value) { + stored_checksum_ = value; + } + const std::string& stored_checksum() const { return stored_checksum_; } + + // Whether ids were reassigned. IDs are reassigned during decoding if the + // checksum of the file doesn't match, some IDs are missing or not + // unique. Basically, if the user modified the bookmarks directly we'll + // reassign the ids to ensure they are unique. + void set_ids_reassigned(bool value) { ids_reassigned_ = value; } + bool ids_reassigned() const { return ids_reassigned_; } + + private: + scoped_ptr<BookmarkPermanentNode> bb_node_; + scoped_ptr<BookmarkPermanentNode> other_folder_node_; + scoped_ptr<BookmarkPermanentNode> mobile_folder_node_; + LoadExtraCallback load_extra_callback_; + BookmarkPermanentNodeList extra_nodes_; + scoped_ptr<BookmarkIndex> index_; + BookmarkNode::MetaInfoMap model_meta_info_map_; + int64_t model_sync_transaction_version_; + int64_t max_id_; + std::string computed_checksum_; + std::string stored_checksum_; + bool ids_reassigned_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkLoadDetails); +}; + +// BookmarkStorage handles reading/write the bookmark bar model. The +// BookmarkModel uses the BookmarkStorage to load bookmarks from disk, as well +// as notifying the BookmarkStorage every time the model changes. +// +// Internally BookmarkStorage uses BookmarkCodec to do the actual read/write. +class BookmarkStorage : public base::ImportantFileWriter::DataSerializer { + public: + // Creates a BookmarkStorage for the specified model. The data will be loaded + // from and saved to a location derived from |profile_path|. The IO code will + // be executed as a task in |sequenced_task_runner|. + BookmarkStorage(BookmarkModel* model, + const base::FilePath& profile_path, + base::SequencedTaskRunner* sequenced_task_runner); + ~BookmarkStorage() override; + + // Loads the bookmarks into the model, notifying the model when done. This + // takes ownership of |details| and send the |OnLoadFinished| callback from + // a task in |task_runner|. See BookmarkLoadDetails for details. + void LoadBookmarks( + scoped_ptr<BookmarkLoadDetails> details, + const scoped_refptr<base::SequencedTaskRunner>& task_runner); + + // Schedules saving the bookmark bar model to disk. + void ScheduleSave(); + + // Notification the bookmark bar model is going to be deleted. If there is + // a pending save, it is saved immediately. + void BookmarkModelDeleted(); + + // Callback from backend after loading the bookmark file. + void OnLoadFinished(scoped_ptr<BookmarkLoadDetails> details); + + // ImportantFileWriter::DataSerializer implementation. + bool SerializeData(std::string* output) override; + + private: + // The state of the bookmark file backup. We lazily backup this file in order + // to reduce disk writes until absolutely necessary. Will also leave the + // backup unchanged if the browser starts & quits w/o changing bookmarks. + enum BackupState { + // No attempt has yet been made to backup the bookmarks file. + BACKUP_NONE, + // A request to backup the bookmarks file has been posted, but not yet + // fulfilled. + BACKUP_DISPATCHED, + // The bookmarks file has been backed up (or at least attempted). + BACKUP_ATTEMPTED + }; + + // Serializes the data and schedules save using ImportantFileWriter. + // Returns true on successful serialization. + bool SaveNow(); + + // Callback from backend after creation of backup file. + void OnBackupFinished(); + + // The model. The model is NULL once BookmarkModelDeleted has been invoked. + BookmarkModel* model_; + + // Helper to write bookmark data safely. + base::ImportantFileWriter writer_; + + // The state of the backup file creation which is created lazily just before + // the first scheduled save. + BackupState backup_state_ = BACKUP_NONE; + + // Sequenced task runner where file I/O operations will be performed at. + scoped_refptr<base::SequencedTaskRunner> sequenced_task_runner_; + + base::WeakPtrFactory<BookmarkStorage> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkStorage); +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_STORAGE_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_undo_delegate.h b/chromium/components/bookmarks/browser/bookmark_undo_delegate.h new file mode 100644 index 00000000000..f0e2d7fe517 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_undo_delegate.h @@ -0,0 +1,35 @@ +// Copyright 2015 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 COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_UNDO_DELEGATE_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_UNDO_DELEGATE_H_ + +#include "base/memory/scoped_ptr.h" + +namespace bookmarks { + +class BookmarkModel; +class BookmarkNode; +class BookmarkUndoProvider; + +// Delegate to handle bookmark change events in order to support undo when +// requested. +class BookmarkUndoDelegate { + public: + virtual ~BookmarkUndoDelegate() {} + + // Sets the provider that will do the undo work. + virtual void SetUndoProvider(BookmarkUndoProvider* provider) = 0; + + // Called when |node| was removed from |parent| at position |index|. + virtual void OnBookmarkNodeRemoved(BookmarkModel* model, + const BookmarkNode* parent, + int index, + scoped_ptr<BookmarkNode> node) = 0; +}; + + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_UNDO_DELEGATE_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_undo_provider.h b/chromium/components/bookmarks/browser/bookmark_undo_provider.h new file mode 100644 index 00000000000..a76602b1a73 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_undo_provider.h @@ -0,0 +1,29 @@ +// Copyright 2015 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 COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_UNDO_PROVIDER_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_UNDO_PROVIDER_H_ + +#include "base/memory/scoped_ptr.h" + +namespace bookmarks { + +class BookmarkNode; + +// The interface for providing undo support. +class BookmarkUndoProvider { + public: + // Restores the previously removed |node| at |parent| in the specified + // |index|. + virtual void RestoreRemovedNode(const BookmarkNode* parent, + int index, + scoped_ptr<BookmarkNode> node) = 0; + + protected: + virtual ~BookmarkUndoProvider() {} +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_UNDO_PROVIDER_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_utils.cc b/chromium/components/bookmarks/browser/bookmark_utils.cc new file mode 100644 index 00000000000..6fef26dca62 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_utils.cc @@ -0,0 +1,572 @@ +// 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/bookmarks/browser/bookmark_utils.h" + +#include <stdint.h> +#include <utility> + +#include "base/bind.h" +#include "base/containers/hash_tables.h" +#include "base/files/file_path.h" +#include "base/i18n/case_conversion.h" +#include "base/i18n/string_search.h" +#include "base/macros.h" +#include "base/metrics/user_metrics_action.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "build/build_config.h" +#include "components/bookmarks/browser/bookmark_client.h" +#include "components/bookmarks/browser/bookmark_model.h" +#include "components/bookmarks/browser/scoped_group_bookmark_actions.h" +#include "components/bookmarks/common/bookmark_pref_names.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/prefs/pref_service.h" +#include "components/query_parser/query_parser.h" +#include "components/url_formatter/url_formatter.h" +#include "ui/base/clipboard/clipboard.h" +#include "ui/base/models/tree_node_iterator.h" +#include "url/gurl.h" + +using base::Time; + +namespace bookmarks { + +namespace { + +// The maximum length of URL or title returned by the Cleanup functions. +const size_t kCleanedUpUrlMaxLength = 1024u; +const size_t kCleanedUpTitleMaxLength = 1024u; + +void CloneBookmarkNodeImpl(BookmarkModel* model, + const BookmarkNodeData::Element& element, + const BookmarkNode* parent, + int index_to_add_at, + bool reset_node_times) { + // Make sure to not copy non clonable keys. + BookmarkNode::MetaInfoMap meta_info_map = element.meta_info_map; + for (const std::string& key : model->non_cloned_keys()) + meta_info_map.erase(key); + + if (element.is_url) { + Time date_added = reset_node_times ? Time::Now() : element.date_added; + DCHECK(!date_added.is_null()); + + model->AddURLWithCreationTimeAndMetaInfo(parent, + index_to_add_at, + element.title, + element.url, + date_added, + &meta_info_map); + } else { + const BookmarkNode* cloned_node = model->AddFolderWithMetaInfo( + parent, index_to_add_at, element.title, &meta_info_map); + if (!reset_node_times) { + DCHECK(!element.date_folder_modified.is_null()); + model->SetDateFolderModified(cloned_node, element.date_folder_modified); + } + for (int i = 0; i < static_cast<int>(element.children.size()); ++i) + CloneBookmarkNodeImpl(model, element.children[i], cloned_node, i, + reset_node_times); + } +} + +// Comparison function that compares based on date modified of the two nodes. +bool MoreRecentlyModified(const BookmarkNode* n1, const BookmarkNode* n2) { + return n1->date_folder_modified() > n2->date_folder_modified(); +} + +// Returns true if |text| contains each string in |words|. This is used when +// searching for bookmarks. +bool DoesBookmarkTextContainWords(const base::string16& text, + const std::vector<base::string16>& words) { + for (size_t i = 0; i < words.size(); ++i) { + if (!base::i18n::StringSearchIgnoringCaseAndAccents( + words[i], text, NULL, NULL)) { + return false; + } + } + return true; +} + +// Returns true if |node|s title or url contains the strings in |words|. +bool DoesBookmarkContainWords(const BookmarkNode* node, + const std::vector<base::string16>& words) { + return DoesBookmarkTextContainWords(node->GetTitle(), words) || + DoesBookmarkTextContainWords(base::UTF8ToUTF16(node->url().spec()), + words) || + DoesBookmarkTextContainWords( + url_formatter::FormatUrl( + node->url(), url_formatter::kFormatUrlOmitNothing, + net::UnescapeRule::NORMAL, NULL, NULL, NULL), + words); +} + +// This is used with a tree iterator to skip subtrees which are not visible. +bool PruneInvisibleFolders(const BookmarkNode* node) { + return !node->IsVisible(); +} + +// This traces parents up to root, determines if node is contained in a +// selected folder. +bool HasSelectedAncestor(BookmarkModel* model, + const std::vector<const BookmarkNode*>& selected_nodes, + const BookmarkNode* node) { + if (!node || model->is_permanent_node(node)) + return false; + + for (size_t i = 0; i < selected_nodes.size(); ++i) + if (node->id() == selected_nodes[i]->id()) + return true; + + return HasSelectedAncestor(model, selected_nodes, node->parent()); +} + +const BookmarkNode* GetNodeByID(const BookmarkNode* node, int64_t id) { + if (node->id() == id) + return node; + + for (int i = 0, child_count = node->child_count(); i < child_count; ++i) { + const BookmarkNode* result = GetNodeByID(node->GetChild(i), id); + if (result) + return result; + } + return NULL; +} + +// Attempts to shorten a URL safely (i.e., by preventing the end of the URL +// from being in the middle of an escape sequence) to no more than +// kCleanedUpUrlMaxLength characters, returning the result. +std::string TruncateUrl(const std::string& url) { + if (url.length() <= kCleanedUpUrlMaxLength) + return url; + + // If we're in the middle of an escape sequence, truncate just before it. + if (url[kCleanedUpUrlMaxLength - 1] == '%') + return url.substr(0, kCleanedUpUrlMaxLength - 1); + if (url[kCleanedUpUrlMaxLength - 2] == '%') + return url.substr(0, kCleanedUpUrlMaxLength - 2); + + return url.substr(0, kCleanedUpUrlMaxLength); +} + +// Returns the URL from the clipboard. If there is no URL an empty URL is +// returned. +GURL GetUrlFromClipboard() { + base::string16 url_text; +#if !defined(OS_IOS) + ui::Clipboard::GetForCurrentThread()->ReadText(ui::CLIPBOARD_TYPE_COPY_PASTE, + &url_text); +#endif + return GURL(url_text); +} + +class VectorIterator { + public: + explicit VectorIterator(std::vector<const BookmarkNode*>* nodes) + : nodes_(nodes), current_(nodes->begin()) {} + bool has_next() { return (current_ != nodes_->end()); } + const BookmarkNode* Next() { + const BookmarkNode* result = *current_; + ++current_; + return result; + } + + private: + std::vector<const BookmarkNode*>* nodes_; + std::vector<const BookmarkNode*>::iterator current_; + + DISALLOW_COPY_AND_ASSIGN(VectorIterator); +}; + +template <class type> +void GetBookmarksMatchingPropertiesImpl( + type& iterator, + BookmarkModel* model, + const QueryFields& query, + const std::vector<base::string16>& query_words, + size_t max_count, + std::vector<const BookmarkNode*>* nodes) { + while (iterator.has_next()) { + const BookmarkNode* node = iterator.Next(); + if ((!query_words.empty() && + !DoesBookmarkContainWords(node, query_words)) || + model->is_permanent_node(node)) { + continue; + } + if (query.title && node->GetTitle() != *query.title) + continue; + + nodes->push_back(node); + if (nodes->size() == max_count) + return; + } +} + +} // namespace + +QueryFields::QueryFields() {} +QueryFields::~QueryFields() {} + +void CloneBookmarkNode(BookmarkModel* model, + const std::vector<BookmarkNodeData::Element>& elements, + const BookmarkNode* parent, + int index_to_add_at, + bool reset_node_times) { + if (!parent->is_folder() || !model) { + NOTREACHED(); + return; + } + for (size_t i = 0; i < elements.size(); ++i) { + CloneBookmarkNodeImpl(model, elements[i], parent, + index_to_add_at + static_cast<int>(i), + reset_node_times); + } +} + +void CopyToClipboard(BookmarkModel* model, + const std::vector<const BookmarkNode*>& nodes, + bool remove_nodes) { + if (nodes.empty()) + return; + + // Create array of selected nodes with descendants filtered out. + std::vector<const BookmarkNode*> filtered_nodes; + for (size_t i = 0; i < nodes.size(); ++i) + if (!HasSelectedAncestor(model, nodes, nodes[i]->parent())) + filtered_nodes.push_back(nodes[i]); + + BookmarkNodeData(filtered_nodes). + WriteToClipboard(ui::CLIPBOARD_TYPE_COPY_PASTE); + + if (remove_nodes) { + ScopedGroupBookmarkActions group_cut(model); + for (size_t i = 0; i < filtered_nodes.size(); ++i) { + int index = filtered_nodes[i]->parent()->GetIndexOf(filtered_nodes[i]); + if (index > -1) + model->Remove(filtered_nodes[i]); + } + } +} + +// Updates |title| such that |url| and |title| pair are unique among the +// children of |parent|. +void MakeTitleUnique(const BookmarkModel* model, + const BookmarkNode* parent, + const GURL& url, + base::string16* title) { + base::hash_set<base::string16> titles; + base::string16 original_title_lower = base::i18n::ToLower(*title); + for (int i = 0; i < parent->child_count(); i++) { + const BookmarkNode* node = parent->GetChild(i); + if (node->is_url() && (url == node->url()) && + base::StartsWith(base::i18n::ToLower(node->GetTitle()), + original_title_lower, + base::CompareCase::SENSITIVE)) { + titles.insert(node->GetTitle()); + } + } + + if (titles.find(*title) == titles.end()) + return; + + for (size_t i = 0; i < titles.size(); i++) { + const base::string16 new_title(*title + + base::ASCIIToUTF16(base::StringPrintf( + " (%lu)", (unsigned long)(i + 1)))); + if (titles.find(new_title) == titles.end()) { + *title = new_title; + return; + } + } + NOTREACHED(); +} + +void PasteFromClipboard(BookmarkModel* model, + const BookmarkNode* parent, + int index) { + if (!parent) + return; + + BookmarkNodeData bookmark_data; + if (!bookmark_data.ReadFromClipboard(ui::CLIPBOARD_TYPE_COPY_PASTE)) { + GURL url = GetUrlFromClipboard(); + if (!url.is_valid()) + return; + BookmarkNode node(url); + node.SetTitle(base::ASCIIToUTF16(url.spec())); + bookmark_data = BookmarkNodeData(&node); + } + if (index == -1) + index = parent->child_count(); + ScopedGroupBookmarkActions group_paste(model); + + if (bookmark_data.size() == 1 && + model->IsBookmarked(bookmark_data.elements[0].url)) { + MakeTitleUnique(model, + parent, + bookmark_data.elements[0].url, + &bookmark_data.elements[0].title); + } + + CloneBookmarkNode(model, bookmark_data.elements, parent, index, true); +} + +bool CanPasteFromClipboard(BookmarkModel* model, const BookmarkNode* node) { + if (!node || !model->client()->CanBeEditedByUser(node)) + return false; + return (BookmarkNodeData::ClipboardContainsBookmarks() || + GetUrlFromClipboard().is_valid()); +} + +std::vector<const BookmarkNode*> GetMostRecentlyModifiedUserFolders( + BookmarkModel* model, + size_t max_count) { + std::vector<const BookmarkNode*> nodes; + ui::TreeNodeIterator<const BookmarkNode> iterator( + model->root_node(), base::Bind(&PruneInvisibleFolders)); + + while (iterator.has_next()) { + const BookmarkNode* parent = iterator.Next(); + if (!model->client()->CanBeEditedByUser(parent)) + continue; + if (parent->is_folder() && parent->date_folder_modified() > Time()) { + if (max_count == 0) { + nodes.push_back(parent); + } else { + std::vector<const BookmarkNode*>::iterator i = + std::upper_bound(nodes.begin(), nodes.end(), parent, + &MoreRecentlyModified); + if (nodes.size() < max_count || i != nodes.end()) { + nodes.insert(i, parent); + while (nodes.size() > max_count) + nodes.pop_back(); + } + } + } // else case, the root node, which we don't care about or imported nodes + // (which have a time of 0). + } + + if (nodes.size() < max_count) { + // Add the permanent nodes if there is space. The permanent nodes are the + // only children of the root_node. + const BookmarkNode* root_node = model->root_node(); + + for (int i = 0; i < root_node->child_count(); ++i) { + const BookmarkNode* node = root_node->GetChild(i); + if (node->IsVisible() && model->client()->CanBeEditedByUser(node) && + std::find(nodes.begin(), nodes.end(), node) == nodes.end()) { + nodes.push_back(node); + + if (nodes.size() == max_count) + break; + } + } + } + return nodes; +} + +void GetMostRecentlyAddedEntries(BookmarkModel* model, + size_t count, + std::vector<const BookmarkNode*>* nodes) { + ui::TreeNodeIterator<const BookmarkNode> iterator(model->root_node()); + while (iterator.has_next()) { + const BookmarkNode* node = iterator.Next(); + if (node->is_url()) { + std::vector<const BookmarkNode*>::iterator insert_position = + std::upper_bound(nodes->begin(), nodes->end(), node, + &MoreRecentlyAdded); + if (nodes->size() < count || insert_position != nodes->end()) { + nodes->insert(insert_position, node); + while (nodes->size() > count) + nodes->pop_back(); + } + } + } +} + +bool MoreRecentlyAdded(const BookmarkNode* n1, const BookmarkNode* n2) { + return n1->date_added() > n2->date_added(); +} + +void GetBookmarksMatchingProperties(BookmarkModel* model, + const QueryFields& query, + size_t max_count, + std::vector<const BookmarkNode*>* nodes) { + std::vector<base::string16> query_words; + query_parser::QueryParser parser; + if (query.word_phrase_query) { + parser.ParseQueryWords(base::i18n::ToLower(*query.word_phrase_query), + query_parser::MatchingAlgorithm::DEFAULT, + &query_words); + if (query_words.empty()) + return; + } + + if (query.url) { + // Shortcut into the BookmarkModel if searching for URL. + GURL url(*query.url); + std::vector<const BookmarkNode*> url_matched_nodes; + if (url.is_valid()) + model->GetNodesByURL(url, &url_matched_nodes); + VectorIterator iterator(&url_matched_nodes); + GetBookmarksMatchingPropertiesImpl<VectorIterator>( + iterator, model, query, query_words, max_count, nodes); + } else { + ui::TreeNodeIterator<const BookmarkNode> iterator(model->root_node()); + GetBookmarksMatchingPropertiesImpl< + ui::TreeNodeIterator<const BookmarkNode>>( + iterator, model, query, query_words, max_count, nodes); + } +} + +void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) { + registry->RegisterBooleanPref( + prefs::kShowBookmarkBar, + false, + user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); + registry->RegisterBooleanPref(prefs::kEditBookmarksEnabled, true); + registry->RegisterBooleanPref( + prefs::kShowAppsShortcutInBookmarkBar, + true, + user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); + registry->RegisterBooleanPref( + prefs::kShowManagedBookmarksInBookmarkBar, + true, + user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); + RegisterManagedBookmarksPrefs(registry); +} + +void RegisterManagedBookmarksPrefs(PrefRegistrySimple* registry) { + // Don't sync this, as otherwise, due to a limitation in sync, it + // will cause a deadlock (see http://crbug.com/97955). If we truly + // want to sync the expanded state of folders, it should be part of + // bookmark sync itself (i.e., a property of the sync folder nodes). + registry->RegisterListPref(prefs::kBookmarkEditorExpandedNodes, + new base::ListValue); + registry->RegisterListPref(prefs::kManagedBookmarks); + registry->RegisterStringPref( + prefs::kManagedBookmarksFolderName, std::string()); + registry->RegisterListPref(prefs::kSupervisedBookmarks); +} + +const BookmarkNode* GetParentForNewNodes( + const BookmarkNode* parent, + const std::vector<const BookmarkNode*>& selection, + int* index) { + const BookmarkNode* real_parent = parent; + + if (selection.size() == 1 && selection[0]->is_folder()) + real_parent = selection[0]; + + if (index) { + if (selection.size() == 1 && selection[0]->is_url()) { + *index = real_parent->GetIndexOf(selection[0]) + 1; + if (*index == 0) { + // Node doesn't exist in parent, add to end. + NOTREACHED(); + *index = real_parent->child_count(); + } + } else { + *index = real_parent->child_count(); + } + } + + return real_parent; +} + +void DeleteBookmarkFolders(BookmarkModel* model, + const std::vector<int64_t>& ids) { + // Remove the folders that were removed. This has to be done after all the + // other changes have been committed. + for (std::vector<int64_t>::const_iterator iter = ids.begin(); + iter != ids.end(); + ++iter) { + const BookmarkNode* node = GetBookmarkNodeByID(model, *iter); + if (!node) + continue; + model->Remove(node); + } +} + +void AddIfNotBookmarked(BookmarkModel* model, + const GURL& url, + const base::string16& title) { + if (IsBookmarkedByUser(model, url)) + return; // Nothing to do, a user bookmark with that url already exists. + model->client()->RecordAction(base::UserMetricsAction("BookmarkAdded")); + const BookmarkNode* parent = model->GetParentForNewNodes(); + model->AddURL(parent, parent->child_count(), title, url); +} + +void RemoveAllBookmarks(BookmarkModel* model, const GURL& url) { + std::vector<const BookmarkNode*> bookmarks; + model->GetNodesByURL(url, &bookmarks); + + // Remove all the user bookmarks. + for (size_t i = 0; i < bookmarks.size(); ++i) { + const BookmarkNode* node = bookmarks[i]; + int index = node->parent()->GetIndexOf(node); + if (index > -1 && model->client()->CanBeEditedByUser(node)) + model->Remove(node); + } +} + +base::string16 CleanUpUrlForMatching( + const GURL& gurl, + base::OffsetAdjuster::Adjustments* adjustments) { + base::OffsetAdjuster::Adjustments tmp_adjustments; + return base::i18n::ToLower(url_formatter::FormatUrlWithAdjustments( + GURL(TruncateUrl(gurl.spec())), + url_formatter::kFormatUrlOmitUsernamePassword, + net::UnescapeRule::SPACES | net::UnescapeRule::PATH_SEPARATORS | + net::UnescapeRule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS, + nullptr, nullptr, adjustments ? adjustments : &tmp_adjustments)); +} + +base::string16 CleanUpTitleForMatching(const base::string16& title) { + return base::i18n::ToLower(title.substr(0u, kCleanedUpTitleMaxLength)); +} + +bool CanAllBeEditedByUser(BookmarkClient* client, + const std::vector<const BookmarkNode*>& nodes) { + for (size_t i = 0; i < nodes.size(); ++i) { + if (!client->CanBeEditedByUser(nodes[i])) + return false; + } + return true; +} + +bool IsBookmarkedByUser(BookmarkModel* model, const GURL& url) { + std::vector<const BookmarkNode*> nodes; + model->GetNodesByURL(url, &nodes); + for (size_t i = 0; i < nodes.size(); ++i) { + if (model->client()->CanBeEditedByUser(nodes[i])) + return true; + } + return false; +} + +const BookmarkNode* GetBookmarkNodeByID(const BookmarkModel* model, + int64_t id) { + // TODO(sky): TreeNode needs a method that visits all nodes using a predicate. + return GetNodeByID(model->root_node(), id); +} + +bool IsDescendantOf(const BookmarkNode* node, const BookmarkNode* root) { + return node && node->HasAncestor(root); +} + +bool HasDescendantsOf(const std::vector<const BookmarkNode*>& list, + const BookmarkNode* root) { + for (const BookmarkNode* node : list) { + if (IsDescendantOf(node, root)) + return true; + } + return false; +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/bookmark_utils.h b/chromium/components/bookmarks/browser/bookmark_utils.h new file mode 100644 index 00000000000..0a3a3730f9d --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_utils.h @@ -0,0 +1,167 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_UTILS_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_UTILS_H_ + +#include <stddef.h> +#include <stdint.h> + +#include <string> +#include <vector> + +#include "base/strings/string16.h" +#include "base/strings/utf_offset_string_conversions.h" +#include "components/bookmarks/browser/bookmark_node_data.h" +#include "components/prefs/pref_registry_simple.h" + +class GURL; + +namespace user_prefs { +class PrefRegistrySyncable; +} + +// A collection of bookmark utility functions used by various parts of the UI +// that show bookmarks (bookmark manager, bookmark bar view, ...) and other +// systems that involve indexing and searching bookmarks. +namespace bookmarks { + +class BookmarkClient; +class BookmarkModel; +class BookmarkNode; + +// Fields to use when finding matching bookmarks. +struct QueryFields { + QueryFields(); + ~QueryFields(); + + scoped_ptr<base::string16> word_phrase_query; + scoped_ptr<base::string16> url; + scoped_ptr<base::string16> title; +}; + +// Clones bookmark node, adding newly created nodes to |parent| starting at +// |index_to_add_at|. If |reset_node_times| is true cloned bookmarks and +// folders will receive new creation times and folder modification times +// instead of using the values stored in |elements|. +void CloneBookmarkNode(BookmarkModel* model, + const std::vector<BookmarkNodeData::Element>& elements, + const BookmarkNode* parent, + int index_to_add_at, + bool reset_node_times); + +// Copies nodes onto the clipboard. If |remove_nodes| is true the nodes are +// removed after copied to the clipboard. The nodes are copied in such a way +// that if pasted again copies are made. +void CopyToClipboard(BookmarkModel* model, + const std::vector<const BookmarkNode*>& nodes, + bool remove_nodes); + +// Pastes from the clipboard. The new nodes are added to |parent|, unless +// |parent| is null in which case this does nothing. The nodes are inserted +// at |index|. If |index| is -1 the nodes are added to the end. +void PasteFromClipboard(BookmarkModel* model, + const BookmarkNode* parent, + int index); + +// Returns true if the user can copy from the pasteboard. +bool CanPasteFromClipboard(BookmarkModel* model, const BookmarkNode* node); + +// Returns a vector containing up to |max_count| of the most recently modified +// user folders. This never returns an empty vector. +std::vector<const BookmarkNode*> GetMostRecentlyModifiedUserFolders( + BookmarkModel* model, size_t max_count); + +// Returns the most recently added bookmarks. This does not return folders, +// only nodes of type url. +void GetMostRecentlyAddedEntries(BookmarkModel* model, + size_t count, + std::vector<const BookmarkNode*>* nodes); + +// Returns true if |n1| was added more recently than |n2|. +bool MoreRecentlyAdded(const BookmarkNode* n1, const BookmarkNode* n2); + +// Returns up to |max_count| bookmarks from |model| whose url or title contain +// the text |query.word_phrase_query| and exactly match |query.url| and +// |query.title|, for all of the preceding fields that are not NULL. +void GetBookmarksMatchingProperties(BookmarkModel* model, + const QueryFields& query, + size_t max_count, + std::vector<const BookmarkNode*>* nodes); + +// Register user preferences for Bookmarks Bar. +void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); + +// Register managed bookmarks preferences. +void RegisterManagedBookmarksPrefs(PrefRegistrySimple* registry); + +// Returns the parent for newly created folders/bookmarks. If |selection| has +// one element and it is a folder, |selection[0]| is returned, otherwise +// |parent| is returned. If |index| is non-null it is set to the index newly +// added nodes should be added at. +const BookmarkNode* GetParentForNewNodes( + const BookmarkNode* parent, + const std::vector<const BookmarkNode*>& selection, + int* index); + +// Deletes the bookmark folders for the given list of |ids|. +void DeleteBookmarkFolders(BookmarkModel* model, + const std::vector<int64_t>& ids); + +// If there are no user bookmarks for url, a bookmark is created. +void AddIfNotBookmarked(BookmarkModel* model, + const GURL& url, + const base::string16& title); + +// Removes all bookmarks for the given |url|. +void RemoveAllBookmarks(BookmarkModel* model, const GURL& url); + +// Truncates an overly-long URL, unescapes it and interprets the +// characters as UTF-8 (both via url_formatter::FormatUrl()), and +// lower-cases it, returning the result. |adjustments|, if non-NULL, is +// set to reflect the transformations the URL spec underwent to become the +// return value. If a caller computes offsets (e.g., for the position +// of matched text) in this cleaned-up string, it can use |adjustments| +// to calculate the location of these offsets in the original string +// (via base::OffsetAdjuster::UnadjustOffsets()). This is useful if later +// the original string gets formatted in a different way for displaying. +// In this case, knowing the offsets in the original string will allow them +// to be properly translated to offsets in the newly-formatted string. +// +// The unescaping done by this function makes it possible to match substrings +// that were originally escaped for navigation; for example, if the user +// searched for "a&p", the query would be escaped as "a%26p", so without +// unescaping, an input string of "a&p" would no longer match this URL. Note +// that the resulting unescaped URL may not be directly navigable (which is +// why it was escaped to begin with). +base::string16 CleanUpUrlForMatching( + const GURL& gurl, + base::OffsetAdjuster::Adjustments* adjustments); + +// Returns the lower-cased title, possibly truncated if the original title +// is overly-long. +base::string16 CleanUpTitleForMatching(const base::string16& title); + +// Returns true if all the |nodes| can be edited by the user, +// as determined by BookmarkClient::CanBeEditedByUser(). +bool CanAllBeEditedByUser(BookmarkClient* client, + const std::vector<const BookmarkNode*>& nodes); + +// Returns true if |url| has a bookmark in the |model| that can be edited +// by the user. +bool IsBookmarkedByUser(BookmarkModel* model, const GURL& url); + +// Returns the node with |id|, or NULL if there is no node with |id|. +const BookmarkNode* GetBookmarkNodeByID(const BookmarkModel* model, int64_t id); + +// Returns true if |node| is a descendant of |root|. +bool IsDescendantOf(const BookmarkNode* node, const BookmarkNode* root); + +// Returns true if any node in |list| is a descendant of |root|. +bool HasDescendantsOf(const std::vector<const BookmarkNode*>& list, + const BookmarkNode* root); + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_BOOKMARK_UTILS_H_ diff --git a/chromium/components/bookmarks/browser/bookmark_utils_unittest.cc b/chromium/components/bookmarks/browser/bookmark_utils_unittest.cc new file mode 100644 index 00000000000..f27676d8041 --- /dev/null +++ b/chromium/components/bookmarks/browser/bookmark_utils_unittest.cc @@ -0,0 +1,606 @@ +// 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/bookmarks/browser/bookmark_utils.h" + +#include <stddef.h> +#include <utility> +#include <vector> + +#include "base/macros.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/utf_string_conversions.h" +#include "build/build_config.h" +#include "components/bookmarks/browser/base_bookmark_model_observer.h" +#include "components/bookmarks/browser/bookmark_model.h" +#include "components/bookmarks/browser/bookmark_node_data.h" +#include "components/bookmarks/test/test_bookmark_client.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/clipboard/clipboard.h" +#include "ui/base/clipboard/scoped_clipboard_writer.h" + +using base::ASCIIToUTF16; +using std::string; + +namespace bookmarks { +namespace { + +class BookmarkUtilsTest : public testing::Test, + public BaseBookmarkModelObserver { + public: + BookmarkUtilsTest() + : grouped_changes_beginning_count_(0), + grouped_changes_ended_count_(0) {} + ~BookmarkUtilsTest() override {} + +// Copy and paste is not yet supported on iOS. http://crbug.com/228147 +#if !defined(OS_IOS) + void TearDown() override { + ui::Clipboard::DestroyClipboardForCurrentThread(); + } +#endif // !defined(OS_IOS) + + // Certain user actions require multiple changes to the bookmark model, + // however these modifications need to be atomic for the undo framework. The + // BaseBookmarkModelObserver is used to inform the boundaries of the user + // action. For example, when multiple bookmarks are cut to the clipboard we + // expect one call each to GroupedBookmarkChangesBeginning/Ended. + void ExpectGroupedChangeCount(int expected_beginning_count, + int expected_ended_count) { + // The undo framework is not used under Android. Thus the group change + // events will not be fired and so should not be tested for Android. +#if !defined(OS_ANDROID) + EXPECT_EQ(grouped_changes_beginning_count_, expected_beginning_count); + EXPECT_EQ(grouped_changes_ended_count_, expected_ended_count); +#endif + } + + private: + // BaseBookmarkModelObserver: + void BookmarkModelChanged() override {} + + void GroupedBookmarkChangesBeginning(BookmarkModel* model) override { + ++grouped_changes_beginning_count_; + } + + void GroupedBookmarkChangesEnded(BookmarkModel* model) override { + ++grouped_changes_ended_count_; + } + + int grouped_changes_beginning_count_; + int grouped_changes_ended_count_; + + // Clipboard requires a message loop. + base::MessageLoopForUI loop_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkUtilsTest); +}; + +TEST_F(BookmarkUtilsTest, GetBookmarksMatchingPropertiesWordPhraseQuery) { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + const BookmarkNode* node1 = model->AddURL(model->other_node(), + 0, + ASCIIToUTF16("foo bar"), + GURL("http://www.google.com")); + const BookmarkNode* node2 = model->AddURL(model->other_node(), + 0, + ASCIIToUTF16("baz buz"), + GURL("http://www.cnn.com")); + const BookmarkNode* folder1 = + model->AddFolder(model->other_node(), 0, ASCIIToUTF16("foo")); + std::vector<const BookmarkNode*> nodes; + QueryFields query; + query.word_phrase_query.reset(new base::string16); + // No nodes are returned for empty string. + *query.word_phrase_query = ASCIIToUTF16(""); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + EXPECT_TRUE(nodes.empty()); + nodes.clear(); + + // No nodes are returned for space-only string. + *query.word_phrase_query = ASCIIToUTF16(" "); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + EXPECT_TRUE(nodes.empty()); + nodes.clear(); + + // Node "foo bar" and folder "foo" are returned in search results. + *query.word_phrase_query = ASCIIToUTF16("foo"); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + ASSERT_EQ(2U, nodes.size()); + EXPECT_TRUE(nodes[0] == folder1); + EXPECT_TRUE(nodes[1] == node1); + nodes.clear(); + + // Ensure url matches return in search results. + *query.word_phrase_query = ASCIIToUTF16("cnn"); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + ASSERT_EQ(1U, nodes.size()); + EXPECT_TRUE(nodes[0] == node2); + nodes.clear(); + + // Ensure folder "foo" is not returned in more specific search. + *query.word_phrase_query = ASCIIToUTF16("foo bar"); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + ASSERT_EQ(1U, nodes.size()); + EXPECT_TRUE(nodes[0] == node1); + nodes.clear(); + + // Bookmark Bar and Other Bookmarks are not returned in search results. + *query.word_phrase_query = ASCIIToUTF16("Bookmark"); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + ASSERT_EQ(0U, nodes.size()); + nodes.clear(); +} + +// Check exact matching against a URL query. +TEST_F(BookmarkUtilsTest, GetBookmarksMatchingPropertiesUrl) { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + const BookmarkNode* node1 = model->AddURL(model->other_node(), + 0, + ASCIIToUTF16("Google"), + GURL("https://www.google.com/")); + model->AddURL(model->other_node(), + 0, + ASCIIToUTF16("Google Calendar"), + GURL("https://www.google.com/calendar")); + + model->AddFolder(model->other_node(), 0, ASCIIToUTF16("Folder")); + + std::vector<const BookmarkNode*> nodes; + QueryFields query; + query.url.reset(new base::string16); + *query.url = ASCIIToUTF16("https://www.google.com/"); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + ASSERT_EQ(1U, nodes.size()); + EXPECT_TRUE(nodes[0] == node1); + nodes.clear(); + + *query.url = ASCIIToUTF16("calendar"); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + ASSERT_EQ(0U, nodes.size()); + nodes.clear(); + + // Empty URL should not match folders. + *query.url = ASCIIToUTF16(""); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + ASSERT_EQ(0U, nodes.size()); + nodes.clear(); +} + +// Check exact matching against a title query. +TEST_F(BookmarkUtilsTest, GetBookmarksMatchingPropertiesTitle) { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + const BookmarkNode* node1 = model->AddURL(model->other_node(), + 0, + ASCIIToUTF16("Google"), + GURL("https://www.google.com/")); + model->AddURL(model->other_node(), + 0, + ASCIIToUTF16("Google Calendar"), + GURL("https://www.google.com/calendar")); + + const BookmarkNode* folder1 = + model->AddFolder(model->other_node(), 0, ASCIIToUTF16("Folder")); + + std::vector<const BookmarkNode*> nodes; + QueryFields query; + query.title.reset(new base::string16); + *query.title = ASCIIToUTF16("Google"); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + ASSERT_EQ(1U, nodes.size()); + EXPECT_TRUE(nodes[0] == node1); + nodes.clear(); + + *query.title = ASCIIToUTF16("Calendar"); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + ASSERT_EQ(0U, nodes.size()); + nodes.clear(); + + // Title should match folders. + *query.title = ASCIIToUTF16("Folder"); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + ASSERT_EQ(1U, nodes.size()); + EXPECT_TRUE(nodes[0] == folder1); + nodes.clear(); +} + +// Check matching against a query with multiple predicates. +TEST_F(BookmarkUtilsTest, GetBookmarksMatchingPropertiesConjunction) { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + const BookmarkNode* node1 = model->AddURL(model->other_node(), + 0, + ASCIIToUTF16("Google"), + GURL("https://www.google.com/")); + model->AddURL(model->other_node(), + 0, + ASCIIToUTF16("Google Calendar"), + GURL("https://www.google.com/calendar")); + + model->AddFolder(model->other_node(), 0, ASCIIToUTF16("Folder")); + + std::vector<const BookmarkNode*> nodes; + QueryFields query; + + // Test all fields matching. + query.word_phrase_query.reset(new base::string16(ASCIIToUTF16("www"))); + query.url.reset(new base::string16(ASCIIToUTF16("https://www.google.com/"))); + query.title.reset(new base::string16(ASCIIToUTF16("Google"))); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + ASSERT_EQ(1U, nodes.size()); + EXPECT_TRUE(nodes[0] == node1); + nodes.clear(); + + scoped_ptr<base::string16>* fields[] = { + &query.word_phrase_query, &query.url, &query.title }; + + // Test two fields matching. + for (size_t i = 0; i < arraysize(fields); i++) { + scoped_ptr<base::string16> original_value(fields[i]->release()); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + ASSERT_EQ(1U, nodes.size()); + EXPECT_TRUE(nodes[0] == node1); + nodes.clear(); + fields[i]->reset(original_value.release()); + } + + // Test two fields matching with one non-matching field. + for (size_t i = 0; i < arraysize(fields); i++) { + scoped_ptr<base::string16> original_value(fields[i]->release()); + fields[i]->reset(new base::string16(ASCIIToUTF16("fjdkslafjkldsa"))); + GetBookmarksMatchingProperties(model.get(), query, 100, &nodes); + ASSERT_EQ(0U, nodes.size()); + nodes.clear(); + fields[i]->reset(original_value.release()); + } +} + +// Copy and paste is not yet supported on iOS. http://crbug.com/228147 +#if !defined(OS_IOS) +TEST_F(BookmarkUtilsTest, PasteBookmarkFromURL) { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + const base::string16 url_text = ASCIIToUTF16("http://www.google.com/"); + const BookmarkNode* new_folder = model->AddFolder( + model->bookmark_bar_node(), 0, ASCIIToUTF16("New_Folder")); + + // Write blank text to clipboard. + { + ui::ScopedClipboardWriter clipboard_writer(ui::CLIPBOARD_TYPE_COPY_PASTE); + clipboard_writer.WriteText(base::string16()); + } + // Now we shouldn't be able to paste from the clipboard. + EXPECT_FALSE(CanPasteFromClipboard(model.get(), new_folder)); + + // Write some valid url to the clipboard. + { + ui::ScopedClipboardWriter clipboard_writer(ui::CLIPBOARD_TYPE_COPY_PASTE); + clipboard_writer.WriteText(url_text); + } + // Now we should be able to paste from the clipboard. + EXPECT_TRUE(CanPasteFromClipboard(model.get(), new_folder)); + + PasteFromClipboard(model.get(), new_folder, 0); + ASSERT_EQ(1, new_folder->child_count()); + + // Url for added node should be same as url_text. + EXPECT_EQ(url_text, ASCIIToUTF16(new_folder->GetChild(0)->url().spec())); +} + +TEST_F(BookmarkUtilsTest, CopyPaste) { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + const BookmarkNode* node = model->AddURL(model->other_node(), + 0, + ASCIIToUTF16("foo bar"), + GURL("http://www.google.com")); + + // Copy a node to the clipboard. + std::vector<const BookmarkNode*> nodes; + nodes.push_back(node); + CopyToClipboard(model.get(), nodes, false); + + // And make sure we can paste a bookmark from the clipboard. + EXPECT_TRUE(CanPasteFromClipboard(model.get(), model->bookmark_bar_node())); + + // Write some text to the clipboard. + { + ui::ScopedClipboardWriter clipboard_writer( + ui::CLIPBOARD_TYPE_COPY_PASTE); + clipboard_writer.WriteText(ASCIIToUTF16("foo")); + } + + // Now we shouldn't be able to paste from the clipboard. + EXPECT_FALSE(CanPasteFromClipboard(model.get(), model->bookmark_bar_node())); +} + +// Test for updating title such that url and title pair are unique among the +// children of parent. +TEST_F(BookmarkUtilsTest, MakeTitleUnique) { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + const base::string16 url_text = ASCIIToUTF16("http://www.google.com/"); + const base::string16 title_text = ASCIIToUTF16("foobar"); + const BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); + + const BookmarkNode* node = + model->AddURL(bookmark_bar_node, 0, title_text, GURL(url_text)); + + EXPECT_EQ(url_text, + ASCIIToUTF16(bookmark_bar_node->GetChild(0)->url().spec())); + EXPECT_EQ(title_text, bookmark_bar_node->GetChild(0)->GetTitle()); + + // Copy a node to the clipboard. + std::vector<const BookmarkNode*> nodes; + nodes.push_back(node); + CopyToClipboard(model.get(), nodes, false); + + // Now we should be able to paste from the clipboard. + EXPECT_TRUE(CanPasteFromClipboard(model.get(), bookmark_bar_node)); + + PasteFromClipboard(model.get(), bookmark_bar_node, 1); + ASSERT_EQ(2, bookmark_bar_node->child_count()); + + // Url for added node should be same as url_text. + EXPECT_EQ(url_text, + ASCIIToUTF16(bookmark_bar_node->GetChild(1)->url().spec())); + // Title for added node should be numeric subscript suffix with copied node + // title. + EXPECT_EQ(ASCIIToUTF16("foobar (1)"), + bookmark_bar_node->GetChild(1)->GetTitle()); +} + +TEST_F(BookmarkUtilsTest, CopyPasteMetaInfo) { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + const BookmarkNode* node = model->AddURL(model->other_node(), + 0, + ASCIIToUTF16("foo bar"), + GURL("http://www.google.com")); + model->SetNodeMetaInfo(node, "somekey", "somevalue"); + model->SetNodeMetaInfo(node, "someotherkey", "someothervalue"); + + // Copy a node to the clipboard. + std::vector<const BookmarkNode*> nodes; + nodes.push_back(node); + CopyToClipboard(model.get(), nodes, false); + + // Paste node to a different folder. + const BookmarkNode* folder = + model->AddFolder(model->bookmark_bar_node(), 0, ASCIIToUTF16("Folder")); + EXPECT_EQ(0, folder->child_count()); + + // And make sure we can paste a bookmark from the clipboard. + EXPECT_TRUE(CanPasteFromClipboard(model.get(), folder)); + + PasteFromClipboard(model.get(), folder, 0); + ASSERT_EQ(1, folder->child_count()); + + // Verify that the pasted node contains the same meta info. + const BookmarkNode* pasted = folder->GetChild(0); + ASSERT_TRUE(pasted->GetMetaInfoMap()); + EXPECT_EQ(2u, pasted->GetMetaInfoMap()->size()); + std::string value; + EXPECT_TRUE(pasted->GetMetaInfo("somekey", &value)); + EXPECT_EQ("somevalue", value); + EXPECT_TRUE(pasted->GetMetaInfo("someotherkey", &value)); + EXPECT_EQ("someothervalue", value); +} + +#if defined(OS_LINUX) || defined(OS_MACOSX) +// http://crbug.com/396472 +#define MAYBE_CutToClipboard DISABLED_CutToClipboard +#else +#define MAYBE_CutToClipboard CutToClipboard +#endif +TEST_F(BookmarkUtilsTest, MAYBE_CutToClipboard) { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + model->AddObserver(this); + + base::string16 title(ASCIIToUTF16("foo")); + GURL url("http://foo.com"); + const BookmarkNode* n1 = model->AddURL(model->other_node(), 0, title, url); + const BookmarkNode* n2 = model->AddURL(model->other_node(), 1, title, url); + + // Cut the nodes to the clipboard. + std::vector<const BookmarkNode*> nodes; + nodes.push_back(n1); + nodes.push_back(n2); + CopyToClipboard(model.get(), nodes, true); + + // Make sure the nodes were removed. + EXPECT_EQ(0, model->other_node()->child_count()); + + // Make sure observers were notified the set of changes should be grouped. + ExpectGroupedChangeCount(1, 1); + + // And make sure we can paste from the clipboard. + EXPECT_TRUE(CanPasteFromClipboard(model.get(), model->other_node())); +} + +TEST_F(BookmarkUtilsTest, PasteNonEditableNodes) { + // Load a model with an extra node that is not editable. + scoped_ptr<TestBookmarkClient> client(new TestBookmarkClient()); + BookmarkPermanentNode* extra_node = new BookmarkPermanentNode(100); + BookmarkPermanentNodeList extra_nodes; + extra_nodes.push_back(extra_node); + client->SetExtraNodesToLoad(std::move(extra_nodes)); + + scoped_ptr<BookmarkModel> model( + TestBookmarkClient::CreateModelWithClient(std::move(client))); + const BookmarkNode* node = model->AddURL(model->other_node(), + 0, + ASCIIToUTF16("foo bar"), + GURL("http://www.google.com")); + + // Copy a node to the clipboard. + std::vector<const BookmarkNode*> nodes; + nodes.push_back(node); + CopyToClipboard(model.get(), nodes, false); + + // And make sure we can paste a bookmark from the clipboard. + EXPECT_TRUE(CanPasteFromClipboard(model.get(), model->bookmark_bar_node())); + + // But it can't be pasted into a non-editable folder. + BookmarkClient* upcast = model->client(); + EXPECT_FALSE(upcast->CanBeEditedByUser(extra_node)); + EXPECT_FALSE(CanPasteFromClipboard(model.get(), extra_node)); +} +#endif // !defined(OS_IOS) + +TEST_F(BookmarkUtilsTest, GetParentForNewNodes) { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + // This tests the case where selection contains one item and that item is a + // folder. + std::vector<const BookmarkNode*> nodes; + nodes.push_back(model->bookmark_bar_node()); + int index = -1; + const BookmarkNode* real_parent = + GetParentForNewNodes(model->bookmark_bar_node(), nodes, &index); + EXPECT_EQ(real_parent, model->bookmark_bar_node()); + EXPECT_EQ(0, index); + + nodes.clear(); + + // This tests the case where selection contains one item and that item is an + // url. + const BookmarkNode* page1 = model->AddURL(model->bookmark_bar_node(), + 0, + ASCIIToUTF16("Google"), + GURL("http://google.com")); + nodes.push_back(page1); + real_parent = GetParentForNewNodes(model->bookmark_bar_node(), nodes, &index); + EXPECT_EQ(real_parent, model->bookmark_bar_node()); + EXPECT_EQ(1, index); + + // This tests the case where selection has more than one item. + const BookmarkNode* folder1 = + model->AddFolder(model->bookmark_bar_node(), 1, ASCIIToUTF16("Folder 1")); + nodes.push_back(folder1); + real_parent = GetParentForNewNodes(model->bookmark_bar_node(), nodes, &index); + EXPECT_EQ(real_parent, model->bookmark_bar_node()); + EXPECT_EQ(2, index); + + // This tests the case where selection doesn't contain any items. + nodes.clear(); + real_parent = GetParentForNewNodes(model->bookmark_bar_node(), nodes, &index); + EXPECT_EQ(real_parent, model->bookmark_bar_node()); + EXPECT_EQ(2, index); +} + +// Verifies that meta info is copied when nodes are cloned. +TEST_F(BookmarkUtilsTest, CloneMetaInfo) { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + // Add a node containing meta info. + const BookmarkNode* node = model->AddURL(model->other_node(), + 0, + ASCIIToUTF16("foo bar"), + GURL("http://www.google.com")); + model->SetNodeMetaInfo(node, "somekey", "somevalue"); + model->SetNodeMetaInfo(node, "someotherkey", "someothervalue"); + + // Clone node to a different folder. + const BookmarkNode* folder = + model->AddFolder(model->bookmark_bar_node(), 0, ASCIIToUTF16("Folder")); + std::vector<BookmarkNodeData::Element> elements; + BookmarkNodeData::Element node_data(node); + elements.push_back(node_data); + EXPECT_EQ(0, folder->child_count()); + CloneBookmarkNode(model.get(), elements, folder, 0, false); + ASSERT_EQ(1, folder->child_count()); + + // Verify that the cloned node contains the same meta info. + const BookmarkNode* clone = folder->GetChild(0); + ASSERT_TRUE(clone->GetMetaInfoMap()); + EXPECT_EQ(2u, clone->GetMetaInfoMap()->size()); + std::string value; + EXPECT_TRUE(clone->GetMetaInfo("somekey", &value)); + EXPECT_EQ("somevalue", value); + EXPECT_TRUE(clone->GetMetaInfo("someotherkey", &value)); + EXPECT_EQ("someothervalue", value); +} + +// Verifies that meta info fields in the non cloned set are not copied when +// cloning a bookmark. +TEST_F(BookmarkUtilsTest, CloneBookmarkResetsNonClonedKey) { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + model->AddNonClonedKey("foo"); + const BookmarkNode* parent = model->other_node(); + const BookmarkNode* node = model->AddURL( + parent, 0, ASCIIToUTF16("title"), GURL("http://www.google.com")); + model->SetNodeMetaInfo(node, "foo", "ignored value"); + model->SetNodeMetaInfo(node, "bar", "kept value"); + std::vector<BookmarkNodeData::Element> elements; + BookmarkNodeData::Element node_data(node); + elements.push_back(node_data); + + // Cloning a bookmark should clear the non cloned key. + CloneBookmarkNode(model.get(), elements, parent, 0, true); + ASSERT_EQ(2, parent->child_count()); + std::string value; + EXPECT_FALSE(parent->GetChild(0)->GetMetaInfo("foo", &value)); + + // Other keys should still be cloned. + EXPECT_TRUE(parent->GetChild(0)->GetMetaInfo("bar", &value)); + EXPECT_EQ("kept value", value); +} + +// Verifies that meta info fields in the non cloned set are not copied when +// cloning a folder. +TEST_F(BookmarkUtilsTest, CloneFolderResetsNonClonedKey) { + scoped_ptr<BookmarkModel> model(TestBookmarkClient::CreateModel()); + model->AddNonClonedKey("foo"); + const BookmarkNode* parent = model->other_node(); + const BookmarkNode* node = model->AddFolder(parent, 0, ASCIIToUTF16("title")); + model->SetNodeMetaInfo(node, "foo", "ignored value"); + model->SetNodeMetaInfo(node, "bar", "kept value"); + std::vector<BookmarkNodeData::Element> elements; + BookmarkNodeData::Element node_data(node); + elements.push_back(node_data); + + // Cloning a folder should clear the non cloned key. + CloneBookmarkNode(model.get(), elements, parent, 0, true); + ASSERT_EQ(2, parent->child_count()); + std::string value; + EXPECT_FALSE(parent->GetChild(0)->GetMetaInfo("foo", &value)); + + // Other keys should still be cloned. + EXPECT_TRUE(parent->GetChild(0)->GetMetaInfo("bar", &value)); + EXPECT_EQ("kept value", value); +} + +TEST_F(BookmarkUtilsTest, RemoveAllBookmarks) { + // Load a model with an extra node that is not editable. + scoped_ptr<TestBookmarkClient> client(new TestBookmarkClient()); + BookmarkPermanentNode* extra_node = new BookmarkPermanentNode(100); + BookmarkPermanentNodeList extra_nodes; + extra_nodes.push_back(extra_node); + client->SetExtraNodesToLoad(std::move(extra_nodes)); + + scoped_ptr<BookmarkModel> model( + TestBookmarkClient::CreateModelWithClient(std::move(client))); + EXPECT_TRUE(model->bookmark_bar_node()->empty()); + EXPECT_TRUE(model->other_node()->empty()); + EXPECT_TRUE(model->mobile_node()->empty()); + EXPECT_TRUE(extra_node->empty()); + + const base::string16 title = base::ASCIIToUTF16("Title"); + const GURL url("http://google.com"); + model->AddURL(model->bookmark_bar_node(), 0, title, url); + model->AddURL(model->other_node(), 0, title, url); + model->AddURL(model->mobile_node(), 0, title, url); + model->AddURL(extra_node, 0, title, url); + + std::vector<const BookmarkNode*> nodes; + model->GetNodesByURL(url, &nodes); + ASSERT_EQ(4u, nodes.size()); + + RemoveAllBookmarks(model.get(), url); + + nodes.clear(); + model->GetNodesByURL(url, &nodes); + ASSERT_EQ(1u, nodes.size()); + EXPECT_TRUE(model->bookmark_bar_node()->empty()); + EXPECT_TRUE(model->other_node()->empty()); + EXPECT_TRUE(model->mobile_node()->empty()); + EXPECT_EQ(1, extra_node->child_count()); +} + +} // namespace +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/scoped_group_bookmark_actions.cc b/chromium/components/bookmarks/browser/scoped_group_bookmark_actions.cc new file mode 100644 index 00000000000..f962e2961d6 --- /dev/null +++ b/chromium/components/bookmarks/browser/scoped_group_bookmark_actions.cc @@ -0,0 +1,22 @@ +// 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/bookmarks/browser/scoped_group_bookmark_actions.h" + +#include "components/bookmarks/browser/bookmark_model.h" + +namespace bookmarks { + +ScopedGroupBookmarkActions::ScopedGroupBookmarkActions(BookmarkModel* model) + : model_(model) { + if (model_) + model_->BeginGroupedChanges(); +} + +ScopedGroupBookmarkActions::~ScopedGroupBookmarkActions() { + if (model_) + model_->EndGroupedChanges(); +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/scoped_group_bookmark_actions.h b/chromium/components/bookmarks/browser/scoped_group_bookmark_actions.h new file mode 100644 index 00000000000..c2f09978439 --- /dev/null +++ b/chromium/components/bookmarks/browser/scoped_group_bookmark_actions.h @@ -0,0 +1,28 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_BROWSER_SCOPED_GROUP_BOOKMARK_ACTIONS_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_SCOPED_GROUP_BOOKMARK_ACTIONS_H_ + +#include "base/macros.h" + +namespace bookmarks { + +class BookmarkModel; + +// Scopes the grouping of a set of changes into one undoable action. +class ScopedGroupBookmarkActions { + public: + explicit ScopedGroupBookmarkActions(BookmarkModel* model); + ~ScopedGroupBookmarkActions(); + + private: + BookmarkModel* model_; + + DISALLOW_COPY_AND_ASSIGN(ScopedGroupBookmarkActions); +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_SCOPED_GROUP_BOOKMARK_ACTIONS_H_ diff --git a/chromium/components/bookmarks/browser/startup_task_runner_service.cc b/chromium/components/bookmarks/browser/startup_task_runner_service.cc new file mode 100644 index 00000000000..5b9b74139a6 --- /dev/null +++ b/chromium/components/bookmarks/browser/startup_task_runner_service.cc @@ -0,0 +1,36 @@ +// Copyright (c) 2013 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/bookmarks/browser/startup_task_runner_service.h" + +#include "base/deferred_sequenced_task_runner.h" +#include "base/logging.h" +#include "base/sequenced_task_runner.h" + +namespace bookmarks { + +StartupTaskRunnerService::StartupTaskRunnerService( + const scoped_refptr<base::SequencedTaskRunner>& io_task_runner) + : io_task_runner_(io_task_runner) { + DCHECK(io_task_runner_); +} + +StartupTaskRunnerService::~StartupTaskRunnerService() { +} + +scoped_refptr<base::DeferredSequencedTaskRunner> + StartupTaskRunnerService::GetBookmarkTaskRunner() { + DCHECK(CalledOnValidThread()); + if (!bookmark_task_runner_) { + bookmark_task_runner_ = + new base::DeferredSequencedTaskRunner(io_task_runner_); + } + return bookmark_task_runner_; +} + +void StartupTaskRunnerService::StartDeferredTaskRunners() { + GetBookmarkTaskRunner()->Start(); +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/browser/startup_task_runner_service.h b/chromium/components/bookmarks/browser/startup_task_runner_service.h new file mode 100644 index 00000000000..b39819af135 --- /dev/null +++ b/chromium/components/bookmarks/browser/startup_task_runner_service.h @@ -0,0 +1,49 @@ +// Copyright (c) 2013 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 COMPONENTS_BOOKMARKS_BROWSER_STARTUP_TASK_RUNNER_SERVICE_H_ +#define COMPONENTS_BOOKMARKS_BROWSER_STARTUP_TASK_RUNNER_SERVICE_H_ + +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/threading/non_thread_safe.h" +#include "components/keyed_service/core/keyed_service.h" + +namespace base { +class DeferredSequencedTaskRunner; +class SequencedTaskRunner; +} // namespace base + +namespace bookmarks { + +// This service manages the startup task runners. +class StartupTaskRunnerService : public base::NonThreadSafe, + public KeyedService { + public: + explicit StartupTaskRunnerService( + const scoped_refptr<base::SequencedTaskRunner>& io_task_runner); + ~StartupTaskRunnerService() override; + + // Returns sequenced task runner where all bookmarks I/O operations are + // performed. + // This method should only be called from the UI thread. + // Note: Using a separate task runner per profile service gives a better + // management of the sequence in which the task are started in order to avoid + // congestion during start-up (e.g the caller may decide to start loading the + // bookmarks only after the history finished). + scoped_refptr<base::DeferredSequencedTaskRunner> GetBookmarkTaskRunner(); + + // Starts the task runners that are deferred during start-up. + void StartDeferredTaskRunners(); + + private: + scoped_refptr<base::SequencedTaskRunner> io_task_runner_; + scoped_refptr<base::DeferredSequencedTaskRunner> bookmark_task_runner_; + + DISALLOW_COPY_AND_ASSIGN(StartupTaskRunnerService); +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_BROWSER_STARTUP_TASK_RUNNER_SERVICE_H_ diff --git a/chromium/components/bookmarks/common/BUILD.gn b/chromium/components/bookmarks/common/BUILD.gn new file mode 100644 index 00000000000..2ba1bd9c411 --- /dev/null +++ b/chromium/components/bookmarks/common/BUILD.gn @@ -0,0 +1,19 @@ +# 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. + +source_set("common") { + sources = [ + "bookmark_constants.cc", + "bookmark_constants.h", + "bookmark_pref_names.cc", + "bookmark_pref_names.h", + ] + + deps = [ + "//base", + ] + if (is_android) { + deps += [ "//components/bookmarks/common/android" ] + } +} diff --git a/chromium/components/bookmarks/common/android/BUILD.gn b/chromium/components/bookmarks/common/android/BUILD.gn new file mode 100644 index 00000000000..69bb7f9ea68 --- /dev/null +++ b/chromium/components/bookmarks/common/android/BUILD.gn @@ -0,0 +1,44 @@ +# 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. + +import("//build/config/android/rules.gni") + +# GYP: //components/bookmarks.gyp:bookmarks_browser (android part) +source_set("android") { + sources = [ + "bookmark_id.cc", + "bookmark_id.h", + "bookmark_type.h", + "component_jni_registrar.cc", + "component_jni_registrar.h", + ] + deps = [ + ":bookmarks_jni_headers", + "//base:base", + ] +} + +# GYP: //components/bookmarks.gyp:bookmarks_java +android_library("bookmarks_java") { + deps = [ + "//base:base_java", + ] + srcjar_deps = [ ":bookmark_type_javagen" ] + java_files = [ "java/src/org/chromium/components/bookmarks/BookmarkId.java" ] +} + +# GYP: //components/bookmarks.gyp:bookmarks_jni_headers +generate_jni("bookmarks_jni_headers") { + jni_package = "components/bookmarks" + sources = [ + "java/src/org/chromium/components/bookmarks/BookmarkId.java", + ] +} + +# GYP: //components/bookmarks.gyp:bookmarks_type_java +java_cpp_enum("bookmark_type_javagen") { + sources = [ + "bookmark_type.h", + ] +} diff --git a/chromium/components/bookmarks/common/android/OWNERS b/chromium/components/bookmarks/common/android/OWNERS new file mode 100644 index 00000000000..48b0da2eaae --- /dev/null +++ b/chromium/components/bookmarks/common/android/OWNERS @@ -0,0 +1,2 @@ +tedchoc@chromium.org +kkimlabs@chromium.org diff --git a/chromium/components/bookmarks/common/android/bookmark_id.cc b/chromium/components/bookmarks/common/android/bookmark_id.cc new file mode 100644 index 00000000000..5cac9eeaff4 --- /dev/null +++ b/chromium/components/bookmarks/common/android/bookmark_id.cc @@ -0,0 +1,30 @@ +// 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/bookmarks/common/android/bookmark_id.h" + +#include "jni/BookmarkId_jni.h" + +namespace bookmarks { +namespace android { + +long JavaBookmarkIdGetId(JNIEnv* env, jobject obj) { + return Java_BookmarkId_getId(env, obj); +} + +int JavaBookmarkIdGetType(JNIEnv* env, jobject obj) { + return Java_BookmarkId_getType(env, obj); +} + +base::android::ScopedJavaLocalRef<jobject> JavaBookmarkIdCreateBookmarkId( + JNIEnv* env, jlong id, jint type) { + return Java_BookmarkId_createBookmarkId(env, id, type); +} + +bool RegisterBookmarkId(JNIEnv* env) { + return RegisterNativesImpl(env); +} + +} // namespace android +} // namespace bookmarks diff --git a/chromium/components/bookmarks/common/android/bookmark_id.h b/chromium/components/bookmarks/common/android/bookmark_id.h new file mode 100644 index 00000000000..af7ae721d5b --- /dev/null +++ b/chromium/components/bookmarks/common/android/bookmark_id.h @@ -0,0 +1,30 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_COMMON_ANDROID_BOOKMARK_ID_H_ +#define COMPONENTS_BOOKMARKS_COMMON_ANDROID_BOOKMARK_ID_H_ + +#include <jni.h> + +#include "base/android/scoped_java_ref.h" + +namespace bookmarks { +namespace android { + +// See BookmarkId#getId +long JavaBookmarkIdGetId(JNIEnv* env, jobject obj); + +// See BookmarkId#getType +int JavaBookmarkIdGetType(JNIEnv* env, jobject obj); + +// See BookmarkId#createBookmarkId +base::android::ScopedJavaLocalRef<jobject> JavaBookmarkIdCreateBookmarkId( + JNIEnv* env, jlong id, jint type); + +bool RegisterBookmarkId(JNIEnv* env); + +} // namespace android +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_COMMON_ANDROID_BOOKMARK_ID_H_ diff --git a/chromium/components/bookmarks/common/android/bookmark_type.h b/chromium/components/bookmarks/common/android/bookmark_type.h new file mode 100644 index 00000000000..75d21386333 --- /dev/null +++ b/chromium/components/bookmarks/common/android/bookmark_type.h @@ -0,0 +1,19 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_COMMON_ANDROID_BOOKMARK_TYPE_H_ +#define COMPONENTS_BOOKMARKS_COMMON_ANDROID_BOOKMARK_TYPE_H_ + +namespace bookmarks { + +// A Java counterpart will be generated for this enum. +// GENERATED_JAVA_ENUM_PACKAGE: org.chromium.components.bookmarks +enum BookmarkType { + BOOKMARK_TYPE_NORMAL, + BOOKMARK_TYPE_PARTNER, +}; + +} + +#endif // COMPONENTS_BOOKMARKS_COMMON_ANDROID_BOOKMARK_TYPE_H_ diff --git a/chromium/components/bookmarks/common/bookmark_constants.cc b/chromium/components/bookmarks/common/bookmark_constants.cc new file mode 100644 index 00000000000..6c2e5a6d542 --- /dev/null +++ b/chromium/components/bookmarks/common/bookmark_constants.cc @@ -0,0 +1,13 @@ +// 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/bookmarks/common/bookmark_constants.h" + +#define FPL FILE_PATH_LITERAL + +namespace bookmarks { + +const base::FilePath::CharType kBookmarksFileName[] = FPL("Bookmarks"); + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/common/bookmark_constants.h b/chromium/components/bookmarks/common/bookmark_constants.h new file mode 100644 index 00000000000..7b1a8f54476 --- /dev/null +++ b/chromium/components/bookmarks/common/bookmark_constants.h @@ -0,0 +1,16 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_COMMON_BOOKMARK_CONSTANTS_H_ +#define COMPONENTS_BOOKMARKS_COMMON_BOOKMARK_CONSTANTS_H_ + +#include "base/files/file_path.h" + +namespace bookmarks { + +extern const base::FilePath::CharType kBookmarksFileName[]; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_COMMON_BOOKMARK_CONSTANTS_H_ diff --git a/chromium/components/bookmarks/common/bookmark_pref_names.cc b/chromium/components/bookmarks/common/bookmark_pref_names.cc new file mode 100644 index 00000000000..f9746aff998 --- /dev/null +++ b/chromium/components/bookmarks/common/bookmark_pref_names.cc @@ -0,0 +1,43 @@ +// 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/bookmarks/common/bookmark_pref_names.h" + +namespace bookmarks { +namespace prefs { + +// Boolean which specifies the ids of the bookmark nodes that are expanded in +// the bookmark editor. +const char kBookmarkEditorExpandedNodes[] = "bookmark_editor.expanded_nodes"; + +// Modifying bookmarks is completely disabled when this is set to false. +const char kEditBookmarksEnabled[] = "bookmarks.editing_enabled"; + +// A list of bookmarks to include in a Managed Bookmarks root node. Each +// list item is a dictionary containing a "name" and an "url" entry, detailing +// the bookmark name and target URL respectively. +const char kManagedBookmarks[] = "bookmarks.managed_bookmarks"; + +// String which specifies the Managed Bookmarks folder name +const char kManagedBookmarksFolderName[] = + "bookmarks.managed_bookmarks_folder_name"; + +// Boolean which specifies whether the apps shortcut is visible on the bookmark +// bar. +const char kShowAppsShortcutInBookmarkBar[] = "bookmark_bar.show_apps_shortcut"; + +// Boolean which specifies whether the Managed Bookmarks folder is visible on +// the bookmark bar. +const char kShowManagedBookmarksInBookmarkBar[] = + "bookmark_bar.show_managed_bookmarks"; + +// Boolean which specifies whether the bookmark bar is visible on all tabs. +const char kShowBookmarkBar[] = "bookmark_bar.show_on_all_tabs"; + +// A list of bookmarks to include in a Supervised Bookmarks root node. Behaves +// like kManagedBookmarks. +const char kSupervisedBookmarks[] = "bookmarks.supervised_bookmarks"; + +} // namespace prefs +} // namespace bookmarks diff --git a/chromium/components/bookmarks/common/bookmark_pref_names.h b/chromium/components/bookmarks/common/bookmark_pref_names.h new file mode 100644 index 00000000000..9c5b609c77a --- /dev/null +++ b/chromium/components/bookmarks/common/bookmark_pref_names.h @@ -0,0 +1,25 @@ +// 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. + +// Constants for the names of various bookmarks preferences. + +#ifndef COMPONENTS_BOOKMARKS_COMMON_BOOKMARK_PREF_NAMES_H_ +#define COMPONENTS_BOOKMARKS_COMMON_BOOKMARK_PREF_NAMES_H_ + +namespace bookmarks { +namespace prefs { + +extern const char kBookmarkEditorExpandedNodes[]; +extern const char kEditBookmarksEnabled[]; +extern const char kManagedBookmarks[]; +extern const char kManagedBookmarksFolderName[]; +extern const char kShowAppsShortcutInBookmarkBar[]; +extern const char kShowManagedBookmarksInBookmarkBar[]; +extern const char kShowBookmarkBar[]; +extern const char kSupervisedBookmarks[]; + +} // namespace prefs +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_COMMON_BOOKMARK_PREF_NAMES_H_ diff --git a/chromium/components/bookmarks/managed/BUILD.gn b/chromium/components/bookmarks/managed/BUILD.gn new file mode 100644 index 00000000000..f2f27659a61 --- /dev/null +++ b/chromium/components/bookmarks/managed/BUILD.gn @@ -0,0 +1,48 @@ +# Copyright 2015 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. + +source_set("managed") { + sources = [ + "managed_bookmark_service.cc", + "managed_bookmark_service.h", + "managed_bookmark_util.cc", + "managed_bookmark_util.h", + "managed_bookmarks_tracker.cc", + "managed_bookmarks_tracker.h", + ] + + deps = [ + "//base", + "//components/bookmarks/browser", + "//components/bookmarks/common", + "//components/keyed_service/core", + "//components/prefs", + "//components/strings", + "//ui/base", + "//url", + ] +} + +source_set("unit_tests") { + testonly = true + sources = [ + "managed_bookmarks_tracker_unittest.cc", + ] + + configs += [ "//build/config/compiler:no_size_t_to_int_warning" ] + + deps = [ + ":managed", + "//base", + "//components/bookmarks/browser", + "//components/bookmarks/common", + "//components/bookmarks/test", + "//components/prefs:test_support", + "//components/strings", + "//testing/gmock", + "//testing/gtest", + "//ui/base", + "//url", + ] +} diff --git a/chromium/components/bookmarks/managed/managed_bookmark_service.cc b/chromium/components/bookmarks/managed/managed_bookmark_service.cc new file mode 100644 index 00000000000..066df72ea9a --- /dev/null +++ b/chromium/components/bookmarks/managed/managed_bookmark_service.cc @@ -0,0 +1,186 @@ +// Copyright 2015 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/bookmarks/managed/managed_bookmark_service.h" + +#include <stdint.h> +#include <stdlib.h> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/logging.h" +#include "base/macros.h" +#include "base/memory/scoped_vector.h" +#include "base/strings/string16.h" +#include "base/values.h" +#include "components/bookmarks/browser/bookmark_model.h" +#include "components/bookmarks/browser/bookmark_utils.h" +#include "components/bookmarks/managed/managed_bookmarks_tracker.h" +#include "grit/components_strings.h" +#include "ui/base/l10n/l10n_util.h" + +namespace bookmarks { +namespace { + +// BookmarkPermanentNodeLoader initializes a BookmarkPermanentNode from a JSON +// representation, title id and starting node id. +class BookmarkPermanentNodeLoader { + public: + BookmarkPermanentNodeLoader(scoped_ptr<BookmarkPermanentNode> node, + scoped_ptr<base::ListValue> initial_bookmarks, + int title_id) + : node_(std::move(node)), + initial_bookmarks_(std::move(initial_bookmarks)), + title_id_(title_id) { + DCHECK(node_); + } + + ~BookmarkPermanentNodeLoader() {} + + // Initializes |node_| from |initial_bookmarks_| and |title_id_| and returns + // it. The ids are assigned starting at |next_node_id| and the value is + // updated as a side-effect. + scoped_ptr<BookmarkPermanentNode> Load(int64_t* next_node_id) { + node_->set_id(*next_node_id); + *next_node_id = ManagedBookmarksTracker::LoadInitial( + node_.get(), initial_bookmarks_.get(), node_->id() + 1); + node_->set_visible(!node_->empty()); + node_->SetTitle(l10n_util::GetStringUTF16(title_id_)); + return std::move(node_); + } + + private: + scoped_ptr<BookmarkPermanentNode> node_; + scoped_ptr<base::ListValue> initial_bookmarks_; + int title_id_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkPermanentNodeLoader); +}; + +// Returns a list of initialized BookmarkPermanentNodes using |next_node_id| to +// start assigning id. |next_node_id| is updated as a side effect of calling +// this method. +BookmarkPermanentNodeList LoadExtraNodes( + ScopedVector<BookmarkPermanentNodeLoader> loaders, + int64_t* next_node_id) { + BookmarkPermanentNodeList extra_nodes; + for (const auto& loader : loaders) + extra_nodes.push_back(loader->Load(next_node_id).release()); + return extra_nodes; +} + +} // namespace + +ManagedBookmarkService::ManagedBookmarkService( + PrefService* prefs, + const GetManagementDomainCallback& callback) + : prefs_(prefs), + bookmark_model_(nullptr), + managed_domain_callback_(callback), + managed_node_(nullptr), + supervised_node_(nullptr) {} + +ManagedBookmarkService::~ManagedBookmarkService() { + DCHECK(!bookmark_model_); +} + +void ManagedBookmarkService::BookmarkModelCreated( + BookmarkModel* bookmark_model) { + DCHECK(bookmark_model); + DCHECK(!bookmark_model_); + bookmark_model_ = bookmark_model; + bookmark_model_->AddObserver(this); + + managed_bookmarks_tracker_.reset(new ManagedBookmarksTracker( + bookmark_model_, prefs_, false /* is_supervised */, + managed_domain_callback_)); + + GetManagementDomainCallback unbound_callback; + supervised_bookmarks_tracker_.reset(new ManagedBookmarksTracker( + bookmark_model_, prefs_, true /* is_supervised */, unbound_callback)); +} + +LoadExtraCallback ManagedBookmarkService::GetLoadExtraNodesCallback() { + // Create two BookmarkPermanentNode with a temporary id of 0. They will be + // populated and assigned proper ids in the LoadExtraNodes callback. Until + // then, they are owned by the returned closure. + scoped_ptr<BookmarkPermanentNode> managed(new BookmarkPermanentNode(0)); + scoped_ptr<BookmarkPermanentNode> supervised(new BookmarkPermanentNode(0)); + + managed_node_ = managed.get(); + supervised_node_ = supervised.get(); + + ScopedVector<BookmarkPermanentNodeLoader> loaders; + loaders.push_back(new BookmarkPermanentNodeLoader( + std::move(managed), + managed_bookmarks_tracker_->GetInitialManagedBookmarks(), + IDS_BOOKMARK_BAR_MANAGED_FOLDER_DEFAULT_NAME)); + loaders.push_back(new BookmarkPermanentNodeLoader( + std::move(supervised), + supervised_bookmarks_tracker_->GetInitialManagedBookmarks(), + IDS_BOOKMARK_BAR_SUPERVISED_FOLDER_DEFAULT_NAME)); + + return base::Bind(&LoadExtraNodes, base::Passed(&loaders)); +} + +bool ManagedBookmarkService::CanSetPermanentNodeTitle( + const BookmarkNode* node) { + // |managed_node_| can have its title updated if the user signs in or out, + // since the name of the managed domain can appear in it. Also both + // |managed_node_| and |supervised_node_| can have their title updated on + // locale changes (http://crbug.com/459448). + if (node == managed_node_ || node == supervised_node_) + return true; + return !IsDescendantOf(node, managed_node_) && + !IsDescendantOf(node, supervised_node_); +} + +bool ManagedBookmarkService::CanSyncNode(const BookmarkNode* node) { + return !IsDescendantOf(node, managed_node_) && + !IsDescendantOf(node, supervised_node_); +} + +bool ManagedBookmarkService::CanBeEditedByUser(const BookmarkNode* node) { + return !IsDescendantOf(node, managed_node_) && + !IsDescendantOf(node, supervised_node_); +} + +void ManagedBookmarkService::Shutdown() { + Cleanup(); +} + +void ManagedBookmarkService::BookmarkModelChanged() {} + +void ManagedBookmarkService::BookmarkModelLoaded(BookmarkModel* bookmark_model, + bool ids_reassigned) { + BaseBookmarkModelObserver::BookmarkModelLoaded(bookmark_model, + ids_reassigned); + // Start tracking the managed and supervised bookmarks. This will detect any + // changes that may have occurred while the initial managed and supervised + // bookmarks were being loaded on the background. + managed_bookmarks_tracker_->Init(managed_node_); + supervised_bookmarks_tracker_->Init(supervised_node_); +} + +void ManagedBookmarkService::BookmarkModelBeingDeleted( + BookmarkModel* bookmark_model) { + Cleanup(); +} + +void ManagedBookmarkService::Cleanup() { + if (bookmark_model_) { + bookmark_model_->RemoveObserver(this); + bookmark_model_ = nullptr; + } + + managed_bookmarks_tracker_.reset(); + supervised_bookmarks_tracker_.reset(); + + managed_node_ = nullptr; + supervised_node_ = nullptr; +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/managed/managed_bookmark_service.h b/chromium/components/bookmarks/managed/managed_bookmark_service.h new file mode 100644 index 00000000000..db24b2bf5e3 --- /dev/null +++ b/chromium/components/bookmarks/managed/managed_bookmark_service.h @@ -0,0 +1,102 @@ +// Copyright 2015 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 COMPONENTS_BOOKMARKS_MANAGED_MANAGED_BOOKMARK_SERVICE_H_ +#define COMPONENTS_BOOKMARKS_MANAGED_MANAGED_BOOKMARK_SERVICE_H_ + +#include <string> + +#include "base/callback_forward.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "components/bookmarks/browser/base_bookmark_model_observer.h" +#include "components/bookmarks/browser/bookmark_node.h" +#include "components/bookmarks/browser/bookmark_storage.h" +#include "components/keyed_service/core/keyed_service.h" + +class PrefService; + +namespace bookmarks { + +class BookmarkModel; +class ManagedBookmarksTracker; + +// ManagedBookmarkService manages the bookmark folder controlled by enterprise +// policy or custodian of supervised users. +class ManagedBookmarkService : public KeyedService, + public BaseBookmarkModelObserver { + public: + typedef base::Callback<std::string()> GetManagementDomainCallback; + + ManagedBookmarkService(PrefService* prefs, + const GetManagementDomainCallback& callback); + ~ManagedBookmarkService() override; + + // Called upon creation of the BookmarkModel. + void BookmarkModelCreated(BookmarkModel* bookmark_model); + + // Returns a task that will be used to load any additional root nodes. This + // task will be invoked in the Profile's IO task runner. + LoadExtraCallback GetLoadExtraNodesCallback(); + + // Returns true if the |node| can have its title updated. + bool CanSetPermanentNodeTitle(const BookmarkNode* node); + + // Returns true if |node| should sync. + bool CanSyncNode(const BookmarkNode* node); + + // Returns true if |node| can be edited by the user. + // TODO(joaodasilva): the model should check this more aggressively, and + // should give the client a means to temporarily disable those checks. + // http://crbug.com/49598 + bool CanBeEditedByUser(const BookmarkNode* node); + + // Top-level managed bookmarks folder, defined by an enterprise policy; may be + // null. + const BookmarkNode* managed_node() { return managed_node_; } + + // Top-level supervised bookmarks folder, defined by the custodian of a + // supervised user; may be null. + const BookmarkNode* supervised_node() { return supervised_node_; } + + private: + // KeyedService implementation. + void Shutdown() override; + + // BaseBookmarkModelObserver implementation. + void BookmarkModelChanged() override; + + // BookmarkModelObserver implementation. + void BookmarkModelLoaded(BookmarkModel* bookmark_model, + bool ids_reassigned) override; + void BookmarkModelBeingDeleted(BookmarkModel* bookmark_model) override; + + // Cleanup, called when service is shutdown or when BookmarkModel is being + // destroyed. + void Cleanup(); + + // Pointer to the PrefService. Must outlive ManagedBookmarkService. + PrefService* prefs_; + + // Pointer to the BookmarkModel; may be null. Only valid between the calls to + // BookmarkModelCreated() and to BookmarkModelBeingDestroyed(). + BookmarkModel* bookmark_model_; + + // Managed bookmarks are defined by an enterprise policy. The lifetime of the + // BookmarkPermanentNode is controlled by BookmarkModel. + scoped_ptr<ManagedBookmarksTracker> managed_bookmarks_tracker_; + GetManagementDomainCallback managed_domain_callback_; + BookmarkPermanentNode* managed_node_; + + // Supervised bookmarks are defined by the custodian of a supervised user. The + // lifetime of the BookmarkPermanentNode is controlled by BookmarkModel. + scoped_ptr<ManagedBookmarksTracker> supervised_bookmarks_tracker_; + BookmarkPermanentNode* supervised_node_; + + DISALLOW_COPY_AND_ASSIGN(ManagedBookmarkService); +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_MANAGED_MANAGED_BOOKMARK_SERVICE_H_ diff --git a/chromium/components/bookmarks/managed/managed_bookmark_util.cc b/chromium/components/bookmarks/managed/managed_bookmark_util.cc new file mode 100644 index 00000000000..d19d941e78d --- /dev/null +++ b/chromium/components/bookmarks/managed/managed_bookmark_util.cc @@ -0,0 +1,33 @@ +// Copyright 2015 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/bookmarks/managed/managed_bookmark_util.h" + +#include "components/bookmarks/browser/bookmark_node.h" +#include "components/bookmarks/managed/managed_bookmark_service.h" + +namespace bookmarks { + +bool IsPermanentNode(const BookmarkPermanentNode* node, + ManagedBookmarkService* managed_bookmark_service) { + BookmarkNode::Type type = node->type(); + if (type == BookmarkNode::BOOKMARK_BAR || + type == BookmarkNode::OTHER_NODE || + type == BookmarkNode::MOBILE) { + return true; + } + + return IsManagedNode(node, managed_bookmark_service); +} + +bool IsManagedNode(const BookmarkPermanentNode* node, + ManagedBookmarkService* managed_bookmark_service) { + if (!managed_bookmark_service) + return false; + + return node == managed_bookmark_service->managed_node() || + node == managed_bookmark_service->supervised_node(); +} + +} // namespace bookmarks diff --git a/chromium/components/bookmarks/managed/managed_bookmark_util.h b/chromium/components/bookmarks/managed/managed_bookmark_util.h new file mode 100644 index 00000000000..9b3ec1815fc --- /dev/null +++ b/chromium/components/bookmarks/managed/managed_bookmark_util.h @@ -0,0 +1,23 @@ +// Copyright 2015 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 COMPONENTS_BOOKMARKS_MANAGED_MANAGED_BOOKMARK_UTIL_H_ +#define COMPONENTS_BOOKMARKS_MANAGED_MANAGED_BOOKMARK_UTIL_H_ + +namespace bookmarks { + +class BookmarkPermanentNode; +class ManagedBookmarkService; + +// Returns whether |node| is a permanent node. +bool IsPermanentNode(const BookmarkPermanentNode* node, + ManagedBookmarkService* managed_bookmark_service); + +// Returns whether |node| is a managed node. +bool IsManagedNode(const BookmarkPermanentNode* node, + ManagedBookmarkService* managed_bookmark_service); + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_MANAGED_MANAGED_BOOKMARK_UTIL_H_ diff --git a/chromium/components/bookmarks/managed/managed_bookmarks_tracker.cc b/chromium/components/bookmarks/managed/managed_bookmarks_tracker.cc new file mode 100644 index 00000000000..1c1813392df --- /dev/null +++ b/chromium/components/bookmarks/managed/managed_bookmarks_tracker.cc @@ -0,0 +1,214 @@ +// 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/bookmarks/managed/managed_bookmarks_tracker.h" + +#include <string> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/callback.h" +#include "base/logging.h" +#include "base/strings/utf_string_conversions.h" +#include "base/values.h" +#include "components/bookmarks/browser/bookmark_model.h" +#include "components/bookmarks/browser/bookmark_node.h" +#include "components/bookmarks/common/bookmark_pref_names.h" +#include "components/prefs/pref_service.h" +#include "grit/components_strings.h" +#include "ui/base/l10n/l10n_util.h" +#include "url/gurl.h" + +namespace bookmarks { + +const char ManagedBookmarksTracker::kName[] = "name"; +const char ManagedBookmarksTracker::kUrl[] = "url"; +const char ManagedBookmarksTracker::kChildren[] = "children"; +const char ManagedBookmarksTracker::kFolderName[] = "toplevel_name"; + +ManagedBookmarksTracker::ManagedBookmarksTracker( + BookmarkModel* model, + PrefService* prefs, + bool is_supervised, + const GetManagementDomainCallback& callback) + : model_(model), + is_supervised_(is_supervised), + managed_node_(NULL), + prefs_(prefs), + get_management_domain_callback_(callback) { +} + +ManagedBookmarksTracker::~ManagedBookmarksTracker() {} + +scoped_ptr<base::ListValue> +ManagedBookmarksTracker::GetInitialManagedBookmarks() { + const base::ListValue* list = prefs_->GetList(GetPrefName()); + return make_scoped_ptr(list->DeepCopy()); +} + +// static +int64_t ManagedBookmarksTracker::LoadInitial(BookmarkNode* folder, + const base::ListValue* list, + int64_t next_node_id) { + for (size_t i = 0; i < list->GetSize(); ++i) { + // Extract the data for the next bookmark from the |list|. + base::string16 title; + GURL url; + const base::ListValue* children = NULL; + if (!LoadBookmark(list, i, &title, &url, &children)) + continue; + + BookmarkNode* child = new BookmarkNode(next_node_id++, url); + child->SetTitle(title); + folder->Add(child, folder->child_count()); + if (children) { + child->set_type(BookmarkNode::FOLDER); + child->set_date_folder_modified(base::Time::Now()); + next_node_id = LoadInitial(child, children, next_node_id); + } else { + child->set_type(BookmarkNode::URL); + child->set_date_added(base::Time::Now()); + } + } + + return next_node_id; +} + +void ManagedBookmarksTracker::Init(BookmarkPermanentNode* managed_node) { + managed_node_ = managed_node; + registrar_.Init(prefs_); + registrar_.Add(GetPrefName(), + base::Bind(&ManagedBookmarksTracker::ReloadManagedBookmarks, + base::Unretained(this))); + // Reload now just in case something changed since the initial load started. + // Note that if we track managed bookmarks rather than supervised bookmarks, + // then we must not load them until cloud policy system has been fully + // initialized (which will make our preference a managed preference). + const bool are_managed_bookmarks_available = + is_supervised_ || prefs_->IsManagedPreference(GetPrefName()); + if (are_managed_bookmarks_available) + ReloadManagedBookmarks(); +} + +// static +const char* ManagedBookmarksTracker::GetPrefName(bool is_supervised) { + return is_supervised ? prefs::kSupervisedBookmarks + : prefs::kManagedBookmarks; +} + +const char* ManagedBookmarksTracker::GetPrefName() const { + return GetPrefName(is_supervised_); +} + +base::string16 ManagedBookmarksTracker::GetBookmarksFolderTitle() const { + if (is_supervised_) { + return l10n_util::GetStringUTF16( + IDS_BOOKMARK_BAR_SUPERVISED_FOLDER_DEFAULT_NAME); + } else { + std::string name = prefs_->GetString(prefs::kManagedBookmarksFolderName); + if (!name.empty()) + return base::UTF8ToUTF16(name); + + const std::string domain = get_management_domain_callback_.Run(); + if (domain.empty()) { + return l10n_util::GetStringUTF16( + IDS_BOOKMARK_BAR_MANAGED_FOLDER_DEFAULT_NAME); + } else { + return l10n_util::GetStringFUTF16( + IDS_BOOKMARK_BAR_MANAGED_FOLDER_DOMAIN_NAME, + base::UTF8ToUTF16(domain)); + } + } +} + +void ManagedBookmarksTracker::ReloadManagedBookmarks() { + // In case the user just signed into or out of the account. + model_->SetTitle(managed_node_, GetBookmarksFolderTitle()); + + // Recursively update all the managed bookmarks and folders. + const base::ListValue* list = prefs_->GetList(GetPrefName()); + UpdateBookmarks(managed_node_, list); + + // The managed bookmarks folder isn't visible when that pref isn't present. + managed_node_->set_visible(!managed_node_->empty()); +} + +void ManagedBookmarksTracker::UpdateBookmarks(const BookmarkNode* folder, + const base::ListValue* list) { + int folder_index = 0; + for (size_t i = 0; i < list->GetSize(); ++i) { + // Extract the data for the next bookmark from the |list|. + base::string16 title; + GURL url; + const base::ListValue* children = NULL; + if (!LoadBookmark(list, i, &title, &url, &children)) { + // Skip this bookmark from |list| but don't advance |folder_index|. + continue; + } + + // Look for a bookmark at |folder_index| or ahead that matches the current + // bookmark from the pref. + const BookmarkNode* existing = NULL; + for (int k = folder_index; k < folder->child_count(); ++k) { + const BookmarkNode* node = folder->GetChild(k); + if (node->GetTitle() == title && + ((children && node->is_folder()) || + (!children && node->url() == url))) { + existing = node; + break; + } + } + + if (existing) { + // Reuse the existing node. The Move() is a nop if |existing| is already + // at |folder_index|. + model_->Move(existing, folder, folder_index); + if (children) + UpdateBookmarks(existing, children); + } else { + // Create a new node for this bookmark now. + if (children) { + const BookmarkNode* sub = + model_->AddFolder(folder, folder_index, title); + UpdateBookmarks(sub, children); + } else { + model_->AddURL(folder, folder_index, title, url); + } + } + + // The |folder_index| index of |folder| has been updated, so advance it. + ++folder_index; + } + + // Remove any extra children of |folder| that haven't been reused. + while (folder->child_count() != folder_index) + model_->Remove(folder->GetChild(folder_index)); +} + +// static +bool ManagedBookmarksTracker::LoadBookmark(const base::ListValue* list, + size_t index, + base::string16* title, + GURL* url, + const base::ListValue** children) { + std::string spec; + *url = GURL(); + *children = NULL; + const base::DictionaryValue* dict = NULL; + if (!list->GetDictionary(index, &dict) || + !dict->GetString(kName, title) || + (!dict->GetString(kUrl, &spec) && + !dict->GetList(kChildren, children))) { + // Should never happen after policy validation. + NOTREACHED(); + return false; + } + if (!*children) { + *url = GURL(spec); + DCHECK(url->is_valid()); + } + return true; +} + +} // namespace policy diff --git a/chromium/components/bookmarks/managed/managed_bookmarks_tracker.h b/chromium/components/bookmarks/managed/managed_bookmarks_tracker.h new file mode 100644 index 00000000000..4586aa3c8ff --- /dev/null +++ b/chromium/components/bookmarks/managed/managed_bookmarks_tracker.h @@ -0,0 +1,98 @@ +// 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. + +#ifndef COMPONENTS_BOOKMARKS_MANAGED_MANAGED_BOOKMARKS_TRACKER_H_ +#define COMPONENTS_BOOKMARKS_MANAGED_MANAGED_BOOKMARKS_TRACKER_H_ + +#include <stddef.h> +#include <stdint.h> + +#include "base/callback_forward.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/string16.h" +#include "components/prefs/pref_change_registrar.h" + +class GURL; +class PrefService; + +namespace base { +class ListValue; +} + +namespace bookmarks { + +class BookmarkModel; +class BookmarkNode; +class BookmarkPermanentNode; + +// Tracks either the Managed Bookmarks pref (set by policy) or the Supervised +// Bookmarks pref (set for a supervised user by their custodian) and makes the +// managed_node()/supervised_node() in the BookmarkModel follow the +// policy/custodian-defined bookmark tree. +class ManagedBookmarksTracker { + public: + typedef base::Callback<std::string()> GetManagementDomainCallback; + + // Shared constants used in the policy configuration. + static const char kName[]; + static const char kUrl[]; + static const char kChildren[]; + static const char kFolderName[]; + + // If |is_supervised| is true, this will track supervised bookmarks rather + // than managed bookmarks. + ManagedBookmarksTracker(BookmarkModel* model, + PrefService* prefs, + bool is_supervised, + const GetManagementDomainCallback& callback); + ~ManagedBookmarksTracker(); + + // Returns the initial list of managed bookmarks, which can be passed to + // LoadInitial() to do the initial load. + scoped_ptr<base::ListValue> GetInitialManagedBookmarks(); + + // Loads the initial managed/supervised bookmarks in |list| into |folder|. + // New nodes will be assigned IDs starting at |next_node_id|. + // Returns the next node ID to use. + static int64_t LoadInitial(BookmarkNode* folder, + const base::ListValue* list, + int64_t next_node_id); + + // Starts tracking the pref for updates to the managed/supervised bookmarks. + // Should be called after loading the initial bookmarks. + void Init(BookmarkPermanentNode* managed_node); + + bool is_supervised() const { return is_supervised_; } + + // Public for testing. + static const char* GetPrefName(bool is_supervised); + + private: + const char* GetPrefName() const; + base::string16 GetBookmarksFolderTitle() const; + + void ReloadManagedBookmarks(); + + void UpdateBookmarks(const BookmarkNode* folder, const base::ListValue* list); + static bool LoadBookmark(const base::ListValue* list, + size_t index, + base::string16* title, + GURL* url, + const base::ListValue** children); + + BookmarkModel* model_; + const bool is_supervised_; + BookmarkPermanentNode* managed_node_; + PrefService* prefs_; + PrefChangeRegistrar registrar_; + GetManagementDomainCallback get_management_domain_callback_; + + DISALLOW_COPY_AND_ASSIGN(ManagedBookmarksTracker); +}; + +} // namespace bookmarks + +#endif // COMPONENTS_BOOKMARKS_MANAGED_MANAGED_BOOKMARKS_TRACKER_H_ + diff --git a/chromium/components/bookmarks/managed/managed_bookmarks_tracker_unittest.cc b/chromium/components/bookmarks/managed/managed_bookmarks_tracker_unittest.cc new file mode 100644 index 00000000000..d03502501ad --- /dev/null +++ b/chromium/components/bookmarks/managed/managed_bookmarks_tracker_unittest.cc @@ -0,0 +1,360 @@ +// 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/bookmarks/managed/managed_bookmarks_tracker.h" + +#include <utility> + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/utf_string_conversions.h" +#include "base/thread_task_runner_handle.h" +#include "components/bookmarks/browser/bookmark_model.h" +#include "components/bookmarks/browser/bookmark_model_observer.h" +#include "components/bookmarks/browser/bookmark_node.h" +#include "components/bookmarks/browser/bookmark_utils.h" +#include "components/bookmarks/common/bookmark_pref_names.h" +#include "components/bookmarks/test/bookmark_test_helpers.h" +#include "components/bookmarks/test/mock_bookmark_model_observer.h" +#include "components/bookmarks/test/test_bookmark_client.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/testing_pref_service.h" +#include "grit/components_strings.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/l10n/l10n_util.h" +#include "url/gurl.h" + +using testing::Mock; +using testing::_; + +namespace bookmarks { + +class ManagedBookmarksTrackerTest : public testing::Test { + public: + ManagedBookmarksTrackerTest() : managed_node_(NULL) {} + ~ManagedBookmarksTrackerTest() override {} + + void SetUp() override { + RegisterManagedBookmarksPrefs(prefs_.registry()); + } + + void TearDown() override { + if (model_) + model_->RemoveObserver(&observer_); + loop_.RunUntilIdle(); + } + + void CreateModel(bool is_supervised) { + // Simulate the creation of the managed node by the BookmarkClient. + BookmarkPermanentNode* managed_node = new BookmarkPermanentNode(100); + ManagedBookmarksTracker::LoadInitial( + managed_node, + prefs_.GetList(ManagedBookmarksTracker::GetPrefName(is_supervised)), + 101); + managed_node->set_visible(!managed_node->empty()); + managed_node->SetTitle(l10n_util::GetStringUTF16( + is_supervised ? IDS_BOOKMARK_BAR_SUPERVISED_FOLDER_DEFAULT_NAME + : IDS_BOOKMARK_BAR_MANAGED_FOLDER_DEFAULT_NAME)); + + BookmarkPermanentNodeList extra_nodes; + extra_nodes.push_back(managed_node); + + scoped_ptr<TestBookmarkClient> client(new TestBookmarkClient); + client->SetExtraNodesToLoad(std::move(extra_nodes)); + model_.reset(new BookmarkModel(std::move(client))); + + model_->AddObserver(&observer_); + EXPECT_CALL(observer_, BookmarkModelLoaded(model_.get(), _)); + model_->Load(&prefs_, base::FilePath(), + base::ThreadTaskRunnerHandle::Get(), + base::ThreadTaskRunnerHandle::Get()); + test::WaitForBookmarkModelToLoad(model_.get()); + Mock::VerifyAndClearExpectations(&observer_); + + TestBookmarkClient* client_ptr = + static_cast<TestBookmarkClient*>(model_->client()); + ASSERT_EQ(1u, client_ptr->extra_nodes().size()); + managed_node_ = client_ptr->extra_nodes()[0]; + ASSERT_EQ(managed_node, managed_node_); + + managed_bookmarks_tracker_.reset(new ManagedBookmarksTracker( + model_.get(), + &prefs_, + is_supervised, + base::Bind(&ManagedBookmarksTrackerTest::GetManagementDomain))); + managed_bookmarks_tracker_->Init(managed_node_); + } + + const BookmarkNode* managed_node() { + return managed_node_; + } + + bool IsManaged(const BookmarkNode* node) { + return node && node->HasAncestor(managed_node_); + } + + static base::DictionaryValue* CreateBookmark(const std::string& title, + const std::string& url) { + EXPECT_TRUE(GURL(url).is_valid()); + base::DictionaryValue* dict = new base::DictionaryValue(); + dict->SetString("name", title); + dict->SetString("url", GURL(url).spec()); + return dict; + } + + static base::DictionaryValue* CreateFolder(const std::string& title, + base::ListValue* children) { + base::DictionaryValue* dict = new base::DictionaryValue(); + dict->SetString("name", title); + dict->Set("children", children); + return dict; + } + + static base::ListValue* CreateTestTree() { + base::ListValue* folder = new base::ListValue(); + base::ListValue* empty = new base::ListValue(); + folder->Append(CreateFolder("Empty", empty)); + folder->Append(CreateBookmark("Youtube", "http://youtube.com/")); + + base::ListValue* list = new base::ListValue(); + list->Append(CreateBookmark("Google", "http://google.com/")); + list->Append(CreateFolder("Folder", folder)); + + return list; + } + + static std::string GetManagementDomain() { + return std::string(); + } + + static std::string GetManagedFolderTitle() { + return l10n_util::GetStringUTF8( + IDS_BOOKMARK_BAR_MANAGED_FOLDER_DEFAULT_NAME); + } + + static base::DictionaryValue* CreateExpectedTree() { + return CreateFolder(GetManagedFolderTitle(), CreateTestTree()); + } + + static bool NodeMatchesValue(const BookmarkNode* node, + const base::DictionaryValue* dict) { + base::string16 title; + if (!dict->GetString("name", &title) || node->GetTitle() != title) + return false; + + if (node->is_folder()) { + const base::ListValue* children = NULL; + if (!dict->GetList("children", &children) || + node->child_count() != static_cast<int>(children->GetSize())) { + return false; + } + for (int i = 0; i < node->child_count(); ++i) { + const base::DictionaryValue* child = NULL; + if (!children->GetDictionary(i, &child) || + !NodeMatchesValue(node->GetChild(i), child)) { + return false; + } + } + } else if (node->is_url()) { + std::string url; + if (!dict->GetString("url", &url) || node->url() != GURL(url)) + return false; + } else { + return false; + } + return true; + } + + base::MessageLoop loop_; + TestingPrefServiceSimple prefs_; + scoped_ptr<BookmarkModel> model_; + MockBookmarkModelObserver observer_; + BookmarkPermanentNode* managed_node_; + scoped_ptr<ManagedBookmarksTracker> managed_bookmarks_tracker_; +}; + +TEST_F(ManagedBookmarksTrackerTest, Empty) { + CreateModel(false /* is_supervised */); + EXPECT_TRUE(model_->bookmark_bar_node()->empty()); + EXPECT_TRUE(model_->other_node()->empty()); + EXPECT_TRUE(managed_node()->empty()); + EXPECT_FALSE(managed_node()->IsVisible()); +} + +TEST_F(ManagedBookmarksTrackerTest, LoadInitial) { + // Set a policy before loading the model. + prefs_.SetManagedPref(prefs::kManagedBookmarks, CreateTestTree()); + CreateModel(false /* is_supervised */); + EXPECT_TRUE(model_->bookmark_bar_node()->empty()); + EXPECT_TRUE(model_->other_node()->empty()); + EXPECT_FALSE(managed_node()->empty()); + EXPECT_TRUE(managed_node()->IsVisible()); + + scoped_ptr<base::DictionaryValue> expected(CreateExpectedTree()); + EXPECT_TRUE(NodeMatchesValue(managed_node(), expected.get())); +} + +TEST_F(ManagedBookmarksTrackerTest, LoadInitialWithTitle) { + // Set the managed folder title. + const char kExpectedFolderName[] = "foo"; + prefs_.SetString(prefs::kManagedBookmarksFolderName, kExpectedFolderName); + // Set a policy before loading the model. + prefs_.SetManagedPref(prefs::kManagedBookmarks, CreateTestTree()); + CreateModel(false /* is_supervised */); + EXPECT_TRUE(model_->bookmark_bar_node()->empty()); + EXPECT_TRUE(model_->other_node()->empty()); + EXPECT_FALSE(managed_node()->empty()); + EXPECT_TRUE(managed_node()->IsVisible()); + + scoped_ptr<base::DictionaryValue> expected( + CreateFolder(kExpectedFolderName, CreateTestTree())); + EXPECT_TRUE(NodeMatchesValue(managed_node(), expected.get())); +} + +TEST_F(ManagedBookmarksTrackerTest, SupervisedTrackerIgnoresManagedPref) { + prefs_.SetManagedPref(prefs::kManagedBookmarks, CreateTestTree()); + CreateModel(true /* is_supervised */); + EXPECT_TRUE(managed_node()->empty()); + EXPECT_FALSE(managed_node()->IsVisible()); +} + +TEST_F(ManagedBookmarksTrackerTest, SupervisedTrackerHandlesSupervisedPref) { + prefs_.SetManagedPref(prefs::kSupervisedBookmarks, CreateTestTree()); + CreateModel(true /* is_supervised */); + EXPECT_FALSE(managed_node()->empty()); + EXPECT_TRUE(managed_node()->IsVisible()); + // Don't bother checking the actual contents, the non-supervised tests cover + // that already. +} + +TEST_F(ManagedBookmarksTrackerTest, SwapNodes) { + prefs_.SetManagedPref(prefs::kManagedBookmarks, CreateTestTree()); + CreateModel(false /* is_supervised */); + + // Swap the Google bookmark with the Folder. + scoped_ptr<base::ListValue> updated(CreateTestTree()); + scoped_ptr<base::Value> removed; + ASSERT_TRUE(updated->Remove(0, &removed)); + updated->Append(removed.release()); + + // These two nodes should just be swapped. + const BookmarkNode* parent = managed_node(); + EXPECT_CALL(observer_, BookmarkNodeMoved(model_.get(), parent, 1, parent, 0)); + prefs_.SetManagedPref(prefs::kManagedBookmarks, updated->DeepCopy()); + Mock::VerifyAndClearExpectations(&observer_); + + // Verify the final tree. + scoped_ptr<base::DictionaryValue> expected( + CreateFolder(GetManagedFolderTitle(), updated.release())); + EXPECT_TRUE(NodeMatchesValue(managed_node(), expected.get())); +} + +TEST_F(ManagedBookmarksTrackerTest, RemoveNode) { + prefs_.SetManagedPref(prefs::kManagedBookmarks, CreateTestTree()); + CreateModel(false /* is_supervised */); + + // Remove the Folder. + scoped_ptr<base::ListValue> updated(CreateTestTree()); + ASSERT_TRUE(updated->Remove(1, NULL)); + + const BookmarkNode* parent = managed_node(); + EXPECT_CALL(observer_, BookmarkNodeRemoved(model_.get(), parent, 1, _, _)); + prefs_.SetManagedPref(prefs::kManagedBookmarks, updated->DeepCopy()); + Mock::VerifyAndClearExpectations(&observer_); + + // Verify the final tree. + scoped_ptr<base::DictionaryValue> expected( + CreateFolder(GetManagedFolderTitle(), updated.release())); + EXPECT_TRUE(NodeMatchesValue(managed_node(), expected.get())); +} + +TEST_F(ManagedBookmarksTrackerTest, CreateNewNodes) { + prefs_.SetManagedPref(prefs::kManagedBookmarks, CreateTestTree()); + CreateModel(false /* is_supervised */); + + // Put all the nodes inside another folder. + scoped_ptr<base::ListValue> updated(new base::ListValue); + updated->Append(CreateFolder("Container", CreateTestTree())); + + EXPECT_CALL(observer_, BookmarkNodeAdded(model_.get(), _, _)).Times(5); + // The remaining nodes have been pushed to positions 1 and 2; they'll both be + // removed when at position 1. + const BookmarkNode* parent = managed_node(); + EXPECT_CALL(observer_, BookmarkNodeRemoved(model_.get(), parent, 1, _, _)) + .Times(2); + prefs_.SetManagedPref(prefs::kManagedBookmarks, updated->DeepCopy()); + Mock::VerifyAndClearExpectations(&observer_); + + // Verify the final tree. + scoped_ptr<base::DictionaryValue> expected( + CreateFolder(GetManagedFolderTitle(), updated.release())); + EXPECT_TRUE(NodeMatchesValue(managed_node(), expected.get())); +} + +TEST_F(ManagedBookmarksTrackerTest, RemoveAll) { + prefs_.SetManagedPref(prefs::kManagedBookmarks, CreateTestTree()); + CreateModel(false /* is_supervised */); + EXPECT_TRUE(managed_node()->IsVisible()); + + // Remove the policy. + const BookmarkNode* parent = managed_node(); + EXPECT_CALL(observer_, BookmarkNodeRemoved(model_.get(), parent, 0, _, _)) + .Times(2); + prefs_.RemoveManagedPref(prefs::kManagedBookmarks); + Mock::VerifyAndClearExpectations(&observer_); + + EXPECT_TRUE(managed_node()->empty()); + EXPECT_FALSE(managed_node()->IsVisible()); +} + +TEST_F(ManagedBookmarksTrackerTest, IsManaged) { + prefs_.SetManagedPref(prefs::kManagedBookmarks, CreateTestTree()); + CreateModel(false /* is_supervised */); + + EXPECT_FALSE(IsManaged(model_->root_node())); + EXPECT_FALSE(IsManaged(model_->bookmark_bar_node())); + EXPECT_FALSE(IsManaged(model_->other_node())); + EXPECT_FALSE(IsManaged(model_->mobile_node())); + EXPECT_TRUE(IsManaged(managed_node())); + + const BookmarkNode* parent = managed_node(); + ASSERT_EQ(2, parent->child_count()); + EXPECT_TRUE(IsManaged(parent->GetChild(0))); + EXPECT_TRUE(IsManaged(parent->GetChild(1))); + + parent = parent->GetChild(1); + ASSERT_EQ(2, parent->child_count()); + EXPECT_TRUE(IsManaged(parent->GetChild(0))); + EXPECT_TRUE(IsManaged(parent->GetChild(1))); +} + +TEST_F(ManagedBookmarksTrackerTest, RemoveAllUserBookmarksDoesntRemoveManaged) { + prefs_.SetManagedPref(prefs::kManagedBookmarks, CreateTestTree()); + CreateModel(false /* is_supervised */); + EXPECT_EQ(2, managed_node()->child_count()); + + EXPECT_CALL(observer_, + BookmarkNodeAdded(model_.get(), model_->bookmark_bar_node(), 0)); + EXPECT_CALL(observer_, + BookmarkNodeAdded(model_.get(), model_->bookmark_bar_node(), 1)); + model_->AddURL(model_->bookmark_bar_node(), + 0, + base::ASCIIToUTF16("Test"), + GURL("http://google.com/")); + model_->AddFolder( + model_->bookmark_bar_node(), 1, base::ASCIIToUTF16("Test Folder")); + EXPECT_EQ(2, model_->bookmark_bar_node()->child_count()); + Mock::VerifyAndClearExpectations(&observer_); + + EXPECT_CALL(observer_, BookmarkAllUserNodesRemoved(model_.get(), _)); + model_->RemoveAllUserBookmarks(); + EXPECT_EQ(2, managed_node()->child_count()); + EXPECT_EQ(0, model_->bookmark_bar_node()->child_count()); + Mock::VerifyAndClearExpectations(&observer_); +} + +} // namespace bookmarks |